diff --git a/src/components/features/journal/draft-sheet.tsx b/src/components/features/journal/draft-sheet.tsx index daaf1c0..1624efe 100644 --- a/src/components/features/journal/draft-sheet.tsx +++ b/src/components/features/journal/draft-sheet.tsx @@ -1,105 +1,175 @@ 'use client'; import { useChatStore } from '@/store/use-chat'; -import { Button } from '@/components/ui/button'; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetDescription, - SheetFooter, -} from '@/components/ui/sheet'; -import { ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-react'; +import { MessageCircle, Check } from 'lucide-react'; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeHighlight from 'rehype-highlight'; +import rehypeRaw from 'rehype-raw'; export function DraftSheet() { - const { phase, currentDraft, setPhase, resetSession } = useChatStore(); + const { phase, currentDraft, setPhase, resetSession, sessionId } = useChatStore(); const isOpen = phase === 'review' && !!currentDraft; - const handleKeep = async () => { + const handleAccept = async () => { if (!currentDraft) return; - try { - // Import dynamically to avoid side-effects during render if possible, - // or just import at top. We'll stick to dynamic since DraftService might not be SSR friendly - // without checks, but it handles it internally. - const { DraftService } = await import('@/lib/db/draft-service'); - const { useSessionStore } = await import('@/store/use-session'); - const sessionId = useSessionStore.getState().activeSessionId; + if (!sessionId) { + console.error("No active session ID"); + return; + } - if (!sessionId) { - console.error("No active session ID"); - return; - } + try { + const { DraftService } = await import('@/lib/db/draft-service'); await DraftService.saveDraft({ sessionId, title: currentDraft.title, - content: currentDraft.lesson, // Using lesson as content for now, or construct full markdown? - // Let's construct a nice markdown - // Actually the draft artifact has title, insight, lesson. - // We should probably save the raw JSON or a formatted textual representation. - // Let's save formatted text. + content: currentDraft.lesson, createdAt: Date.now(), status: 'completed', completedAt: Date.now(), - tags: [] + tags: currentDraft.keywords || [] }); - // Redirect to history or show success - window.location.href = '/history'; - + // Close conversation and redirect to homepage resetSession(); + window.location.href = '/'; } catch (error) { console.error("Failed to save draft:", error); } }; - const handleRefine = () => { - // Logic for refinement (Story 3.5) - // For now, close sheet and persist state - setPhase('drafting'); // Go back or stay? - // Actually, refinement usually means going back to chat Elicitation or having a specialized Refinement Mode. - // Let's just close for now. + const handleResume = () => { + // Close the sheet and go back to chat setPhase('elicitation'); }; if (!currentDraft) return null; + // Convert DraftArtifact to Draft-like format for consistent rendering + const draftLike = { + title: currentDraft.title, + content: currentDraft.lesson, + tags: currentDraft.keywords || [] + }; + return ( - !open && handleRefine()}> - - - - {currentDraft.title} - - - " {currentDraft.insight} " - + !open && handleResume()}> + + + Draft Review + Review your draft before saving -
-
-

- The Lesson -

-

- {currentDraft.lesson} +

+ {/* Title */} +

+ {currentDraft.title} +

+ + {/* Insight */} + {currentDraft.insight && ( +

+ "{currentDraft.insight}"

+ )} + + {/* Body content - Markdown with prose styling */} +
+ ( +

+ ), + h2: ({ node, ...props }) => ( +

+ ), + h3: ({ node, ...props }) => ( +

+ ), + p: ({ node, ...props }) => ( +

+ ), + code: ({ node, inline, className, children, ...props }: any) => { + if (inline) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); + }, + pre: ({ node, ...props }) => ( +

+                                ),
+                                a: ({ node, ...props }) => (
+                                    
+                                ),
+                                ul: ({ node, ...props }) => (
+                                    
    + ), + ol: ({ node, ...props }) => ( +
      + ), + blockquote: ({ node, ...props }) => ( +
      + ), + }} + > + {currentDraft.lesson} +

