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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## [Unreleased]

### Phase 6 — Study Buddy wired into the reader (2026-06-15)

Phase 6 **AI-038, slice b** — the panel is now reachable: select a passage in the reader → "Help me understand this" → the agent investigates live.

- **`SelectionToolbar`** gains a **"Help me understand this"** action (sparkles icon) next to Explain, shown only when `onStudyBuddy` is wired (catalog editions).
- **`ReaderHighlights`** passes the whole selected passage up via a new `onStudyBuddy(passage)` prop and clears the selection.
- **`ReaderPage`** holds the passage state and renders `StudyBuddyPanel` (catalog editions only, like Ask), threading the **current chapter number** so the agent's chapter tools have context. Opening it for a new passage re-runs.
- i18n: `reader.selectionToolbar.studyBuddy`.
- Verified: `tsc` + `pnpm build` clean; full web suite green (517); no e2e clicks the selection toolbar positionally (no index drift). The live agent run is exercised on prod (key + corpus).

### Phase 6 — Study Buddy web panel (2026-06-15)

Phase 6 **AI-038, slice a** — the web UI for the agent: an `api` client, a streaming hook, and the panel. Wiring it into the reader's selection toolbar is slice b.
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/reader/ReaderHighlights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ interface ReaderHighlightsProps {
ttsSpeed?: number
scrollToHighlightId?: string | null
showInlineTranslations?: boolean
/** Open the Study Buddy panel for a highlighted passage (AI-038b). Catalog editions only. */
onStudyBuddy?: (passage: string) => void
children: React.ReactNode
}

Expand Down Expand Up @@ -67,6 +69,7 @@ export function ReaderHighlights({
ttsSpeed = 1.0,
scrollToHighlightId,
showInlineTranslations = false,
onStudyBuddy,
children,
}: ReaderHighlightsProps) {
const { nativeLanguage, setNativeLanguage, hasConfirmedLanguage } = useNativeLanguage()
Expand Down Expand Up @@ -401,6 +404,14 @@ export function ReaderHighlights({
explainPopup.openFromSelection(selection.text, selection.range, selection.rect)
}, [explainPopup, selection.text, selection.range, selection.rect])

// --- Study Buddy: hand the whole selected passage up to the reader's panel ---
const handleStudyBuddy = useCallback(() => {
const passage = selection.text?.trim()
if (!passage || !onStudyBuddy) return
onStudyBuddy(passage)
clearSelection()
}, [selection.text, onStudyBuddy, clearSelection])

// --- Selection toolbar ---
const handleHighlight = useCallback(
async (color: HighlightColor) => {
Expand Down Expand Up @@ -462,6 +473,7 @@ export function ReaderHighlights({
onHighlight={handleHighlight}
onTranslate={handleTranslate}
onExplain={handleExplain}
onStudyBuddy={onStudyBuddy ? handleStudyBuddy : undefined}
onSpeak={() => handleSpeak(selection.text)}
onCopy={handleCopy}
/>
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/components/reader/SelectionToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface SelectionToolbarProps {
onHighlight: (color: HighlightColor) => void
onTranslate?: () => void
onExplain?: () => void
onStudyBuddy?: () => void
onSpeak?: () => void
onCopy?: () => void
}
Expand All @@ -27,6 +28,7 @@ export function SelectionToolbar({
onHighlight,
onTranslate,
onExplain,
onStudyBuddy,
onSpeak,
onCopy,
}: SelectionToolbarProps) {
Expand Down Expand Up @@ -130,6 +132,18 @@ export function SelectionToolbar({
<ExplainIcon />
</button>
)}
{onStudyBuddy && (
<button
className="selection-toolbar__action"
onMouseDown={(e) => e.preventDefault()}
onTouchStart={(e) => e.preventDefault()}
onClick={onStudyBuddy}
title={t('reader.selectionToolbar.studyBuddy')}
aria-label={t('reader.selectionToolbar.studyBuddy')}
>
<StudyBuddyIcon />
</button>
)}
{onSpeak && text.trim().length <= 500 && (
<button
className="selection-toolbar__action"
Expand Down Expand Up @@ -188,6 +202,16 @@ function ExplainIcon() {
)
}

function StudyBuddyIcon() {
// Sparkles — "help me understand" / agent assist.
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 3l1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3z" />
<path d="M19 14l.7 1.7L21.5 16.5l-1.8.8L19 19l-.7-1.7L16.5 16.5l1.8-.8L19 14z" />
</svg>
)
}

function SpeakIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
"highlightBlue": "Highlight blue",
"translate": "Translate selected text",
"explain": "Explain in context",
"studyBuddy": "Help me understand this",
"listen": "Listen to pronunciation",
"copy": "Copy selected text"
},
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/pages/ReaderPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ReaderNav } from '../components/reader/ReaderNav'
import { ReaderFooterNav } from '../components/reader/ReaderFooterNav'
import { ReaderSettingsDrawer } from '../components/reader/ReaderSettingsDrawer'
import { AskPanel } from '../components/reader/AskPanel'
import { StudyBuddyPanel } from '../components/reader/StudyBuddyPanel'
import type { AskCitation } from '../api/ask'
import { scrollToCitation } from '../lib/citationScroll'
import { ReaderTocDrawer } from '../components/reader/ReaderTocDrawer'
Expand Down Expand Up @@ -69,6 +70,7 @@ export function ReaderPage({ mode = 'public' }: ReaderPageProps) {
const [tocOpen, setTocOpen] = useState(false)
const [settingsOpen, setSettingsOpen] = useState(false)
const [askOpen, setAskOpen] = useState(false)
const [studyBuddyPassage, setStudyBuddyPassage] = useState<string | null>(null)
const [searchOpen, setSearchOpen] = useState(false)

// Highlight ID from URL — scroll to this highlight after chapter loads
Expand Down Expand Up @@ -514,6 +516,7 @@ export function ReaderPage({ mode = 'public' }: ReaderPageProps) {
ttsSpeed={settings.ttsSpeed}
showInlineTranslations={settings.showInlineTranslations}
scrollToHighlightId={scrollToHighlightId}
onStudyBuddy={askEditionId ? setStudyBuddyPassage : undefined}
>
<div ref={scrollContainerRef}>
<ReaderSection
Expand Down Expand Up @@ -590,6 +593,18 @@ export function ReaderPage({ mode = 'public' }: ReaderPageProps) {
/>
)}

{askEditionId && studyBuddyPassage && (
<StudyBuddyPanel
open
editionId={askEditionId}
passage={studyBuddyPassage}
chapterNumber={activeChapter?.chapterNumber ?? null}
isAuthenticated={isAuthenticated}
onSignIn={openAuthModal}
onClose={() => setStudyBuddyPassage(null)}
/>
)}

<ReaderSearchDrawer
open={searchOpen}
query={searchQuery}
Expand Down
Loading