Skip to content

feat(app): add guided learning, inline citations, and unified streaming - #6

Merged
WilliamAGH merged 62 commits into
mainfrom
dev
Jan 25, 2026
Merged

feat(app): add guided learning, inline citations, and unified streaming#6
WilliamAGH merged 62 commits into
mainfrom
dev

Conversation

@WilliamAGH

@WilliamAGHWilliamAGH commented Jan 24, 2026

Copy link
Copy Markdown
Owner

Summary

Introduces a comprehensive Guided Learning module with interactive lessons and scoped chat, refactors the citation system to stream sources inline with responses, and unifies SSE streaming logic across frontend and backend for improved reliability and performance. Includes a new phase-aware Thinking Indicator and significant clean code improvements.

Changes by Category

Features

  • Guided Learning: Added LearnView with lesson table of contents, markdown content rendering, and context-aware chat.
  • Inline Citations: Refactored citation system to stream sources directly with chat responses (eliminating separate API calls) and added a collapsible CitationPanel.
  • UI Enhancements: Added ThinkingIndicator with "Warm Precision" aesthetic (connecting, searching, generating phases) and smooth cursor fade-out animations.
  • Mobile Experience: Implemented a slide-up chat drawer for mobile guided learning.

Refactoring

  • Frontend Utilities: Extracted shared logic for SSE parsing (sse.ts), scrolling (scroll.ts), URL sanitization (url.ts), and syntax highlighting (highlight.ts).
  • Backend SSE: Unified streaming logic into SseSupport and SseConstants, implementing consistent error handling and backpressure boundaries.
  • Service Layer: Refactored RateLimitHeaderParser, OpenAiSdkUrlNormalizer, and DocsIngestionService to reduce duplication and improve testability.
  • Markdown Parsing: Extracted InlineListParser and OrderedMarkerScanner for better separation of concerns.

Bug Fixes

  • Streaming Reliability: Fixed text decoding issues for multi-byte characters and added bounded backpressure to prevent memory leaks.
  • Security: Implemented strict URL sanitization to prevent XSS (blocking javascript:, data:, and protocol-relative URLs).
  • Race Conditions: Added guards for lesson selection and citation fetching to prevent stale data overwrites.
  • Accessibility: Fixed ARIA ID conflicts in citation panels.

Testing

  • Added unit tests for InlineListParser, OpenAiSdkUrlNormalizer, and GuidedLearningController.

Technical Details

  • Language: TypeScript (Frontend), Java (Backend)
  • Framework: Svelte (Frontend), Spring Boot (Backend)
  • Performance: Reduced network requests by streaming citations inline; optimized SSE parsing and scrolling logic.

Related Issues

…nses
Assistant responses now show their source citations, enabling users to verify
information and explore related documentation. The panel categorizes citations
by type (PDF, API docs, repository, external links) with distinctive icons
and styling for each. Citations are fetched asynchronously based on the user's
query and deduplicated by URL to avoid showing the same source multiple times.
- Create CitationPanel component with type-aware icons and styling
- Track user query on assistant messages for citation lookup
- Integrate CitationPanel below non-error assistant messages in ChatView
- Add wrapper styling to support citation panel layout
Implement the service layer for guided learning functionality. This provides
typed interfaces and API functions for fetching the lesson table of contents,
individual lesson metadata, lesson content as markdown, and streaming chat
responses scoped to a specific lesson context.
- Define GuidedLesson and LessonContentResponse interfaces
- Add fetchTOC, fetchLesson, and fetchLessonContent for lesson data
- Implement streamGuidedChat with SSE parsing, error handling, and cleanup
- Add GuidedCitation type and fetchGuidedCitations for lesson-specific sources
Replace the "coming soon" placeholder with a full guided learning interface.
Users can browse lessons from a table of contents, view lesson content with
syntax-highlighted code blocks, and ask questions in a contextual chat panel
that provides lesson-specific responses.
- Create LearnView component with TOC grid and lesson reader
- Add two-column layout: lesson content panel with markdown rendering
- Integrate chat panel scoped to selected lesson context
- Support code highlighting with highlight.js for Java, XML, JSON, bash
- Wire up App.svelte to render LearnView instead of placeholder
- Add placeholder prop to ChatInput for contextual prompts
@WilliamAGHWilliamAGH self-assigned this Jan 24, 2026
CopilotAI review requested due to automatic review settings January 24, 2026 08:24
@coderabbitai

coderabbitaiBot commented Jan 24, 2026

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Guided learning view with lesson browser and two-pane lesson + chat experience
    • Inline citations panel showing source links for assistant responses
    • Multi-phase streaming status UI for clearer progress (connecting/searching/generating)
    • Improved syntax highlighting and debounced code highlighter
  • Bug Fixes

    • Better streaming error messages surfaced in the chat
    • Auto-scroll respects user scrolling during live updates
  • Improvements

    • Customizable chat input placeholder
    • Visual copy-to-clipboard feedback
    • Added docs describing local data directories and startup requirements

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds a guided learning UI with per-lesson streaming chat and inline citations, introduces unified SSE streaming utilities on frontend and backend, new citation and thinking UI components, and multiple frontend/backend utilities and parsing improvements.

Changes

