feat: add Fresh 2 web frontend - #6
Conversation
Islands architecture with Preact + Vite + Tailwind v4: - Chat island: SSE streaming from Python backend, message history, keyboard shortcuts (Enter submit, Shift+Enter newline, ArrowUp/Down history navigation) - ControlsPanel island: collapsible sidebar with persisted open state, AI settings (thinking effort), search settings, bookmark groups - HealthStatus component: modal showing backend/vLLM reachability and env var status - Bookmark management: groups with drag-to-add URLs, checkbox filtering - API routes: /api/config (port config), /api/health (env var status) - Global CSS with custom properties for theming, custom scrollbar, animated collapsible sections, star-thumb range slider Dependencies bumped to latest: preact 10.28.4, @preact/signals 2.8.1, tailwindcss 4.2.1, vite 7.3.1, marked 17.0.3, daisyui 5.5.19. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No DaisyUI component classes are used anywhere — all styling is custom CSS and Tailwind utilities. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR adds a complete Fresh 2 web frontend for the Librarian RAG application with SSE streaming chat, collapsible controls panel, bookmark management, and health monitoring. Key additions:
Issues found:
Dependencies updated: Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Chat as Chat Island
participant Backend as Python Backend
participant vLLM
participant Health as Health Modal
participant Controls as ControlsPanel
User->>Chat: Submit query (Enter)
Chat->>Backend: POST /api/query (SSE)
Backend->>vLLM: Process query
Backend-->>Chat: event: node (RouterNode)
Backend-->>Chat: event: node (ResearchNode)
Backend-->>Chat: event: answer + references
Chat->>Chat: Update messageHistory
Chat->>User: Display answer + references
User->>Controls: Click health status
Controls->>Health: Open modal
Health->>Backend: GET /api/health
Backend->>vLLM: Check health
Backend-->>Health: Service status
Health->>Chat: GET /api/health (frontend)
Chat-->>Health: Env var status
Health->>User: Show health details
User->>Controls: Configure bookmarks
Controls->>Controls: Update localStorage
User->>Chat: Submit filtered query
Chat->>Backend: POST with allowed_sites
Backend-->>Chat: Filtered results
Last reviewed commit: 2bbc5cf |
| if (line.startsWith("event:")) { | ||
| eventType = line.slice(6).trim(); | ||
| } else if (line.startsWith("data:")) { | ||
| const data = JSON.parse(line.slice(5).trim()); |
There was a problem hiding this comment.
Wrap in try-catch to prevent crashes from malformed SSE data
| const data = JSON.parse(line.slice(5).trim()); | |
| try { | |
| const data = JSON.parse(line.slice(5).trim()); |
| for (const line of lines) { | ||
| if (line.startsWith("event:")) { | ||
| eventType = line.slice(6).trim(); | ||
| } else if (line.startsWith("data:")) { | ||
| const data = JSON.parse(line.slice(5).trim()); | ||
| if (eventType === "node") { | ||
| currentNode.value = data.node_type; | ||
| } else if (eventType === "answer") { | ||
| messageHistory.value = [ | ||
| ...messageHistory.value, | ||
| { | ||
| query: userQuery, | ||
| answer: String(data.answer), | ||
| references: data.references || [], | ||
| }, | ||
| ]; | ||
| currentNode.value = null; | ||
| currentQuery.value = null; | ||
| } else if (eventType === "error") { | ||
| messageHistory.value = [ | ||
| ...messageHistory.value, | ||
| { | ||
| query: userQuery, | ||
| answer: `Error: ${data.message}`, | ||
| references: [], | ||
| }, | ||
| ]; | ||
| currentNode.value = null; | ||
| currentQuery.value = null; | ||
| } | ||
| } |
There was a problem hiding this comment.
JSON parsing can crash if SSE data is malformed. Wrap the parse and data handling in a try-catch:
for (const line of lines) {
if (line.startsWith("event:")) {
eventType = line.slice(6).trim();
} else if (line.startsWith("data:")) {
try {
const data = JSON.parse(line.slice(5).trim());
if (eventType === "node") {
currentNode.value = data.node_type;
} else if (eventType === "answer") {
// ... rest of answer handling
} else if (eventType === "error") {
// ... rest of error handling
}
} catch (parseErr) {
console.error("Failed to parse SSE data:", parseErr);
// Continue processing other lines
}
}
}| <HealthStatus /> | ||
| <ControlRow | ||
| label="Baseline Thinking Effort" | ||
| description="Effects the time it takes to answer queries." |
There was a problem hiding this comment.
"Effects" should be "Affects"
| description="Effects the time it takes to answer queries." | |
| description="Affects the time it takes to answer queries." |
- Fix "Effects" → "Affects" in thinking effort description - Wrap SSE JSON.parse in try-catch so malformed data is skipped rather than crashing the stream handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nodeModulesDir=manual requires an explicit install step so transitive npm dependencies are present in node_modules before type-checking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prose line wrapping in markdown adds no value. Exclude *.md from deno fmt so README can be written with natural line lengths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR introduces a comprehensive Fresh 2 web frontend for the Librarian chatbot, replacing the previous minimal setup with a full-featured interface. The implementation uses Preact signals for reactive state management, SSE streaming for real-time responses, and localStorage for session persistence. Key changes:
Issues found:
Confidence Score: 4/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
User[User Browser]
Chat[Chat Island]
Controls[ControlsPanel Island]
AppState[appState.ts<br/>Persisted Signals]
User -->|Enter query| Chat
User -->|Adjust settings| Controls
Chat -->|Read/Write| AppState
Controls -->|Read/Write| AppState
Chat -->|SSE stream| Backend[Python Backend<br/>localhost:8001]
Chat -->|GET /api/config| ConfigAPI[config.ts route]
Controls -->|GET /api/health| HealthAPI[health.ts route]
Backend -->|node events| Chat
Backend -->|answer events| Chat
Backend -->|error events| Chat
ConfigAPI -->|Read| Config[librarian.config.json]
HealthAPI -->|Read| EnvSchema[env.schema.json]
HealthAPI -->|Check env vars| Deno[Deno.env]
AppState -->|Persist| LocalStorage[(localStorage)]
Chat -->|Render| Answer[Answer Component<br/>marked parse]
Chat -->|Render| Loading[LoadingIndicator]
Controls -->|Manage| Bookmarks[Bookmark Groups]
Bookmarks -->|Filter search| Backend
Last reviewed commit: f0611ac |
| } catch (err) { | ||
| if (err.name !== "AbortError") { |
There was a problem hiding this comment.
err is implicitly any, needs type guard
| } catch (err) { | |
| if (err.name !== "AbortError") { | |
| } catch (err) { | |
| if (err instanceof Error && err.name !== "AbortError") { |
Accessing .name/.message on an untyped catch variable is unsafe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
/api/health/api/config(readslibrarian.config.jsonfor port),/api/health(env var status fromenv.schema.json)Changes vs initial code
ControlSectionTODO:onTogglenow syncs DOM open state back to signal; section state persisted viacreatePersistedSignalCtrl+Z(was overriding textarea undo) andCtrl+R(couldn't prevent browser refresh) shortcutsmarkedsanitization comment —markeddoes not sanitize HTML; content comes from the trusted local backendTest plan
deno task devstarts the Vite dev server🤖 Generated with Claude Code