fix: ChatBubble crash and DeepSeek API compatibility

- Fix ChatBubble to handle non-string content with String() wrapper
- Fix API route to use generateText for non-streaming requests
- Add @ai-sdk/openai-compatible for non-OpenAI providers (DeepSeek, etc.)
- Use Chat Completions API instead of Responses API for compatible providers
- Update ChatBubble tests and fix component exports to kebab-case
- Remove stale PascalCase ChatBubble.tsx file
This commit is contained in:
Max
2026-01-26 16:55:05 +07:00
parent 6b113e0392
commit e9e6fadb1d
544 changed files with 113077 additions and 427 deletions

View File

@@ -0,0 +1,514 @@
# Story 2.1: Ghostwriter Agent & Markdown Generation
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## Story
As a user,
I want the system to draft a polished post based on my chat,
So that I can see my raw thoughts transformed into value.
## Acceptance Criteria
1. **Ghostwriter Agent Trigger**
- Given the user has completed the interview or used "Fast Track"
- When the "Ghostwriter" agent is triggered
- Then it consumes the entire chat history and the "Lesson" context
- And generates a structured Markdown artifact (Title, Body, Tags)
2. **Drafting Animation**
- Given the generation is processing
- When the user waits
- Then they see a distinct "Drafting" animation (different from "Typing")
- And the tone of the output matches the "Professional/LinkedIn" persona
## Tasks / Subtasks
- [x] Implement Ghostwriter Prompt Engine
- [x] Create `generateGhostwriterPrompt()` function in `src/lib/llm/prompt-engine.ts`
- [x] Build prompt structure: Chat history + Intent context + Persona instructions
- [x] Define output format: Markdown with Title, Body, Tags sections
- [x] Add constraints: Professional tone, no hallucinations, grounded in user input
- [x] Implement Ghostwriter LLM Service
- [x] Create `getGhostwriterResponse()` in `src/services/llm-service.ts`
- [x] Handle streaming response for draft generation
- [x] Add retry logic for failed generations
- [x] Return structured Markdown object
- [x] Create Draft State Management
- [x] Add draft state to ChatStore: `currentDraft`, `isDrafting`
- [x] Add `generateDraft()` action to trigger Ghostwriter
- [x] Add `clearDraft()` action for state reset
- [x] Persist draft to `drafts` table in IndexedDB
- [x] Implement Drafting Indicator
- [x] Create `DraftingIndicator.tsx` component
- [x] Use distinct animation (shimmer/skeleton) different from typing indicator
- [x] Show "Drafting your post..." message with professional tone
- [x] Create Draft Storage Schema
- [x] Add `drafts` table to Dexie schema in `src/lib/db/index.ts`
- [x] Define Draft interface: id, sessionId, title, content, tags, createdAt, status
- [x] Add indexes for querying drafts by session and date
- [x] Integrate Ghostwriter with Chat Service
- [x] Modify `ChatService` to route to Ghostwriter after interview completion
- [x] Implement trigger logic: user taps "Draft It" or Fast Track sends input
- [x] Store generated draft in IndexedDB
- [x] Update ChatStore with draft result
- [x] Test Ghostwriter End-to-End
- [x] Unit test: Prompt generation with various chat histories
- [x] Unit test: Ghostwriter LLM service with mocked responses
- [x] Integration test: Full flow from chat to draft generation
- [x] Integration test: Fast Track triggers Ghostwriter directly
- [x] Edge case: Empty chat history
- [x] Edge case: Very long chat history (token limits)
## Dev Notes
### Architecture Compliance (CRITICAL)
**Logic Sandwich Pattern - DO NOT VIOLATE:**
- **UI Components** MUST NOT import `src/lib/llm` or `src/services/llm-service.ts` directly
- All Ghostwriter logic MUST go through `ChatService` layer
- ChatService then calls `LLMService` as needed
- Components use Zustand store via atomic selectors only
- Services return plain objects, not Dexie observables
**State Management - Atomic Selectors Required:**
```typescript
// BAD - Causes unnecessary re-renders
const { currentDraft, isDrafting } = useChatStore();
// GOOD - Atomic selectors
const currentDraft = useChatStore(s => s.currentDraft);
const isDrafting = useChatStore(s => s.isDrafting);
const generateDraft = useChatStore(s => s.generateDraft);
```
**Local-First Data Boundary:**
- Generated drafts MUST be stored in IndexedDB (`drafts` table)
- Drafts are the primary artifacts - chat history is the source context
- Drafts persist offline and can be accessed from history view
- No draft content sent to server for storage
**Edge Runtime Constraint:**
- All API routes under `app/api/` must use the Edge Runtime
- The Ghostwriter LLM call goes through `/api/llm` route (same as Teacher)
- Code: `export const runtime = 'edge';`
### Architecture Implementation Details
**Story Purpose:**
This is the FIRST story of Epic 2 ("The Magic Mirror"). It implements the core value proposition: transforming raw chat input into a polished artifact. The Ghostwriter Agent is the "magic" that turns venting into content.
**State Management:**
```typescript
// Add to ChatStore (src/lib/store/chat-store.ts)
interface ChatStore {
// Draft state
currentDraft: Draft | null;
isDrafting: boolean;
generateDraft: (sessionId: string) => Promise<void>;
clearDraft: () => void;
}
interface Draft {
id: string;
sessionId: string;
title: string;
content: string; // Markdown formatted
tags: string[];
createdAt: number;
status: 'draft' | 'completed' | 'regenerated';
}
```
**Dexie Schema Extensions:**
```typescript
// Add to src/lib/db/schema.ts
db.version(1).stores({
chatLogs: 'id, sessionId, timestamp, role',
sessions: 'id, createdAt, updatedAt',
drafts: 'id, sessionId, createdAt, status' // NEW table
});
interface DraftRecord {
id: string;
sessionId: string;
title: string;
content: string;
tags: string[];
createdAt: number;
status: 'draft' | 'completed' | 'regenerated';
}
```
**Logic Flow:**
1. User completes interview OR uses Fast Track
2. ChatService detects "ready to draft" state
3. ChatService calls `LLMService.getGhostwriterResponse(chatHistory, intent)`
4. LLMService streams response through `/api/llm` edge function
5. ChatStore updates `isDrafting` state (shows drafting indicator)
6. On completion, draft stored in IndexedDB and ChatStore updated
7. Draft view UI displays the result (Story 2.2)
**Files to Create:**
- `src/components/features/chat/DraftingIndicator.tsx` - Drafting animation component
- `src/lib/db/draft-service.ts` - Draft CRUD operations (follows Service pattern)
**Files to Modify:**
- `src/lib/db/schema.ts` - Add drafts table
- `src/lib/llm/prompt-engine.ts` - Add `generateGhostwriterPrompt()` function
- `src/services/llm-service.ts` - Add `getGhostwriterResponse()` function
- `src/services/chat-service.ts` - Add draft generation orchestration
- `src/lib/store/chat-store.ts` - Add draft state and actions
- `src/app/api/llm/route.ts` - Handle Ghostwriter requests (extend existing)
### UX Design Specifications
**From UX Design Document:**
**Visual Feedback - Drafting State:**
- Use "Skeleton card loader" (shimmering lines) to show work is happening
- Different from "Teacher is typing..." dots
- Text: "Drafting your post..." or "Polishing your insight..."
**Output Format - The "Magic Moment":**
- The draft should appear as a "Card" or "Article" view (Story 2.2 will implement)
- Use `Merriweather` font (serif) to signal "published work"
- Distinct visual shift from Chat (casual) to Draft (professional)
**Tone and Persona:**
- Ghostwriter should use "Professional/LinkedIn" persona
- Output should be polished but authentic
- Avoid corporate jargon; maintain the user's voice
**Transition Pattern:**
- When drafting completes, the Draft View slides up (Sheet pattern)
- Chat remains visible underneath for context
- This "Split-Personality" UI reinforces the transformation value
### Testing Requirements
**Unit Tests:**
- `PromptEngine`: `generateGhostwriterPrompt()` produces correct structure
- `PromptEngine`: Includes chat history context in prompt
- `PromptEngine`: Handles empty/short chat history gracefully
- `LLMService`: `getGhostwriterResponse()` calls Edge API correctly
- `LLMService`: Handles streaming response with callbacks
- `ChatStore`: `generateDraft()` action updates state correctly
- `ChatStore`: Draft persisted to IndexedDB
- `DraftService`: CRUD operations work correctly
**Integration Tests:**
- Full flow: Chat history -> Draft generation -> Draft stored
- Fast Track flow: Single input -> Draft generation
- Draft state: Draft appears in UI after generation
- Offline scenario: Draft queued if offline (basic handling for now, full sync in Epic 3)
**Edge Cases:**
- Empty chat history: Should return helpful error message
- Very long chat history: Should truncate/summarize within token limits
- LLM API failure: Should show retry option
- Malformed LLM response: Should handle gracefully
**Performance Tests:**
- Draft generation time: < 5 seconds (NFR requirement)
- Drafting indicator appears within 1 second of trigger
- Large chat history (100+ messages): Should handle efficiently
### Previous Story Intelligence (from Epic 1)
**Patterns Established (must follow):**
- **Logic Sandwich Pattern:** UI -> Zustand -> Service -> LLM (strictly enforced)
- **Atomic Selectors:** All state access uses `useChatStore(s => s.field)`
- **Streaming Pattern:** LLM responses use streaming with callbacks (onToken, onComplete)
- **Edge Runtime:** All API routes use `export const runtime = 'edge'`
- **Typing Indicator:** Pattern for showing processing state
- **Intent Detection:** Teacher agent classifies user input (context for Ghostwriter)
**Key Files from Epic 1 (Reference):**
- `src/lib/llm/prompt-engine.ts` - Has `generateTeacherPrompt()`, add Ghostwriter version
- `src/services/llm-service.ts` - Has `getTeacherResponseStream()`, add Ghostwriter version
- `src/app/api/llm/route.ts` - Handles Teacher requests, extend for Ghostwriter
- `src/lib/store/chat-store.ts` - Has chat state, add draft state
- `src/services/chat-service.ts` - Orchestrates chat flow, add draft generation
- `src/lib/db/schema.ts` - Has chatLogs and sessions, add drafts table
**Learnings to Apply:**
- Story 1.4 established Fast Track mode that directly triggers Ghostwriter
- Use `isProcessing` pattern for `isDrafting` state
- Follow streaming callback pattern: `onToken` for building draft incrementally
- Ghostwriter prompt should include intent context from Teacher agent
- Draft generation should use same Edge API proxy as Teacher agent
**Testing Patterns:**
- Epic 1 established 101 passing tests
- Follow same test structure: unit tests for each service, integration tests for full flow
- Use mocked LLM responses for deterministic testing
- Test streaming behavior with callback mocks
### Ghostwriter Prompt Specifications
**Prompt Structure:**
```typescript
function generateGhostwriterPrompt(
chatHistory: ChatMessage[],
intent?: 'venting' | 'insight'
): string {
return `
You are the Ghostwriter Agent. Your role is to transform a user's chat session into a polished, professional post.
CONTEXT:
- User Intent: ${intent || 'unknown'}
- Chat History: ${formatChatHistory(chatHistory)}
REQUIREMENTS:
1. Extract the core insight or lesson from the chat
2. Write in a professional but authentic tone (LinkedIn-style)
3. Structure as Markdown with: Title, Body, Tags
4. DO NOT hallucinate facts - stay grounded in what the user shared
5. Focus on the "transformation" - how the user's thinking evolved
6. If it was a struggle, frame it as a learning opportunity
7. Keep it concise (300-500 words for the body)
OUTPUT FORMAT:
\`\`\`markdown
# [Compelling Title]
[2-4 paragraphs that tell the story of the insight]
**Tags:** [3-5 relevant tags]
\`\`\`
`;
}
```
**Prompt Engineering Notes:**
- The prompt should emphasize "grounded in user input" to prevent hallucinations
- For "venting" intent, focus on "reframing struggle as lesson"
- For "insight" intent, focus on "articulating the breakthrough"
- Include the full chat history as context
- Title generation is critical - should be catchy but authentic
### Data Schema Specifications
**Dexie Schema - Drafts Table:**
```typescript
// Add to src/lib/db/schema.ts
db.version(1).stores({
chatLogs: 'id, sessionId, timestamp, role, intent',
sessions: 'id, createdAt, updatedAt, isFastTrackMode, currentIntent',
drafts: 'id, sessionId, createdAt, status' // NEW
});
export interface DraftRecord {
id: string;
sessionId: string;
title: string;
content: string; // Markdown formatted
tags: string[]; // Array of tag strings
createdAt: number;
status: 'draft' | 'completed' | 'regenerated';
}
```
**Session-Draft Relationship:**
- Each draft is linked to a session via `sessionId`
- A session can have multiple drafts (regenerations)
- The latest draft for a session is shown in history
- Status tracks draft lifecycle: draft -> completed (user approved) or regenerated
### Performance Requirements
**NFR-01 Compliance (Generation Latency):**
- Draft generation should complete in < 5 seconds total
- First token should appear within 3 seconds
- Streaming should show progressive build-up of the draft
**NFR-06 Compliance (Data Persistence):**
- Draft must be auto-saved to IndexedDB immediately on completion
- No data loss if user closes app during generation
- Drafts persist offline and can be accessed from history
**State Updates:**
- `isDrafting` state should update immediately on trigger
- Draft content should stream into UI as tokens arrive
- Draft should be queryable from history view immediately after completion
### Security & Privacy Requirements
**NFR-03 & NFR-04 Compliance:**
- User content sent to LLM API for inference only (not training)
- No draft content stored on server
- Drafts stored 100% client-side in IndexedDB
- API keys hidden via Edge Function proxy
**Content Safety:**
- Ghostwriter prompt should include guardrails against:
- Toxic or offensive content
- Factually incorrect technical claims
- Overly promotional language
- If LLM generates concerning content, flag for user review
### Project Structure Notes
**Following Feature-First Lite Pattern:**
- New component: `src/components/features/chat/DraftingIndicator.tsx`
- New service: `src/lib/db/draft-service.ts` (could also be in `src/services/`)
- Store updates: `src/lib/store/chat-store.ts`
- Schema updates: `src/lib/db/schema.ts`
**Alignment with Unified Project Structure:**
- All feature code under `src/components/features/`
- Services orchestrate logic, don't touch DB directly from UI
- State managed centrally in Zustand stores
- Database schema versioned properly with Dexie
**No Conflicts Detected:**
- Ghostwriter fits cleanly into existing architecture
- Drafts table is new, no migration conflicts
- Extends existing LLM service pattern
### References
**Epic Reference:**
- [Epic 2: "The Magic Mirror" - Ghostwriter & Draft Refinement](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#epic-2-the-magic-mirror---ghostwriter--draft-refinement)
- [Story 2.1: Ghostwriter Agent & Markdown Generation](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#story-21-ghostwriter-agent--markdown-generation)
- FR-03: "Ghostwriter Agent can transform the structured interview data into a grammatically correct and structured 'Enlightenment' artifact"
**Architecture Documents:**
- [Project Context: Logic Sandwich](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#1-the-logic-sandwich-pattern-service-layer)
- [Project Context: State Management](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#2-state-management-zustand)
- [Project Context: Local-First Boundary](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#3-local-first-data-boundary)
- [Architecture: Service Boundaries](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/architecture.md#architectural-boundaries)
- [Architecture: Data Architecture](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/architecture.md#data-architecture)
**UX Design Specifications:**
- [UX: Experience Mechanics - The Magic](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#2-experience-mechanics)
- [UX: Typography System](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#typography-system)
- [UX: Component Strategy - DraftCard](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#component-strategy)
**PRD Requirements:**
- [PRD: Dual-Agent Pipeline](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/prd.md#dual-agent-pipeline-core-innovation)
- FR-03: "Ghostwriter Agent can transform the structured interview data into a grammatically correct and structured 'Enlightenment' artifact"
- NFR-01: "< 3 seconds for first token, < 5 seconds total generation"
**Previous Stories:**
- [Story 1.4: Fast Track Mode](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/1-4-fast-track-mode.md) - Fast Track directly triggers Ghostwriter
## Dev Agent Record
### Agent Model Used
Claude Opus 4.5 (model ID: 'claude-opus-4-5-20251101')
### Debug Log References
Session file: `/tmp/claude/-home-maximilienmao-Projects-Test01/e83dd24d-bb58-4fba-ac25-3628cdeae3e8/scratchpad`
### Completion Notes List
**Story Analysis Completed:**
- Extracted story requirements from Epic 2, Story 2.1
- Analyzed previous Epic 1 stories for established patterns
- Reviewed architecture for compliance requirements (Logic Sandwich, State Management, Local-First)
- Reviewed UX specification for visual design and interaction patterns
- Identified all files to create and modify
**Implementation Completed:**
All tasks and subtasks have been implemented:
1. **Ghostwriter Prompt Engine** (`src/lib/llm/prompt-engine.ts`)
- Added `generateGhostwriterPrompt()` function with chat history context, intent-specific guidance
- Prompt enforces: professional LinkedIn tone, no hallucinations, grounded in user input
- Output format: Markdown with Title, Body, Tags sections
- 21 new tests added for prompt generation (all passing)
2. **Ghostwriter LLM Service** (`src/services/llm-service.ts`)
- Added `getGhostwriterResponse()` for non-streaming draft generation
- Added `getGhostwriterResponseStream()` for streaming draft generation
- Includes retry logic and comprehensive error handling
- Returns structured Draft object with Markdown content
- 13 new tests added (all passing)
3. **Draft State Management** (`src/lib/store/chat-store.ts`)
- Added `currentDraft`, `isDrafting` state to ChatStore
- Added `generateDraft()` action to trigger Ghostwriter
- Added `clearDraft()` action for state reset
- Drafts automatically persisted to IndexedDB via DraftService
- Follows atomic selector pattern for state access
4. **Drafting Indicator** (`src/components/features/chat/DraftingIndicator.tsx`)
- Created component with shimmer/pulse animation
- Distinct from typing indicator (dots vs skeleton)
- Shows "Drafting your post..." message
5. **Draft Storage Schema** (`src/lib/db/index.ts`)
- Added `DraftRecord` interface with all required fields
- Added `drafts` table to Dexie database with indexes
- Database version 1 with proper schema definition
6. **ChatService Integration** (`src/services/chat-service.ts`)
- Added `generateGhostwriterDraft()` method
- Orchestrates Ghostwriter LLM service and DraftService
- Handles title/content parsing from Markdown
- Returns structured response with draft ID
7. **Fast Track Integration** (`src/lib/store/chat-store.ts`)
- Fast Track mode now triggers Ghostwriter Agent
- Generates actual draft instead of placeholder response
- Draft saved to IndexedDB and loaded into currentDraft state
**Key Technical Decisions:**
1. **Prompt Engineering:** Ghostwriter prompt structure with chat history context, output format requirements, hallucination guardrails
2. **State Management:** Add draft state to ChatStore following atomic selector pattern
3. **Data Schema:** New `drafts` table in IndexedDB with proper indexing
4. **Service Pattern:** DraftService for CRUD operations (follows established pattern)
5. **Streaming:** Use same streaming pattern as Teacher agent for draft generation
6. **Fast Track:** Now calls Ghostwriter instead of returning placeholder (Epic 2 integration)
**Dependencies:**
- No new dependencies required
- Reuses existing Zustand, Dexie, LLM service infrastructure
- Extends existing prompt engine and LLM service
**Integration Points:**
- Connected to existing ChatStore state management
- Ghostwriter triggered by ChatService after interview completion or Fast Track
- Reuses Edge API proxy (`/api/llm`) for LLM calls
- Draft stored in IndexedDB for history access (Epic 3)
**Testing Summary:**
- 146 tests passing (all tests passing)
- All Ghostwriter functionality tested with unit and integration tests
- Error handling tested for timeout, rate limit, network errors
- Edge cases tested: empty history, long history, undefined intent
- Fast Track integration test updated to mock ChatService.generateGhostwriterDraft
**Behavior Changes:**
- Fast Track mode (Story 1.4) now triggers Ghostwriter Agent
- Returns actual draft instead of placeholder response
- Integration test updated to mock ChatService.generateGhostwriterDraft and DraftService.getDraftBySessionId
### File List
**New Files Created:**
- `src/lib/db/draft-service.ts` - Draft CRUD operations service
- `src/lib/db/draft-service.test.ts` - Draft service tests (11 tests)
- `src/components/features/chat/DraftingIndicator.tsx` - Drafting animation component
**Files Modified:**
- `src/lib/db/index.ts` - Added DraftRecord interface and drafts table to Dexie schema
- `src/lib/llm/prompt-engine.ts` - Added `generateGhostwriterPrompt()` function with intent-specific guidance
- `src/services/llm-service.ts` - Added Ghostwriter methods with streaming and error handling
- `src/services/chat-service.ts` - Added `generateGhostwriterDraft()` orchestration method
- `src/lib/store/chat-store.ts` - Added draft state, generateDraft/clearDraft actions, Fast Track integration
- `src/lib/llm/prompt-engine.test.ts` - Added 21 Ghostwriter prompt tests
- `src/services/llm-service.test.ts` - Added 13 Ghostwriter service tests
- `src/integration/fast-track.test.ts` - Updated Fast Track test to mock Ghostwriter services