Cohort / File(s)Summary
Guided Learning
frontend/src/lib/components/LearnView.svelte, frontend/src/lib/services/guided.ts, frontend/src/App.svelte
New LearnView component and guided service: TOC/lesson fetch, markdown content, per-lesson streaming chat, citation handling, lesson-scoped state, and App wiring. Attention: lesson race guards, chat/session lifecycle.
Inline Citations UI
frontend/src/lib/components/CitationPanel.svelte, frontend/src/lib/components/ThinkingIndicator.svelte
New collapsible CitationPanel and phase-aware ThinkingIndicator providing UI for citations and streaming phases; accessible IDs and deduplication logic.
Chat UI & Message Handling
frontend/src/lib/components/ChatView.svelte, frontend/src/lib/components/ChatInput.svelte, frontend/src/lib/components/MessageBubble.svelte, frontend/src/lib/components/ThinkingIndicator.svelte
ChatView refactor for streaming chunks, pendingCitations, stream status lifecycle, auto-scroll; ChatInput accepts placeholder prop; MessageBubble adds debounced highlighting and copy feedback. Attention: stream lifecycle and auto-scroll behavior.
Frontend SSE & Stream Types
frontend/src/lib/services/sse.ts, frontend/src/lib/services/stream-types.ts, frontend/src/lib/services/chat.ts
Centralized SSE parser streamSse with callbacks (onText/onStatus/onCitations), shared StreamStatus/StreamError types; chat service simplified to use streamSse and added onCitations/fetchCitations types. Review: event parsing and error propagation.
Citation/url utilities & highlighting/scroll utilities
frontend/src/lib/utils/url.ts, frontend/src/lib/utils/highlight.ts, frontend/src/lib/utils/scroll.ts, frontend/src/lib/utils/highlight.ts
URL sanitization/classification/display helpers, deduplication; lazy, debounced highlight.js loader; scroll manager for auto-scroll. Security-sensitive code in url.ts—review sanitization rules.
Frontend styles & global vars
frontend/src/styles/global.css
New animation variables and keyframes; header-height responsive vars.
Backend SSE support & constants
src/main/java/.../SseSupport.java, src/main/java/.../SseConstants.java
New SseSupport component for SSE event builders, heartbeats, header configuration, and shared payload records; SseConstants centralizes event names and settings. Review: serialization fallbacks and heartbeat config.
Controllers using SSE support
src/main/java/.../ChatController.java, src/main/java/.../GuidedLearningController.java
Controllers updated to inject/use SseSupport, return ServerSentEvent streams, emit citation events and use unified error/status events. Attention: constructor signature changes and wiring.
Backend: chat & prompt outcome changes
src/main/java/.../service/ChatService.java
ChatPromptOutcome now includes retrieved source Documents (public API change within service layer); call sites updated.
Docs ingestion & URL resolution
src/main/java/.../service/DocsIngestionService.java, src/main/java/.../config/DocsSourceRegistry.java
Store-with-retry flow, local-to-remote path resolution via resolveLocalPath. Review: retry and fallback behaviors.
Markdown parsing & inline list parsing
src/main/java/.../service/markdown/InlineListParser.java, OrderedMarkerScanner.java, UnifiedMarkdownService.java, markdown/*
New inline list parser and ordered marker scanner; markdown normalization refactors and enrichment fence-aware parsing. Attention: correctness of parser edge cases and tests.
Various backend utilities/refactors & tests
many files (OpenAiSdkUrlNormalizer, RateLimitHeaderParser/Manager/State, RetrySupport, OpenAIStreamingService, Embedding model factory changes, tests, Dockerfile, README, spotbugs-exclude.xml)
Numerous refactors: OpenAI URL normalization, rate-limit parsing/formatting, retry sanitization, embedding factory, added/preserved tests, Dockerfile/data-dir changes, SpotBugs suppressions. Review: behavior of rate-limit persistence, normalized URLs, and SpotBugs exclusions.

Sequence Diagram(s)

sequenceDiagram
participant User
participant Frontend as Frontend (LearnView / ChatView)
participant SSE as SSE Stream (streamSse)
participant Backend as Backend (SseSupport + Controller)
participant OpenAI as OpenAI
participant Vector as Vector Store
User->>Frontend: Open LearnView / select lesson
Frontend->>Backend: GET TOC / fetchLessonContent
Backend->>Vector: query lessons/content
Vector-->>Backend: lessons/markdown
Backend-->>Frontend: GuidedLesson / Markdown
User->>Frontend: Send chat message
Frontend->>Frontend: append user message, set streaming UI
Frontend->>SSE: POST streamGuidedChat (streamSse)
rect rgba(100, 200, 255, 0.5)
SSE->>Backend: deliver SSE POST
Backend->>OpenAI: stream with retrieval/context
OpenAI-->>Backend: text chunks + citation info
Backend->>Backend: prepare text/status/citation events via SseSupport
Backend-->>SSE: emit SSE events (text/status/citation + heartbeats)
end
SSE-->>Frontend: processEvent -> onText/onStatus/onCitations
Frontend->>Frontend: render chunks, accumulate citations, show ThinkingIndicator
Frontend->>Frontend: finalize assistant message and show CitationPanel
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

✨ A lesson opens, streams begin,
Sources flow like a friendly wind.
Thinking lights that softly glow,
Code highlights learning as you go.
Citations tuck the answers in—hooray, let’s spin!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.92% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(app): add guided learning, inline citations, and unified streaming' accurately summarizes the three main features added in this comprehensive changeset.
Description check✅ PassedThe description is well-structured, organized by category, and directly relates to the extensive code changes across frontend and backend systems.
Linked Issues check✅ PassedThe changeset implements all major objectives from issue #6: Guided Learning (LearnView) [✓], inline citations with CitationPanel [✓], unified SSE streaming (SseSupport/SseConstants) [✓], ThinkingIndicator UI [✓], frontend utilities extraction [✓], markdown parsing components [✓], service-layer refactors [✓], URL sanitization [✓], and comprehensive tests [✓].
Out of Scope Changes check✅ PassedAll changes align with the stated objectives; no extraneous modifications detected. Even supporting refactors (Locale handling, logging improvements, SpotBugs suppressions) serve the primary features and code quality goals outlined in issue #6.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot changed the title mergeAdd guided learning with citations and streaming chat supportJan 24, 2026

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@frontend/src/lib/components/LearnView.svelte`:
- Around line 66-89: The selectLesson function can suffer a race where an
earlier fetch resolves after a later one and overwrites state; fix by capturing
the requested slug locally (e.g., const requestSlug = lesson.slug) at the top of
selectLesson and, after each async response (both fetchLessonContent and
fetchCitations), check that the current selected identifier
(selectedLesson?.slug or a dedicated selectedSlug store) still equals
requestSlug before assigning lessonMarkdown, lessonCitations, lessonError, or
changing loadingLesson; alternatively implement a cancellable token that is
compared before applying results so only the latest request updates state.
In `@frontend/src/lib/services/guided.ts`:
- Around line 149-154: The error extraction assumes a trailing space by using
payload.slice(8) after the startsWith('[ERROR]') check, which will drop the
first character when the server sends '[ERROR]No space here'; update the
extraction to use payload.slice(7).trim() (keep the same startsWith('[ERROR]')
guard and the existing onError?.(serverError) and throw serverError behavior) so
you correctly capture the message whether or not there is a space after the
marker.
🧹 Nitpick comments (3)
frontend/src/lib/components/ChatView.svelte (1)

58-61: Quick observation on currentUserQuery 🤔

I notice currentUserQuery is set on line 61 but doesn't appear to be used anywhere in the template or other functions—only reset in the finally block. If it's reserved for future features (like showing "Answering: {query}" during streaming), that's cool! Otherwise, you could simplify by removing it since userQuery local variable already tracks the query through the async flow.

frontend/src/lib/components/CitationPanel.svelte (1)

101-131: Potential race condition on rapid query changes 🏃‍♂️

Here's a fun learning moment! If the user types quickly (triggering multiple query changes), older fetch responses could arrive after newer ones and overwrite the correct citations. Consider using an AbortController to cancel stale requests:

🔧 Optional enhancement with AbortController
 $effect(() => {
const currentQuery = query
if (!currentQuery || !currentQuery.trim()) {
citations = []
hasFetched = false
return
}
+ const controller = new AbortController()
loading = true
hasFetched = false
- fetchCitations(currentQuery)+ fetchCitations(currentQuery, { signal: controller.signal })
.then((result) => {
// Deduplicate by URL
const seen = new Set<string>()
citations = result.filter((citation) => {
const key = citation.url?.toLowerCase() ?? ''
if (seen.has(key)) return false
seen.add(key)
return true
})
})
.catch((fetchError) => {
+ if (fetchError.name === 'AbortError') return
console.warn('Failed to fetch citations:', fetchError)
citations = []
})
.finally(() => {
loading = false
hasFetched = true
})
+ return () => controller.abort()
})

This is a nice-to-have—in practice, citation fetches are usually fast enough that users won't notice!

frontend/src/lib/components/LearnView.svelte (1)

11-14: Duplicate MessageWithQuery type 📋

I spy with my little eye... this same interface in ChatView.svelte! Consider extracting it to a shared types file (like types.ts or alongside the chat service) to keep things DRY. Not urgent, but it'll save future-you from updating two places!

💡 Example shared location
// In frontend/src/lib/services/chat.ts or a new types.tsexportinterfaceMessageWithQueryextendsChatMessage{queryForCitations?: string}

Then import it in both components.

Comment threadfrontend/src/lib/components/LearnView.svelte
Comment threadfrontend/src/lib/services/guided.ts Outdated
…cking
Citation fetching was masking failures by silently returning empty arrays,
making it impossible to distinguish "no results" from "fetch failed". This
refactors error handling to surface failures explicitly while extracting
magic strings into named constants and simplifying type detection logic.
- Add fetchError state to CitationPanel with error indicator UI
- Return Result type from fetchGuidedCitations for explicit success/failure
- Extract URL protocol constants (HTTP, HTTPS, LOCAL_PATH_PREFIX, etc.)
- Replace 6+ case if-else with pattern-matching helpers (matchesPatterns)
- Log lesson citation errors with context instead of swallowing silently
- Remove unused currentUserQuery from ChatView (dead code cleanup)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:3cb16b978b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadfrontend/src/lib/components/CitationPanel.svelte Outdated
Comment threadfrontend/src/lib/components/LearnView.svelte

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a Guided Learning experience to the frontend, including lesson TOC browsing, lesson content rendering, and an in-context chat UI with per-answer citations.

Changes:

  • Introduces a guided-learning API client (guided.ts) for TOC, lesson content, citations, and streaming chat.
  • Adds a new LearnView UI for selecting lessons, viewing lesson markdown, and chatting about a lesson.
  • Adds a reusable CitationPanel and integrates it into both ChatView and LearnView; enhances ChatInput to accept a configurable placeholder.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
FileDescription
frontend/src/lib/services/guided.tsNew guided-learning service wrapper, including an SSE streaming helper for guided chat.
frontend/src/lib/components/LearnView.svelteNew guided learning UI (TOC + lesson + chat layout) with markdown rendering and code highlighting.
frontend/src/lib/components/CitationPanel.svelteNew component to fetch and render citations for a query.
frontend/src/lib/components/ChatView.svelteAdds citations rendering per assistant message and tracks query per response.
frontend/src/lib/components/ChatInput.svelteAdds placeholder prop support with a default value.
frontend/src/App.svelteReplaces “coming soon” guided-learning placeholder with the real LearnView.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadfrontend/src/lib/services/guided.ts Outdated
Comment threadfrontend/src/lib/services/guided.ts Outdated
Comment threadfrontend/src/lib/components/LearnView.svelte Outdated
Comment threadfrontend/src/lib/components/LearnView.svelte Outdated
Comment threadfrontend/src/lib/components/CitationPanel.svelte Outdated
Comment threadfrontend/src/lib/components/ChatView.svelte Outdated
Comment threadfrontend/src/lib/components/CitationPanel.svelte Outdated
The guided learning stream endpoint was sending plain text SSE events while
ChatController uses JSON-wrapped events. This inconsistency caused whitespace
to be stripped from guided chat responses. By matching the ChatController
pattern, whitespace in code blocks and formatted content is now preserved.
Backend changes (GuidedLearningController):
- Inject ObjectMapper and add JSON serialization helpers
- Return Flux<ServerSentEvent<String>> instead of Flux<String>
- Wrap text chunks in ChunkMessage records with proper event types
- Use structured ErrorMessage records for error events
- Fix backpressure handling (onBackpressureBuffer + share)
- Add SSE event type constants for consistency
Frontend changes (guided.ts):
- Remove unused GuidedCitation type and CitationFetchResult (dead code)
- Add event type tracking and multi-line event buffering
- Parse JSON-wrapped payloads via tryParseJson helper
- Handle "error" event type with proper error propagation
…son sources
The citation panel was always expanded, taking up vertical space even when users
didn't need to reference sources. The lesson-level citation fetch was also
discarding results without displaying them. This redesigns the panel as a
collapsible pill and completes the lesson sources feature.
CitationPanel.svelte:
- Convert from always-visible panel to collapsible disclosure button
- Replace if/else type detection with rule-based TYPE_DETECTION_RULES array
- Add expand/collapse state with animated chevron rotation
- Modernize styling with smaller badges, refined spacing, dark mode support
- Remove rarely-used snippet display for cleaner UI
LearnView.svelte:
- Add lessonCitations state to store fetched results (was discarding them)
- Add lessonCitationsLoaded flag for proper loading state
- Add lessonCitationsError for error visibility (not silent)
- Display "Lesson Sources" section below lesson content
- Reset citation state on lesson change and back navigation
markdown.ts:
- Clarify whitespace preservation comment in enrichment extension
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 24, 2026 09:00 Inactive
CitationPanel.svelte:
- Restore API_DOC_PATTERNS and REPO_PATTERNS named constants (CS7)
- Replace TYPE_DETECTION_RULES engine with simple if/else (KISS principle)
- Remove duplicate function definitions
GuidedLearningController.java:
- Add comment explaining why SSE streams need error events
- Sanitize error details sent to client for security
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 24, 2026 09:01 Inactive
When SSE streams end, multi-byte UTF-8 characters (emoji, CJK) may be split
across network chunks. The TextDecoder holds partial bytes internally when
using stream mode. Without a final flush via decode() without arguments,
these trailing bytes would be silently dropped, corrupting the last character.
- Add decoder.decode() call on stream completion in chat.ts
- Add decoder.decode() call on stream completion in guided.ts
- Append flushed bytes to buffer before processing remaining content
PR review identified multiple issues: async citation fetches could resolve
out of order causing stale data to overwrite fresh results; URLs from the
backend lacked scheme validation allowing potential XSS via javascript:,
data:, or vbscript: URIs; highlight.js effect cleanup didn't properly
detect unmount during async load.
Race condition fix:
- Add pendingRequestId counter to track active requests
- Ignore responses from superseded requests in CitationPanel
XSS prevention:
- Add sanitizeUrlScheme() to validate URLs use http/https or relative paths
- Apply sanitization in buildFullUrl() before rendering href attributes
- Add sanitizeCitationUrl() to LearnView for lesson source links
Effect cleanup fix:
- Add isCancelled flag with cleanup function in highlight.js effect
- Verify element reference hasn't changed during async load

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@frontend/src/lib/components/CitationPanel.svelte`:
- Around line 153-174: The fixed id "citation-list" causes duplicate IDs when
multiple CitationPanel instances render; change to a unique per-instance id
(e.g., build an id string in the component and use it where "citation-list"
appears). Generate the id in the component (module-level counter or random
suffix) and replace the hard-coded id in the <ul id="citation-list"> and the
button's aria-controls="citation-list" so both reference the generated id; keep
all existing bindings (isExpanded, citations) untouched and ensure the same
generated id is used for both attributes.
In `@frontend/src/lib/components/LearnView.svelte`:
- Around line 111-184: When starting a stream in handleSend, capture the lesson
slug and an AbortController (or similar cancel token) and pass it into
streamGuidedChat; in the onChunk/onError handlers and before appending the final
assistant message check that the current selectedLesson.slug (or the captured
slug) still matches and/or that the controller hasn't been aborted so chunks
aren't applied to a different view, and abort the controller in goBack to cancel
any in‑flight stream and also reset isStreaming/currentStreamingContent there;
update references to selectedLesson.slug, currentStreamingContent, isStreaming,
streamGuidedChat, handleSend and goBack accordingly to implement the guard and
cancellation.
In
`@src/main/java/com/williamcallahan/javachat/web/GuidedLearningController.java`:
- Around line 77-85: Rename the jsonSerialize method parameter from the banned
identifier "value" to a domain-specific name like "payload" and replace the
generic RuntimeException with a more specific exception (e.g.,
IllegalStateException) to improve diagnostics; update the method signature for
jsonSerialize(Object payload), keep the objectMapper.writeValueAsString(payload)
call, and change the catch block to throw new IllegalStateException("Failed to
serialize SSE data", e); update any local usages/call sites of jsonSerialize
accordingly to use the new parameter name.
- Around line 43-46: The SSE error event constant SSE_EVENT_ERROR uses the
non-standard value "error" which isn't in the allowed event taxonomy; change the
constant name/value to align with the taxonomy (e.g., rename SSE_EVENT_ERROR to
SSE_EVENT_STATUS and set its value to "status") and update all references in
this class (and any callers) to use SSE_EVENT_STATUS; keep SSE_EVENT_TEXT as-is
and ensure emitted SSE events use the standardized names: "text", "citation",
"code", "enrichment", "suggestion", "status".
- Around line 244-248: The Flux created in GuidedLearningController (variable
dataStream from openAIStreamingService.streamResponse) uses
onBackpressureBuffer() with no bound—change it to onBackpressureBuffer(512,
/*queueSupplier*/ null, BufferOverflowStrategy.DROP_OLDEST) (or DROP_LATEST if
you prefer) to cap queued chunks and avoid unbounded memory growth; apply the
same change to the identical stream creation in ChatController so both
controllers use a 512-capacity bounded buffer with an explicit overflow
strategy.
♻️ Duplicate comments (3)
frontend/src/lib/components/LearnView.svelte (1)

