Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 48 additions & 11 deletions web/src/components/research/ResearchDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,48 @@ type ConfirmAction =
| { type: "bulk_move"; itemIds: number[]; targetTrackId: number }
| { type: "clear_track_memory"; trackId: number }

type UpstreamErrorBody = {
detail?: string
error?: string
}

function toFriendlyErrorMessage(status: number, rawText: string): string | null {
if (!rawText) return null

let parsed: UpstreamErrorBody | null = null
try {
parsed = JSON.parse(rawText) as UpstreamErrorBody
} catch {
parsed = null
}

const detail = parsed && typeof parsed.detail === "string" ? parsed.detail : undefined

if (detail && (detail.includes("Upstream API unreachable") || detail.includes("Upstream API timed out"))) {
if (detail.includes("timed out")) {
return "Unable to connect to service (request timed out). Please ensure the backend is running. Please try again."
}
return "Unable to connect to service. Please ensure the backend is running."
}

// Fallback: surface backend-provided detail when available
if (detail) return detail

return null
}

async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, init)
if (!res.ok) {
const text = await res.text().catch(() => "")
throw new Error(`${res.status} ${res.statusText} ${text}`.trim())
const friendly = toFriendlyErrorMessage(res.status, text)
if (friendly) {
throw new Error(friendly)
}
const statusLabel = `${res.status} ${res.statusText}`.trim()
const base = statusLabel || "Request failed"
const message = text ? `${base} ${text}`.trim() : base
throw new Error(message)
}
return res.json() as Promise<T>
}
Comment on lines +93 to 137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This block of code introduces helpful error handling, but it has a couple of issues:

  1. Code Duplication: The UpstreamErrorBody type, toFriendlyErrorMessage function, and fetchJson function are duplicated in ResearchDiscoveryPage.tsx and ResearchPageNew.tsx. This should be extracted to a shared utility file (e.g., in web/src/lib/) to adhere to the DRY principle and improve maintainability.
  2. Unused Parameter: The status parameter in toFriendlyErrorMessage (line 98) is not used. It should be removed from both the function definition and its call site on line 127 to keep the code clean.

