Part of epic #1725. Depends on #1744. Phase 4 — Amicus drafting + Subscription rewrite.
Objective
The premium-with-flag-on path. When GUIDED_STUDY_AMICUS_SYNTHESIS=true and the user has Amicus access, synthesis becomes a streaming Amicus draft with mode-specific output, citations to panels actually opened, and persisted artifact.
Files
Create
app/src/services/guidedStudy/synthesis/premiumAmicus.ts
app/src/components/guidedStudy/SynthesisPremiumDraft.tsx
app/src/services/guidedStudy/synthesis/__tests__/premiumAmicus.test.ts
Modify
app/src/services/guidedStudy/synthesis/strategy.ts — chooseStrategy now returns this when conditions met
app/src/screens/StudySessionScreen.tsx — render SynthesisPremiumDraft when strategy.kind === 'premium_amicus'
premiumAmicus.run() behavior
async run(ctx, callbacks) {
// 1. Build mode-specific system prompt
const systemPrompt = buildGuidedStudySystemPrompt({
mode: ctx.plan.mode,
chapterRef: formatChapterRef(`${ctx.bookId}_${ctx.chapterNum}`),
captured: ctx.captured,
panelsOpened: ctx.captured.explore?.panels_opened ?? [],
modeSpecificContext: extractModeContext(ctx.plan.mode, ctx.captured),
});
// 2. Stream via existing streamChat with mode-specific payload
const fullText = await new Promise<string>((resolve, reject) => {
let acc = '';
streamChat({
threadId: synthesisThreadId(ctx.sessionId),
userQuery: 'Draft my synthesis.',
// System prompt is passed via the existing payload (verify the
// streamChat signature supports this; if not, extend it minimally).
conversationHistory: [{ role: 'user', content: systemPrompt }],
// ...standard params from existing askAmicus implementation
onDelta: (token) => { acc += token; callbacks?.onAmicusDelta?.(token); },
onComplete: (final) => { callbacks?.onAmicusComplete?.(); resolve(final.text); },
onError: (err) => { callbacks?.onError?.(err); reject(err); },
});
});
// 3. Parse mode-specific structure out of the response
const artifact = parseModeArtifact(ctx.plan.mode, fullText, ctx.captured);
// 4. Persist
if (ctx.sessionId != null) {
await setModeArtifact(ctx.sessionId, artifact);
await setSynthesisStrategy(ctx.sessionId, 'premium_amicus');
await enqueueReviewItem(ctx.sessionId, artifact);
}
return { kind: 'premium_amicus', output: buildAmicusOutputBlocks(fullText, artifact), artifact };
}
Parsing mode artifact from Amicus text
Each mode's prompt asks for a specific structure. The parser is forgiving — it looks for the labels (CLAIM, EVIDENCE, MAIN POINT, etc.) and extracts; missing sections become empty strings.
function parseModeArtifact(
mode: GuidedStudyMode,
text: string,
captured: CapturedInputs,
): ReviewArtifact;
For each mode, define the expected labels and a tolerant regex. Unit-test with realistic Amicus outputs (use fixtures from a recorded prompt run).
SynthesisPremiumDraft component
Renders the streaming text with:
- Skeleton placeholder while waiting
- Streaming dots while tokens arrive (use existing
StreamingDot from components/amicus/)
- Final formatted output with citation pills (use existing
CitationPill component)
- "Saved to your study" confirmation banner once parsed
- "Open in Amicus to ask follow-ups" CTA → leverages existing
launchAmicusStudyThread infrastructure
Cap handling
Before streaming, check useAmicusAccess. If canUse === false:
reason === 'monthly_cap_reached' → fall back to premiumStructuredStrategy for this session, surface a small banner: "You've hit this month's Amicus limit — your synthesis was saved without the AI draft."
reason === 'offline' → same fallback with offline-tinted copy
reason === 'disabled_in_settings' → fall back; banner: "Amicus is turned off in settings."
Acceptance
- With flag ON and access OK: premium user gets streaming Amicus output, parsed artifact, saved review item.
- With flag ON but cap reached: silently falls back to premiumStructured with a banner explanation.
- With flag OFF: chooseStrategy never returns this strategy; never called.
- Citations link to the panels the user actually opened.
- Cancellation works (user navigates away mid-stream → no orphan persistence).
tsc, lint, test clean.
Out of scope
- Server-side Amicus changes (uses existing
streamChat).
- Cross-session Amicus history-aware nudges (future epic).
Part of epic #1725. Depends on #1744. Phase 4 — Amicus drafting + Subscription rewrite.
Objective
The premium-with-flag-on path. When
GUIDED_STUDY_AMICUS_SYNTHESIS=trueand the user has Amicus access, synthesis becomes a streaming Amicus draft with mode-specific output, citations to panels actually opened, and persisted artifact.Files
Create
app/src/services/guidedStudy/synthesis/premiumAmicus.tsapp/src/components/guidedStudy/SynthesisPremiumDraft.tsxapp/src/services/guidedStudy/synthesis/__tests__/premiumAmicus.test.tsModify
app/src/services/guidedStudy/synthesis/strategy.ts—chooseStrategynow returns this when conditions metapp/src/screens/StudySessionScreen.tsx— renderSynthesisPremiumDraftwhen strategy.kind === 'premium_amicus'premiumAmicus.run() behavior
Parsing mode artifact from Amicus text
Each mode's prompt asks for a specific structure. The parser is forgiving — it looks for the labels (CLAIM, EVIDENCE, MAIN POINT, etc.) and extracts; missing sections become empty strings.
For each mode, define the expected labels and a tolerant regex. Unit-test with realistic Amicus outputs (use fixtures from a recorded prompt run).
SynthesisPremiumDraft component
Renders the streaming text with:
StreamingDotfromcomponents/amicus/)CitationPillcomponent)launchAmicusStudyThreadinfrastructureCap handling
Before streaming, check
useAmicusAccess. IfcanUse === false:reason === 'monthly_cap_reached'→ fall back topremiumStructuredStrategyfor this session, surface a small banner: "You've hit this month's Amicus limit — your synthesis was saved without the AI draft."reason === 'offline'→ same fallback with offline-tinted copyreason === 'disabled_in_settings'→ fall back; banner: "Amicus is turned off in settings."Acceptance
tsc,lint,testclean.Out of scope
streamChat).