68-108: Guard against stale lesson fetches when switching.
A slower response can overwrite a newer selection; capture the target slug and verify before updating state (and mirror the guard in the citation fetch). Fun tidbit: async races love fast clickers.

🔧 Suggested guard using the selected slug
async function selectLesson(lesson: GuidedLesson): Promise<void> {
+ const targetSlug = lesson.slug
// Reset state atomically before async operation
selectedLesson = lesson
loadingLesson = true
lessonMarkdown = ''
lessonError = null
lessonCitations = []
lessonCitationsError = null
lessonCitationsLoaded = false
messages = []
try {
const response = await fetchLessonContent(lesson.slug)
+ if (selectedLesson?.slug !== targetSlug) return
lessonMarkdown = response.markdown
...
} catch (error) {
+ if (selectedLesson?.slug !== targetSlug) return
lessonError = error instanceof Error ? error.message : 'Failed to load lesson'
lessonMarkdown = ''
} finally {
- loadingLesson = false+ if (selectedLesson?.slug === targetSlug) {+ loadingLesson = false+ }
}
}
frontend/src/lib/components/CitationPanel.svelte (2)

104-110: Block unsafe URL schemes before rendering links.
Citation URLs cross a trust boundary; without scheme validation, javascript: links can slip through. Fun tidbit: validating href is a quick win against stored XSS.

