You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent epic:#1446 (Amicus — AI Study Partner v1) Phase: 2 · Size: L · Depends on:#1454 (shell), #1457 (persistence), #1450 (proxy)
The conversation view. Streaming AI responses with inline citation pills, message bubbles, follow-up suggestions, and input handling. The heart of the Amicus tab.
Files to create
app/src/components/amicus/MessageList.tsx — inverted FlatList of messages
app/src/components/amicus/UserMessageBubble.tsx — user message row
app/src/components/amicus/AssistantMessageBubble.tsx — streaming-capable AI message row
app/src/components/amicus/CitationPill.tsx — inline pill for [chunk_id] references
app/src/components/amicus/FollowUpChips.tsx — 3 tappable follow-up suggestions under last AI message
app/src/components/amicus/InputBar.tsx — sticky bottom input with send button
app/src/components/amicus/StreamingDot.tsx — visual indicator during streaming
Network errors, aborts, and rate-limit 429s surface as typed AmicusError via onError.
Assistant message rendering
The assistant bubble interleaves prose and citation pills. Input from the stream:
Reformed scholars like Calvin read Romans 9 as describing God's sovereign freedom [CITE:section_panel:romans-9-s1-calvin]. Jewish scholars like Sarna emphasize the covenantal dimension [CITE:section_panel:exodus-33-s2-sarna].
Parser produces alternating text + pill nodes. Pills are inline React components (not markdown) — tappable, visually distinct (gold-bordered pill, small scholar avatar if available).
CitationPill
interfaceCitationPillProps{chunkId: string;sourceType: string;// "section_panel" | "chapter_panel" | etc.displayLabel: string;// e.g. "Calvin · Romans 9:15"scholarId?: string;// drives avataronTap: ()=>void;// wired to navigation in #1456}
Display: compact gold pill (base.gold + '20' bg, base.gold border, text base.gold, 11px). Scholar avatar circle if scholarId present. Tap handled in #1456.
Streaming visual
While streaming, last assistant bubble shows a subtle pulsing StreamingDot after the last token
Auto-scroll to bottom while user has not manually scrolled up (standard "sticky bottom" chat pattern)
If user scrolls up, show a "New message ↓" pill at the bottom to jump back
Follow-up chips
After onComplete, render 3 follow-up chips under the last assistant message. Generation: the assistant's structured envelope (separate from gap_signal) includes follow_ups: string[] — the server-side system prompt asks for them. If the envelope is missing/malformed, render no chips.
Tap → send chip text as next user message (same flow as typing it).
Input bar
Multi-line text input with max 4 visible lines, scrolls internally after that
Send button (gold circle with arrow icon) — enabled when text length > 0 and not currently streaming
During streaming: send button replaced with "Stop" (square) — tapping aborts the stream via AbortController
Safe-area aware; keyboard avoidance via KeyboardAvoidingView
Character counter only if > 500 chars (subtle warning if approaching 2000-char limit)
Parent epic: #1446 (Amicus — AI Study Partner v1)
Phase: 2 · Size: L · Depends on: #1454 (shell), #1457 (persistence), #1450 (proxy)
The conversation view. Streaming AI responses with inline citation pills, message bubbles, follow-up suggestions, and input handling. The heart of the Amicus tab.
Files to create
app/src/components/amicus/MessageList.tsx— inverted FlatList of messagesapp/src/components/amicus/UserMessageBubble.tsx— user message rowapp/src/components/amicus/AssistantMessageBubble.tsx— streaming-capable AI message rowapp/src/components/amicus/CitationPill.tsx— inline pill for[chunk_id]referencesapp/src/components/amicus/FollowUpChips.tsx— 3 tappable follow-up suggestions under last AI messageapp/src/components/amicus/InputBar.tsx— sticky bottom input with send buttonapp/src/components/amicus/StreamingDot.tsx— visual indicator during streamingapp/src/services/amicus/chat.ts— orchestrator that composes retrieval (ai-partner: client-side retrieval with sqlite-vec #1451) + streaming fetch to proxy (ai-partner: Cloudflare Worker AI proxy stand-up #1450)app/src/hooks/useAmicusThread.ts— hook managing thread state, streaming, optimistic user messagesapp/src/components/amicus/__tests__/*.test.tsx— component testsFiles to modify
app/src/screens/AmicusThreadScreen.tsx— wire in MessageList + InputBar from the shell built in ai-partner: thread management UI #1454The streaming flow (
chat.tsorchestrator)Sequence per user message:
/ai/chatproxy (from ai-partner: Cloudflare Worker AI proxy stand-up #1450) with:onDelta[chunk_id]markers →onCitationwith resolved{ chunk_id, source_type, display_label }onGapSignal[DONE]: assemble final message, persist via ai-partner: conversation persistence (user.db) #1457, callonCompleteNetwork errors, aborts, and rate-limit 429s surface as typed
AmicusErrorviaonError.Assistant message rendering
The assistant bubble interleaves prose and citation pills. Input from the stream:
Parser produces alternating text + pill nodes. Pills are inline React components (not markdown) — tappable, visually distinct (gold-bordered pill, small scholar avatar if available).
CitationPill
Display: compact gold pill (
base.gold + '20'bg,base.goldborder, textbase.gold, 11px). Scholar avatar circle ifscholarIdpresent. Tap handled in #1456.Streaming visual
StreamingDotafter the last tokenFollow-up chips
After
onComplete, render 3 follow-up chips under the last assistant message. Generation: the assistant's structured envelope (separate from gap_signal) includesfollow_ups: string[]— the server-side system prompt asks for them. If the envelope is missing/malformed, render no chips.Tap → send chip text as next user message (same flow as typing it).
Input bar
AbortControllerKeyboardAvoidingViewHook:
useAmicusThread(threadId)Returns:
Loads message history from #1457 on mount, subscribes to real-time streaming updates, persists on complete.
Error states
OFFLINERATE_LIMITEMBED_FAILEDPROXY_5XXABORTEDErrors render as dismissible banners above the input bar, not as message bubbles.
Conventions to follow
useTheme()for colorsanykeyExtractor={m => m.message_id}Performance targets
Acceptance criteria
onTapcallback (navigation wired in ai-partner: citation → source navigation #1456)anytypes; lint cleanOut of scope