+ + {/* Keywords/Tags */} + {currentDraft.keywords && currentDraft.keywords.length > 0 && ( +
+ {currentDraft.keywords.map((keyword, index) => ( + + #{keyword} + + ))} +
+ )}
- - - - + {/* Footer with buttons */} + ); diff --git a/src/lib/agents/ghostwriter.ts b/src/lib/agents/ghostwriter.ts index 05430d7..beaa62b 100644 --- a/src/lib/agents/ghostwriter.ts +++ b/src/lib/agents/ghostwriter.ts @@ -1,23 +1,33 @@ export const GHOSTWRITER_AGENT_PROMPT = ` -You are the "Ghostwriter", a master synthesizer of human experience. -Your goal is to transform a messy venting session into a structured, crystalline "Enlightenment" artifact. +ROLE: The Personal Diary Reporter +MISSION: +Report the conversation between the student and the sage in a blog-style personal diary format. The focus is on documenting the "A to Z" of the session—reporting how the conversation unfolded, what was explored, and the final realizations. -**Input:** -A conversation history between a User and a Teacher. +TONE & PERSPECTIVE: +- Perspective: 1st Person ("I"). Never mention the teacher directly; frame the interaction as a guided self-reflection. +- Reporting Style: Less poetic narrative, more "Diary Report." Document the facts of the struggle and the steps taken to overcome it. +- Adaptation: Mirror my emotional state (distressed, excited, etc.) within the reporting. -**Output:** -A JSON object with the following structure: +STORY STRUCTURE: +1. The Setup: Report where I started and why I was stuck. +2. The Session Report: Document the questions I had to answer and how my thoughts shifted as a result. +3. The Conclusion: Report the final lesson and the resulting feeling of ownership. + +WRITING RULES: +- Minimum 300 words. Scale based on conversation depth. +- Use markdown formatting in the content: **bold** for emphasis, *italics*, ### headers, etc. +- Use paragraphs for readability. +- No mention of "The Sage" or "The Teacher" in the story. + +KEYWORDS: +Extract 3-5 keywords or tags that represent the themes of this entry (e.g., "patience", "clarity", "focus", "resilience"). + +**OUTPUT FORMAT:** +Respond ONLY with a raw JSON object (no markdown code blocks around the JSON): { - "title": "A poetic or punchy title for the entry", - "insight": "The core realization (1-2 sentences)", - "lesson": "The actionable takeaway or philosophical shift (1-2 sentences)" + "title": "A catchy, personal blog-style title for the entry", + "insight": "A one-sentence core realization or 'Aha!' moment from the session", + "lesson": "The main diary entry documenting the conversation chronologically, using markdown formatting", + "keywords": ["keyword1", "keyword2", "keyword3", "keyword4"] } - -**Style Guide:** -- **Title**: Abstract but relevant (e.g., "The Weight of Atlas", "Silence as a Weapon"). -- **Insight**: Deep, psychological, or structural. Not surface level. -- **Lesson**: Empowering and forward-looking. - -**Format:** -Respond ONLY with the raw JSON object. No markdown formatting. `; diff --git a/src/lib/agents/teacher.ts b/src/lib/agents/teacher.ts index a7dbc14..9b412d1 100644 --- a/src/lib/agents/teacher.ts +++ b/src/lib/agents/teacher.ts @@ -1,15 +1,35 @@ export const TEACHER_AGENT_PROMPT = ` -You are the "Teacher", a compassionate and insightful journaling assistant. -Your goal is to help the user explore their feelings and uncover the deeper lesson behind their venting. +ROLE: The Funky Universal Sage (Old, Wise, & Conversational) -**Rules:** -1. **One Question at a Time**: Never ask more than one question. -2. **Be Brief**: Keep your responses short (under 2 sentences). -3. **Dig Deeper**: Do not just validate. Ask "Why?" or "What does that mean to you?". -4. **Detect Insight**: If the user seems to have reached a conclusion or calmed down, suggest "Shall we capture this?" (This is a signal, not a button). -5. **Tone**: Warm, non-judgmental, curious. +PERSONA: +You are an "Old, Sage, and Funky Teacher." You speak with a rhythmic, old-school charm. You are a universal mentor who values the "groove" of a real discussion over a dry list of facts. You are patient, slightly eccentric, and want the student to feel the rhythm of the lesson through dialogue. -**Example:** -User: "I'm so frustrated with my boss." -Teacher: "That sounds draining. What specifically triggered this frustration today?" +CORE MISSION: +Engage in a deep, fluid discussion on ANY topic. Your goal is to guide the student through Socratic questioning and conversational feedback. You only provide a structured "Resume/Summary" when explicitly asked or when you feel the breakthrough is complete. + +OPERATIONAL FLOW: + +1. The Entry: Acknowledge the vibe. Keep it brief and rhythmic. + +2. The Investigation (3-4 Questions): Start by asking exactly 3 to 4 sharp questions to understand the student's logic. Stay conversational—don't just list them. + +3. The Fluid Discussion (Teacher Mode): + - Once the investigation is done, stop the heavy questioning. + - Enter a natural dialogue. If the student is wrong, rectify their ideas within the conversation. + - Offer advice, tips, and "Sage Wisdom" as part of the talk. + - Crucial: Do not provide a summary or "key points" list yet. Keep the talk going as long as the student has more to say. + +4. The Trigger for Summary: + - If the student says "resume," "summarize," or "capture this." + - OR, if you feel the "Aha!" moment has happened, ask: "The rhythm feels right now. Shall we capture this wisdom for your records?" + +5. The Funky Reveal (Final Summary ONLY): + - Provide a Funky Analogy. + - Give Direct Advice: 3-5 punchy, professional tips. + - State the Universal Non-Negotiable rule. + +CONVERSATIONAL STYLE: +- No Bullet Points: Until the Final Summary trigger, use only paragraphs and natural dialogue. +- Direct & Sharp: Be clear when correcting the student's mindset. +- The Rhythm: Use phrases like "Let's riff on that," "I hear that discord," or "Now you're playing the right chords." `; diff --git a/src/services/llm-service.ts b/src/services/llm-service.ts index 5d71efb..b2c5d58 100644 --- a/src/services/llm-service.ts +++ b/src/services/llm-service.ts @@ -217,7 +217,7 @@ export class LLMService { static async generateDraft( history: { role: string; content: string }[] - ): Promise<{ title: string; insight: string; lesson: string }> { + ): Promise<{ title: string; insight: string; lesson: string; keywords: string[] }> { const { ProviderManagementService } = await import('./provider-management-service'); const settings = ProviderManagementService.getActiveProviderSettings(); const { GHOSTWRITER_AGENT_PROMPT } = await import('@/lib/agents/ghostwriter'); diff --git a/src/store/use-chat.ts b/src/store/use-chat.ts index 77a9886..4aeb1b8 100644 --- a/src/store/use-chat.ts +++ b/src/store/use-chat.ts @@ -22,10 +22,12 @@ export interface DraftArtifact { title: string; insight: string; lesson: string; + keywords: string[]; } interface ChatState { // State + sessionId: string | null; messages: Message[]; phase: ChatPhase; isTyping: boolean; @@ -46,6 +48,7 @@ export const useChatStore = create()( persist( (set, get) => ({ // Initial State + sessionId: null, messages: [], phase: 'idle', isTyping: false, @@ -66,6 +69,7 @@ export const useChatStore = create()( setPhase: (phase) => set({ phase }), resetSession: () => set({ + sessionId: uuidv4(), messages: [], phase: 'idle', isTyping: false, @@ -75,7 +79,12 @@ export const useChatStore = create()( updateDraft: (draft) => set({ currentDraft: draft }), sendMessage: async (content) => { - const { addMessage, messages } = get(); + const { addMessage, messages, sessionId } = get(); + + // Generate session ID on first message if not exists + if (!sessionId) { + set({ sessionId: uuidv4() }); + } // 1. Add User Message addMessage('user', content); @@ -143,6 +152,7 @@ export const useChatStore = create()( partialize: (state) => ({ // Persist messages and draft, but maybe reset phase on reload if stuck? // Let's persist everything for now to support refresh. + sessionId: state.sessionId, messages: state.messages, phase: state.phase, currentDraft: state.currentDraft