🛡️ Suggested URL scheme guard
+function isSafeCitationUrl(url: string): boolean {+ const lower = url.toLowerCase()+ return lower.startsWith(URL_SCHEME_HTTP)+ || lower.startsWith(URL_SCHEME_HTTPS)+ || lower.startsWith(LOCAL_PATH_PREFIX)+}+
function buildFullUrl(citation: Citation): string {
if (!citation.url) return FALLBACK_LINK_TARGET
+ if (!isSafeCitationUrl(citation.url)) return FALLBACK_LINK_TARGET
if (citation.anchor && !citation.url.includes(ANCHOR_SEPARATOR)) {
return `${citation.url}${ANCHOR_SEPARATOR}${citation.anchor}`
}
return citation.url
}

116-149: Guard citation state against out‑of‑order fetches.
Rapid query changes can let older results overwrite newer ones. Fun tidbit: a simple request counter is a reliable async seatbelt.

🧵 Suggested request guard
let citations = $state<Citation[]>([])
let hasFetched = $state(false)
let fetchError = $state<string | null>(null)
let isExpanded = $state(false)
+let requestId = 0
$effect(() => {
const currentQuery = query
+ const currentRequest = ++requestId
if (!currentQuery || !currentQuery.trim()) {
citations = []
hasFetched = false
fetchError = null
isExpanded = false
return
}
hasFetched = false
fetchError = null
isExpanded = false
fetchCitations(currentQuery)
.then((result) => {
+ if (currentRequest !== requestId) return
// Deduplicate by URL
const seen = new Set<string>()
citations = result.filter((citation) => {
const key = citation.url?.toLowerCase() ?? ''
if (seen.has(key)) return false
seen.add(key)
return true
})
})
.catch((error: unknown) => {
+ if (currentRequest !== requestId) return
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch citations'
fetchError = errorMessage
citations = []
})
.finally(() => {
- hasFetched = true+ if (currentRequest === requestId) {+ hasFetched = true+ }
})
})