Expand Down Expand Up @@ -219,7 +256,7 @@ export default function ResearchDashboard() {

useEffect(() => {
setError(null)
refreshTracks().catch((e) => setError(String(e)))
refreshTracks().catch((e) => setError(e instanceof Error ? e.message : String(e)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pattern e instanceof Error ? e.message : String(e) is used to extract an error message in many places in this file and others in this pull request. To improve maintainability and reduce code duplication, consider creating a shared utility function for this logic.

For example:

const getErrorMessage = (error: unknown): string => {
  return error instanceof Error ? error.message : String(error);
};

This can then be used as setError(getErrorMessage(e)) in all catch blocks.

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

Expand Down Expand Up @@ -272,7 +309,7 @@ export default function ResearchDashboard() {
await refreshTracks()
}
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand All @@ -297,7 +334,7 @@ export default function ResearchDashboard() {
setSuggestText("")
await refreshInbox(activeTrackId)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand All @@ -320,7 +357,7 @@ export default function ResearchDashboard() {
})
await refreshInbox(activeTrackId)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand All @@ -344,7 +381,7 @@ export default function ResearchDashboard() {
})
await refreshInbox(activeTrackId)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand Down Expand Up @@ -383,7 +420,7 @@ export default function ResearchDashboard() {
const activeId = await refreshTracks()
await refreshInbox(activeId)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand Down Expand Up @@ -421,7 +458,7 @@ export default function ResearchDashboard() {
})
await buildContext(false)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand All @@ -437,7 +474,7 @@ export default function ResearchDashboard() {
)
await refreshInbox(trackId)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand Down Expand Up @@ -936,7 +973,7 @@ export default function ResearchDashboard() {
/>
<Button
variant="outline"
onClick={() => refreshEval().catch((e) => setError(String(e)))}
onClick={() => refreshEval().catch((e) => setError(e instanceof Error ? e.message : String(e)))}
disabled={loading}
>
Refresh
Expand All @@ -952,7 +989,7 @@ export default function ResearchDashboard() {
</div>

{!evalSummary ? (
<Button variant="secondary" onClick={() => refreshEval().catch((e) => setError(String(e)))} disabled={loading}>
<Button variant="secondary" onClick={() => refreshEval().catch((e) => setError(e instanceof Error ? e.message : String(e)))} disabled={loading}>
Load Summary
</Button>
) : (
Expand Down
43 changes: 40 additions & 3 deletions web/src/components/research/ResearchDiscoveryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,48 @@ import type { Track } from "./TrackSelector"

type SeedType = "doi" | "arxiv" | "openalex" | "semantic_scholar" | "author"

type UpstreamErrorBody = {
detail?: string
error?: string
}

function toFriendlyErrorMessage(status: number, rawText: string): string | null {
if (!rawText) return null

let parsed: UpstreamErrorBody | null = null
try {
parsed = JSON.parse(rawText) as UpstreamErrorBody
} catch {
parsed = null
}

const detail = parsed && typeof parsed.detail === "string" ? parsed.detail : undefined

if (detail && (detail.includes("Upstream API unreachable") || detail.includes("Upstream API timed out"))) {
if (detail.includes("timed out")) {
return "Unable to connect to service (request timed out). Please ensure the backend is running. Please try again."
}
return "Unable to connect to service. Please ensure the backend is running."
}

// Fallback: surface backend-provided detail when available
if (detail) return detail

return null
}

async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, init)
if (!res.ok) {
const text = await res.text().catch(() => "")
throw new Error(`${res.status} ${res.statusText} ${text}`.trim())
const friendly = toFriendlyErrorMessage(res.status, text)
if (friendly) {
throw new Error(friendly)
}
const statusLabel = `${res.status} ${res.statusText}`.trim()
const base = statusLabel || "Request failed"
const message = text ? `${base} ${text}`.trim() : base
throw new Error(message)
}
return res.json() as Promise<T>
}
Expand Down Expand Up @@ -54,7 +91,7 @@ export default function ResearchDiscoveryPage() {
)

useEffect(() => {
refreshTracks().catch((err) => setError(String(err)))
refreshTracks().catch((err) => setError(err instanceof Error ? err.message : String(err)))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

Expand Down Expand Up @@ -89,7 +126,7 @@ export default function ResearchDiscoveryPage() {
)
await refreshTracks()
} catch (err) {
setError(String(err))
setError(err instanceof Error ? err.message : String(err))
} finally {
setLoading(false)
}
Expand Down
51 changes: 44 additions & 7 deletions web/src/components/research/ResearchPageNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,48 @@ type ContextPack = {
paper_recommendation_reasons?: Record<string, string[]>
}

type UpstreamErrorBody = {
detail?: string
error?: string
}

function toFriendlyErrorMessage(status: number, rawText: string): string | null {
if (!rawText) return null

let parsed: UpstreamErrorBody | null = null
try {
parsed = JSON.parse(rawText) as UpstreamErrorBody
} catch {
parsed = null
}

const detail = parsed && typeof parsed.detail === "string" ? parsed.detail : undefined

if (detail && (detail.includes("Upstream API unreachable") || detail.includes("Upstream API timed out"))) {
if (detail.includes("timed out")) {
return "Unable to connect to service (request timed out). Please ensure the backend is running. Please try again."
}
return "Unable to connect to service. Please ensure the backend is running."
}

// Fallback: surface backend-provided detail when available
if (detail) return detail

return null
}

async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, init)
if (!res.ok) {
const text = await res.text().catch(() => "")
throw new Error(`${res.status} ${res.statusText} ${text}`.trim())
const friendly = toFriendlyErrorMessage(res.status, text)
if (friendly) {
throw new Error(friendly)
}
const statusLabel = `${res.status} ${res.statusText}`.trim()
const base = statusLabel || "Request failed"
const message = text ? `${base} ${text}`.trim() : base
throw new Error(message)
}
return res.json() as Promise<T>
}
Expand Down Expand Up @@ -115,7 +152,7 @@ export default function ResearchPageNew() {

// Load tracks on mount
useEffect(() => {
refreshTracks().catch((e) => setError(String(e)))
refreshTracks().catch((e) => setError(e instanceof Error ? e.message : String(e)))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

Expand Down Expand Up @@ -157,7 +194,7 @@ export default function ResearchPageNew() {
)
await refreshTracks()
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand Down Expand Up @@ -207,7 +244,7 @@ export default function ResearchPageNew() {

setContextPack(data.context_pack)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setIsSearching(false)
}
Expand Down Expand Up @@ -278,7 +315,7 @@ export default function ResearchPageNew() {
await refreshTracks()
return true
} catch (e) {
const message = String(e)
const message = e instanceof Error ? e.message : String(e)
if (message.startsWith("409")) {
setCreateError(`Track "${name}" already exists.`)
} else {
Expand Down Expand Up @@ -324,7 +361,7 @@ export default function ResearchPageNew() {
await refreshTracks()
return true
} catch (e) {
const message = String(e)
const message = e instanceof Error ? e.message : String(e)
if (message.startsWith("409")) {
setEditError(`Track "${name}" already exists.`)
} else {
Expand Down Expand Up @@ -358,7 +395,7 @@ export default function ResearchPageNew() {
setConfirmClearOpen(false)
setTrackToClear(null)
} catch (e) {
setError(String(e))
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
Expand Down
Loading