Comment threadfrontend/src/lib/components/CitationPanel.svelte Outdated
Comment threadfrontend/src/lib/components/LearnView.svelte
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 24, 2026 09:09 Inactive
Multiple rapid lesson selections could cause stale fetch responses to overwrite
fresh content. Similarly, navigating away during streaming could leave orphaned
chunks updating the wrong lesson context. This adds slug-based guards throughout
the lesson loading and chat streaming workflows.
Lesson selection guards:
- Capture targetSlug before async operations
- Check selectedLesson.slug matches before applying content, citations, errors
- Skip state updates if user switched lessons during fetch
Stream navigation guards:
- Capture streamLessonSlug at stream start
- Guard onChunk handler to ignore chunks after navigation
- Guard message append and finally block with slug comparison
- Reset streaming state in goBack() to cancel in-flight streams
…licts
Multiple CitationPanel instances on the same page shared a hardcoded
"citation-list" ID, breaking ARIA associations and causing DOM conflicts.
Each component instance now generates a unique ID using a random suffix,
ensuring correct aria-controls binding between trigger and list elements.
…ffer
PR review identified several issues: SSE error events used non-standard "error"
type instead of the allowed "status" taxonomy; jsonSerialize used banned
identifier "value" and generic RuntimeException; unbounded onBackpressureBuffer
could cause memory growth with slow clients.
- Rename SSE_EVENT_ERROR to SSE_EVENT_STATUS per allowed taxonomy
- Rename jsonSerialize parameter from "value" to "payload"
- Replace RuntimeException with IllegalStateException for specificity
- Add STREAM_BACKPRESSURE_BUFFER_SIZE constant (512) matching codebase convention
- Apply bounded buffer to streaming Flux
The streaming response Flux used unbounded onBackpressureBuffer() which could
accumulate indefinitely if SSE clients consume slowly. This adds a 512-element
bound matching the convention used elsewhere in the codebase (SitemapController,
RobotsController). The existing onErrorResume handler provides graceful overflow
handling.
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 24, 2026 09:14 Inactive

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@src/main/java/com/williamcallahan/javachat/web/GuidedLearningController.java`:
- Around line 76-105: Extract the inline fallback JSON in sseError into a named
constant (e.g., private static final String SSE_ERROR_FALLBACK_JSON =
"{\"message\":\"Error serialization failed\",\"details\":\"See server logs\"}"),
replace the literal in sseError(...) with that constant, and keep the constant
near the other helper records (ChunkMessage, ErrorMessage) so intent is clear
and reusable; ensure the constant is used only for the serialization fallback
path and update any references to the literal accordingly.
♻️ Duplicate comments (1)
frontend/src/lib/components/CitationPanel.svelte (1)

163-171: Invalidate pending requests when the query clears.
Right now, an in‑flight fetch can still populate citations after the query becomes empty.

🔧 Suggested fix
 if (!currentQuery || !currentQuery.trim()) {
+ pendingRequestId += 1
citations = []
hasFetched = false
fetchError = null
isExpanded = false
return
}

Also applies to: 174-207

Per CS7 (magic literals), the inline JSON string for SSE error fallback
was extracted to SSE_ERROR_FALLBACK_JSON constant for clarity and reuse.
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 24, 2026 09:23 Inactive

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@src/main/java/com/williamcallahan/javachat/web/GuidedLearningController.java`:
- Line 251: Extract the magic literal 0.7 used in the call to
openAIStreamingService.streamResponse(fullPrompt, 0.7) into a clearly named
constant (e.g., LLM_TEMPERATURE or GUIDED_LEARNING_TEMPERATURE) declared near
other LLM configuration constants or at the top of GuidedLearningController;
replace the inline literal with that constant to make intent and tuning easier
and keep all LLM settings grouped for future adjustments.
🧹 Nitpick comments (1)
src/main/java/com/williamcallahan/javachat/web/GuidedLearningController.java (1)

217-217: Consider a more domain-specific variable name.

The variable text is a bit generic—per coding guidelines, identifiers should be intent-revealing. Something like markdownContent or lessonMarkdown would make it instantly clear what this holds. Tiny learning nugget: descriptive names save future-you (and teammates) precious brain cycles! 🧠

🔧 Suggested tweak
- String text = String.join("", chunks);- guidedService.putLessonCache(slug, text);- return text;+ String markdownContent = String.join("", chunks);+ guidedService.putLessonCache(slug, markdownContent);+ return markdownContent;

Exception response formatting was guarded with null checks that don’t apply to
current API contracts. This simplifies the formatting code and uses Optional to
normalize missing headers.
- Assume non-blank status/body strings only after blank checks
- Normalize headers with Optional
- Always append OpenAI response body
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 25, 2026 01:54 Inactive

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/williamcallahan/javachat/service/DocumentFactory.java (1)

38-50: Unintended NPE risk: Map.of() rejects null before createDocumentWithOptionalId can handle it gracefully.

createDocumentWithOptionalId already checks for null/blank hash, but the metadata map built just before tries to insert hash unconditionally via Map.of(), which throws NPE if hash is null. While call sites appear to always generate hash, the method signature accepts nullable String, signaling this pattern should be defensive.

Switch to a mutable map and only add hash when it's present—aligns with the learning that Map<String, Object> is acceptable for Document metadata:

💡 Suggested fix
- Map<String, ?> metadata = Map.of(- "url", url,- "title", title,- "chunkIndex", chunkIndex,- "package", packageName,- "hash", hash- );+ Map<String, Object> metadata = new HashMap<>();+ metadata.put("url", url);+ metadata.put("title", title);+ metadata.put("chunkIndex", chunkIndex);+ metadata.put("package", packageName);+ if (hash != null && !hash.isBlank()) {+ metadata.put("hash", hash);+ }

Also applies to: 117-121

🤖 Fix all issues with AI agents
In `@docs/domains/local-store-directories.md`:
- Around line 3-5: Clarify the wording in the LocalStoreService documentation so
it distinguishes startup failures from runtime errors: update the text that
currently says the app "fails to start and endpoints like `/api/guided/toc`
return 500" to state that invalid/unwritable configured directories cause the
app to fail to start (making endpoints unreachable), and separately note that if
permissions change after startup the service may return 500s for endpoints such
as `/api/guided/toc`; keep reference to LocalStoreService, configured
directories, and the `/api/guided/toc` endpoint for clarity.
In `@spotbugs-exclude.xml`:
- Around line 78-85: The current SpotBugs suppression is too broad; either
narrow it to only the verified classes (replace the wildcard Match with explicit
<Class> entries for RetrySupport, RateLimitManager, GuidedLearningService and
RateLimitState if needed) or introduce a single LogSanitizer/LogHelper utility
that centralizes sanitizeLogValue(), sanitizeForLogText(), sanitizeLogMessage()
and update all logging call sites to use that helper, then remove the broad
com.williamcallahan.javachat.* suppression so SpotBugs can still catch
CRLF_INJECTION_LOGS in new classes.
In
`@src/main/java/com/williamcallahan/javachat/service/markdown/InlineListParser.java`:
- Around line 149-159: The parser currently splits trailing text on the first
punctuation even inside tokens (e.g., "1.8") because extractTrailingText /
findTrailingTextStart splits on any punctuation; update findTrailingTextStart so
it only treats a punctuation as a split point when it is followed by whitespace
(or end-of-string) to avoid breaking decimals/abbreviations, then use that
improved logic where EntryTextSplit is created (the branch around
markers/markerIndex and the other places that call extractTrailingText or rely
on trailingText, e.g., the other parsing blocks that build EntryTextSplit and
set trailingText) so digits and abbreviations remain in the main entryText
instead of being moved into trailingText.
In
`@src/main/java/com/williamcallahan/javachat/service/markdown/OrderedMarkerScanner.java`:
- Around line 113-129: finalizeMarker currently only advances afterIndex by one
when it sees a single space, which makes markers like "1. Item" invalid; update
the logic in finalizeMarker to skip all contiguous whitespace characters after
sequenceEnd (use a loop advancing afterIndex while afterIndex < text.length()
and Character.isWhitespace(text.charAt(afterIndex))). Ensure this change
preserves downstream checks (e.g., calls to isContentStartValid and the
MarkerMatch return) and still handles end-of-string correctly for
InlineListOrderedKind and MarkerMatch creation.
In `@src/main/java/com/williamcallahan/javachat/service/RateLimitManager.java`:
- Around line 352-369: The applyRateLimit method can NPE if resetTime is null
and retryAfterSecondsOverride == 0; update applyRateLimit(ApiProvider provider,
Instant resetTime, long retryAfterSecondsOverride) to compute retryAfterSeconds
as: if retryAfterSecondsOverride > 0 use it, else if resetTime != null compute
Duration.between(Instant.now(), resetTime).getSeconds(), else set
retryAfterSeconds = 0. Leave the
rateLimitState.recordRateLimit(provider.getName(), resetTime, ...) call as-is
(it can receive a null resetTime) and continue to call
state.recordRateLimit(retryAfterSeconds) and the log using the sanitized
providerName.
In `@src/main/java/com/williamcallahan/javachat/web/ChatController.java`:
- Around line 56-60: Replace the scattered `@Value` fields in ChatController
(localEmbeddingServerUrl and localEmbeddingEnabled) by injecting the existing
AppProperties and using its LocalEmbedding config; remove the two `@Value` fields,
add an AppProperties dependency to the ChatController (constructor or field
injection), and replace usages of localEmbeddingEnabled and
localEmbeddingServerUrl with appProperties.getLocalEmbedding().isEnabled() and
appProperties.getLocalEmbedding().getServerUrl() respectively so all
app.local-embedding.* reads come from the centralized LocalEmbedding config
object.
🧹 Nitpick comments (5)
src/test/java/com/williamcallahan/javachat/service/OpenAIStreamingServiceTest.java (1)

19-57: Consider testing observable behavior rather than private internals via reflection.

A fun tidbit: tests that poke at private methods are sometimes called "white-box" tests—they peer inside the box! The trade-off is that they can shatter when you refactor internals, even if external behavior stays rock solid.

Per the coding guidelines ("Assert observable behavior" and "Refactor-resilient"), these tests would be more durable if they verified retry behavior through the public streaming API—e.g., confirming that a 429 triggers a retry attempt or that an IO error results in fallback behavior. That way, renaming or restructuring isRetryablePrimaryFailure won't break your tests.

That said, if isolating this classification logic is intentional for focused unit coverage, this approach is pragmatic—just be aware of the coupling.

src/main/java/com/williamcallahan/javachat/support/RetrySupport.java (1)

120-125: Solid CRLF sanitization for log safety.

This handles the primary log injection attack vector. For extra defense-in-depth (optional), you could consider escaping additional control characters like \t, \b, \f, or even ANSI escape sequences (\u001B) that could affect log readability or formatting in certain terminals. That said, CRLF is the critical vector for log forging, so this is already effective!

♻️ Optional: broader control character sanitization
 private static String sanitizeLogValue(String rawValue) {
if (rawValue == null) {
return "null";
}
- return rawValue.replace("\r", "\\r").replace("\n", "\\n");+ return rawValue+ .replace("\r", "\\r")+ .replace("\n", "\\n")+ .replace("\t", "\\t")+ .replace("\u001B", "\\u001B"); // ANSI escape
}
src/main/java/com/williamcallahan/javachat/service/markdown/MarkdownNormalizer.java (1)

113-172: Replace the inline digit cap with a named constant.
The 3 limit in isNumericHeader reads as a magic literal. A named constant will clarify intent and align with style rules.

♻️ Suggested refactor
 final class MarkdownNormalizer {
private MarkdownNormalizer() {}
private static final int INDENTED_CODE_BLOCK_SPACES = 4;
+ private static final int MAX_NUMERIC_HEADER_DIGITS = 3;
@@
- if (digitIndex == 0 || digitIndex > 3 || digitIndex >= trimmedLine.length()) {+ if (digitIndex == 0 || digitIndex > MAX_NUMERIC_HEADER_DIGITS || digitIndex >= trimmedLine.length()) {
return false;
}

As per coding guidelines, avoid magic literals by introducing named constants.

src/main/java/com/williamcallahan/javachat/service/markdown/InlineListParser.java (1)

38-50: Name the HTML tag literals as constants

The "li", "ol", and "ul" literals are domain policy knobs and repeated; naming them improves clarity and keeps the “no magic literals” rule crisp. Tiny tip: HTML tags are part of your domain vocabulary, so naming them pays off.

♻️ Suggested refactor
 final class InlineListParser {
private static final int MAX_NESTED_DEPTH = 3;
private static final int MIN_MARKER_COUNT = 2;
private static final int BULLET_MARKER_WIDTH = 2;
private static final int COLON_BACKTRACK_OFFSET = 2;
+ private static final String TAG_ORDERED_LIST = "ol";+ private static final String TAG_UNORDERED_LIST = "ul";+ private static final String TAG_LIST_ITEM = "li";
@@
- for (String entryLabel : parse.primaryBlock().entryLabels()) {- listElement.appendChild(new Element("li").text(entryLabel));+ for (String entryLabel : parse.primaryBlock().entryLabels()) {+ listElement.appendChild(new Element(TAG_LIST_ITEM).text(entryLabel));
}
@@
- for (String entryLabel : nestedParse.primaryBlock().entryLabels()) {- nestedListElement.appendChild(new Element("li").text(entryLabel));+ for (String entryLabel : nestedParse.primaryBlock().entryLabels()) {+ nestedListElement.appendChild(new Element(TAG_LIST_ITEM).text(entryLabel));
}
@@
- Block primaryBlock = new Block("ol", entryLabels);+ Block primaryBlock = new Block(TAG_ORDERED_LIST, entryLabels);
@@
- Block primaryBlock = new Block("ul", entryLabels);+ Block primaryBlock = new Block(TAG_UNORDERED_LIST, entryLabels);

As per coding guidelines, avoid magic literals in production code.

Also applies to: 66-69, 164-201

src/main/java/com/williamcallahan/javachat/service/RateLimitManager.java (1)

163-168: Minor inconsistency: mixed static and instance usage of RateLimitHeaderParser.

The constructor instantiates headerParser as an instance field (line 166), but formatDuration is called statically (line 451). Both approaches work, but it's a bit inconsistent. If formatDuration is a static utility method while other parser methods require instance state, this is intentional—just something to be aware of for future readers.

No action needed unless you want to align on one pattern.

Comment threaddocs/domains/local-store-directories.md Outdated
Comment threadspotbugs-exclude.xml
Comment threadsrc/main/java/com/williamcallahan/javachat/web/ChatController.java Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@spotbugs-exclude.xml`:
- Around line 73-83: The SpotBugs suppression includes a blanket <Bug
pattern="SPRING_ENDPOINT"/> Match that overrides the targeted controller
suppressions; remove the redundant global Match (the standalone <Bug
pattern="SPRING_ENDPOINT"/> block) so the regex-scoped Match for controllers
(Class name="~com\\.williamcallahan\\.javachat\\.web\\.(.*Controller)$") and the
specific test controller entry (Class
name="com.williamcallahan.javachat.web.MarkdownApiIntegrationTest$TestMarkdownController")
remain effective, or alternatively merge the test controller into the regex
Match if you intend a single rule.
- Around line 97-110: The spotbugs-exclude.xml contains global <Match> entries
that suppress EI_EXPOSE_REP and EI_EXPOSE_REP2 without class filters; remove the
two blanket <Match> blocks that have <Bug pattern="EI_EXPOSE_REP"/> and <Bug
pattern="EI_EXPOSE_REP2"/> (the ones with no <Class> child) or replace them with
scoped <Class
name="~com\\.williamcallahan\\.javachat\\.(config|domain|web)\\..*"/> entries to
match the package-limited rules already present, ensuring only the intended
packages are excluded rather than suppressing these warnings project-wide.
In `@src/main/java/com/williamcallahan/javachat/service/RetrievalService.java`:
- Around line 241-243: The access to docs.get(0).getMetadata() in
RetrievalService can return null and cause an exception; replace direct usage
with a null-safe check (e.g., wrap getMetadata() with
Optional.ofNullable(...).orElse(Collections.emptyMap()) or an explicit null
check) so that metadataSize is computed from a non-null Map and docText remains
unaffected; update the variables metadata and metadataSize (and any downstream
usage that assumes non-null metadata) to use the safe Map instead of risking a
null pointer from getMetadata().
In
`@src/main/java/com/williamcallahan/javachat/web/ExceptionResponseBuilder.java`:
- Around line 107-110: The code calls exception.getStatusText() without checking
for null, risking an NPE in ExceptionResponseBuilder; change the logic around
the statusText variable so you null-check (or use a safe string check) before
calling isBlank(), e.g., retrieve statusText from exception.getStatusText(),
ensure it's not null and not blank, then append it to details (reference:
ExceptionResponseBuilder, the exception.getStatusText() call, the statusText
local variable, and details.append invocation).
- Line 127: The current ExceptionResponseBuilder unconditionally appends
exception.body() via details.append(", body=").append(exception.body()), which
can produce noisy ", body=null" or empty values; update the code in
ExceptionResponseBuilder to check that exception.body() is non-null and
non-empty (e.g., not blank) before appending the ", body=" segment so it only
adds body information when meaningful.
- Around line 89-92: The call to exception.getStatusText() in
ExceptionResponseBuilder can return null, so calling statusText.isBlank() may
throw an NPE; update the code that builds the details string to null-safe check
the status text (e.g., test for statusText != null && !statusText.isBlank() or
use a utility like StringUtils.hasText/hasLength) before appending, referencing
the local variable statusText and the method
RestClientResponseException.getStatusText() so only non-null, non-blank status
text is appended to details.
♻️ Duplicate comments (2)
spotbugs-exclude.xml (1)

85-91: Global CRLF_INJECTION_LOGS suppression is overly broad.

This blanket suppression hides potential log injection vulnerabilities across the entire codebase. As noted in a prior review, sanitization utilities exist but are scattered—future logging additions may inadvertently skip sanitization without SpotBugs catching it.

Consider narrowing this to the specific classes where sanitization has been verified (e.g., RetrySupport, RateLimitManager, GuidedLearningService), or centralize log sanitization into a single helper so the broad exclusion becomes defensible.

src/main/java/com/williamcallahan/javachat/service/RateLimitManager.java (1)

344-369: Guard against null resetTime before Duration.between.
Duration.between(Instant.now(), resetTime) will NPE if resetTime is ever null. Even if callers guard today, a defensive branch keeps future changes safe and avoids a hard crash.

🛡️ Defensive fix
- long retryAfterSeconds = retryAfterSecondsOverride > 0- ? retryAfterSecondsOverride- : Math.max(0, Duration.between(Instant.now(), resetTime).getSeconds());+ long retryAfterSeconds;+ if (retryAfterSecondsOverride > 0) {+ retryAfterSeconds = retryAfterSecondsOverride;+ } else if (resetTime != null) {+ retryAfterSeconds = Math.max(0, Duration.between(Instant.now(), resetTime).getSeconds());+ } else {+ retryAfterSeconds = 0;+ }
🧹 Nitpick comments (3)
spotbugs-exclude.xml (1)

112-121: Global IMPROPER_UNICODE suppression duplicates the scoped rule.

Lines 115-118 already suppress IMPROPER_UNICODE for all classes under com.williamcallahan.javachat.*. The second global match at lines 119-121 (no class filter) is redundant and would suppress warnings from any package—including third-party code or test helpers where Unicode issues might actually matter.

Keeping just the scoped rule preserves intent while maintaining SpotBugs coverage elsewhere.

🧹 Remove the redundant global match
 <Match>
<Bug pattern="IMPROPER_UNICODE"/>
<Class name="~com\\.williamcallahan\\.javachat\\..*"/>
</Match>
- <Match>- <Bug pattern="IMPROPER_UNICODE"/>- </Match>
src/main/java/com/williamcallahan/javachat/service/RateLimitManager.java (1)

305-315: Rename generic variable info for clarity.
Since this method was touched, consider renaming inforateLimitInfo to comply with the banned generic-name list. As per coding guidelines, please use domain-specific naming.

♻️ Suggested rename
- ParsedRateLimitInfo info = parseRateLimitHeaders(webError);+ ParsedRateLimitInfo rateLimitInfo = parseRateLimitHeaders(webError);- if (info.hasResetTime()) {- applyRateLimit(provider, info.resetTime(), 0);+ if (rateLimitInfo.hasResetTime()) {+ applyRateLimit(provider, rateLimitInfo.resetTime(), 0);
} else {
recordRateLimit(provider, webError.getMessage());
}
src/main/java/com/williamcallahan/javachat/service/OpenAiCompatibleEmbeddingModel.java (1)

70-79: Great testability seam! 🧪

This package-private overload is exactly what makes unit testing pleasant—you can inject a mock OpenAIClient without hitting the network. The Objects.requireNonNull call with an explicit message is a nice touch for debugging.

A small suggestion: a brief single-line comment (e.g., // For testing with mock clients) would help future maintainers immediately understand why this overload exists. Totally optional though!

Comment threadspotbugs-exclude.xml
Comment threadspotbugs-exclude.xml Outdated
When lesson citations loaded after the initial content render, the browser's
scroll anchoring behavior would unexpectedly shift the viewport, causing a
jarring user experience. This was particularly noticeable when users had
scrolled partway through lesson content before citations finished loading.
The fix manually preserves and restores scroll position around citation
updates, and disables browser scroll anchoring on the content panel to
prevent the browser from fighting the manual restoration.
- Add lessonContentPanelEl ref to track scroll container
- Capture scrollTop before citation state updates
- Restore scroll position via requestAnimationFrame after DOM update
- Disable overflow-anchor CSS to prevent browser interference
The header height was hardcoded as a magic number (57px) in App.svelte,
making responsive adjustments impossible and violating DRY when multiple
components need to account for header offset. This extracts the value to
a CSS custom property with responsive breakpoints that match the actual
header sizing at different viewport widths.
- Define --header-height CSS variable in global.css (57px default)
- Add responsive overrides for mobile (60px @ 640px, 52px @ 380px)
- Replace hardcoded padding-top in App.svelte with var(--header-height)
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 25, 2026 02:04 Inactive
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 25, 2026 02:20 Inactive

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/williamcallahan/javachat/service/RateLimitManager.java (1)

309-314: Rename local variable info to a domain‑specific name.
Generic identifiers are banned; rateLimitInfo fits and is already used elsewhere.

🔧 Suggested rename
- ParsedRateLimitInfo info = parseRateLimitHeaders(webError);+ ParsedRateLimitInfo rateLimitInfo = parseRateLimitHeaders(webError);- if (info.hasResetTime()) {- applyRateLimit(provider, info.resetTime(), 0);+ if (rateLimitInfo.hasResetTime()) {+ applyRateLimit(provider, rateLimitInfo.resetTime(), 0);
} else {
recordRateLimit(provider, webError.getMessage());
}
As per coding guidelines, avoid banned generic identifiers.
🤖 Fix all issues with AI agents
In
`@src/main/java/com/williamcallahan/javachat/service/markdown/OrderedMarkerScanner.java`:
- Around line 113-131: The finalizeMarker method currently accepts markers where
the delimiter (markerChar '.' or ')') is immediately followed by non-whitespace
(e.g., "1.Foo"); update finalizeMarker to require at least one whitespace
character after the delimiter before treating it as a list marker: after
computing afterIndex (first non-whitespace after sequenceEnd), if no whitespace
was skipped (i.e., afterIndex == sequenceEnd + 1) return null; keep existing
numeric-version and bounds checks and return new MarkerMatch(startIndex,
afterIndex, kind) only when that mandatory whitespace is present so downstream
checks like isContentStartValid won't misclassify "1.Foo".
In
`@src/main/java/com/williamcallahan/javachat/web/ExceptionResponseBuilder.java`:
- Around line 124-126: In ExceptionResponseBuilder, guard against a null return
from exception.headers() before calling isEmpty(): check that
exception.headers() != null && !exception.headers().isEmpty() (similar to
RateLimitManager.parseRateLimitFromHeaders()), and only then append the headers
to details; update the conditional referencing exception.headers() so it won’t
NPE when headers() returns null.
- Around line 93-95: The current code calls exception.getResponseBodyAsString()
and then responseBody.isBlank(), which can NPE for WebClientResponseException
instances that may return null; update ExceptionResponseBuilder to only call
getResponseBodyAsString() and perform the null/blank check when exception is an
instance of WebClientResponseException (use "exception instanceof
WebClientResponseException"), assign the result to a local String responseBody,
then if responseBody != null && !responseBody.isBlank() append it to details;
reference getResponseBodyAsString(), the exception variable, and the local
responseBody in your change.
🧹 Nitpick comments (3)
spotbugs-exclude.xml (1)

116-125: Avoid the global IMPROPER_UNICODE blanket.
The second <Match> without a <Class> suppresses all IMPROPER_UNICODE findings, which makes the scoped rule redundant. Tiny tidbit: SpotBugs filters are independent, so broad matches mute everything. Consider removing the global match or narrowing it.

🧹 Suggested cleanup
 <Match>
<Bug pattern="IMPROPER_UNICODE"/>
</Match>
- <Match>- <Bug pattern="IMPROPER_UNICODE"/>- </Match>
src/main/java/com/williamcallahan/javachat/service/RateLimitManager.java (1)

345-374: Avoid sentinel 0 for “optional” retry‑after.
Using a nullable Long (or an overload) makes intent explicit and avoids encoding “unknown” in a value.

♻️ One possible refactor (nullable override)
-private void applyRateLimit(ApiProvider provider, Instant resetTime, long retryAfterSecondsOverride) {+private void applyRateLimit(ApiProvider provider, Instant resetTime, Long retryAfterSecondsOverride) {
String providerName = sanitizeLogValue(provider.getName());
ApiEndpointState state = getOrCreateEndpointState(provider);
long retryAfterSeconds;
- if (retryAfterSecondsOverride > 0) {+ if (retryAfterSecondsOverride != null && retryAfterSecondsOverride > 0) {
retryAfterSeconds = retryAfterSecondsOverride;
} else if (resetTime != null) {
retryAfterSeconds = Math.max(0, Duration.between(Instant.now(), resetTime).getSeconds());
} else {
retryAfterSeconds = 0;
}

Also update call sites that pass 0 to pass null instead:

-applyRateLimit(provider, rateLimitInfo.resetTime(), 0);+applyRateLimit(provider, rateLimitInfo.resetTime(), null);
As per coding guidelines, optional parameters shouldn’t be encoded via sentinel values.
src/main/java/com/williamcallahan/javachat/web/ChatController.java (1)

286-306: Extract health-check literals into named constants.

Per the project guidelines, avoid inline strings like "/v1/models" and "UNREACHABLE: "—constants make intent clearer and prevent drift. As per coding guidelines, please use intent-revealing constants.

♻️ Suggested refactor
+ private static final String EMBEDDINGS_MODELS_PATH = "/v1/models";+ private static final String EMBEDDINGS_UNREACHABLE_PREFIX = "UNREACHABLE: ";
- String healthUrl = serverUrl + "/v1/models";+ String healthUrl = serverUrl + EMBEDDINGS_MODELS_PATH;
restTemplate.getForEntity(healthUrl, String.class);
return ResponseEntity.ok(EmbeddingsHealthResponse.healthy(serverUrl));
} catch (RestClientException httpError) {
log.debug("Embedding server health check failed", httpError);
String details = describeException(httpError);
return ResponseEntity.ok(EmbeddingsHealthResponse.unhealthy(
- serverUrl, "UNREACHABLE: " + details));+ serverUrl, EMBEDDINGS_UNREACHABLE_PREFIX + details));
}

Loading and error states in LearnView were not centered within their container.
Using absolute positioning with inset: 0 fills the parent while flexbox centers
the content both horizontally and vertically.
- Add position: absolute and inset: 0 to loading-state and error-state
- Add justify-content: center for vertical centering
- Add position: relative to lesson-content-panel as positioning context
- Add explicit grid-template-rows to lesson-view-content
Fixed positioning required manual padding-top on the app container to prevent
content from being hidden under the header. Sticky positioning keeps the header
in the document flow, eliminating the need for compensating padding and
simplifying the layout. The header still stays visible at the top when scrolling.
- Change header from position: fixed to position: sticky
- Remove left/right declarations (unnecessary for sticky in flex container)
- Remove padding-top: var(--header-height) from app container
@railway-app
railway-appBottemporarily deployed to insightful-intuition / production January 25, 2026 02:40 Inactive

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/lib/components/LearnView.svelte`:
- Around line 99-120: The fetchCitations promise chain currently lacks a
rejection handler so if fetchCitations rejects lessonCitationsLoaded never gets
set and the error surfaces; add a .catch() (or use try/catch if converting to
async/await) on the fetchCitations(lesson.title) call to handle rejections by
setting lessonCitationsError to the caught error and setting
lessonCitationsLoaded = true, keeping the same stale-response guard
(selectedLesson?.slug !== targetSlug) and the scroll-preservation/restore logic
around lessonContentPanelEl so UI state remains consistent; update the promise
chain where fetchCitations is invoked and ensure deduplicateCitations is only
used on successful results.

Comment threadfrontend/src/lib/components/LearnView.svelte
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requestrefactorCode refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@WilliamAGH