feat: improve journaling flow with better prompts and UX
- Fix: Add sessionId tracking to useChatStore for draft saving - feat: Update Teacher prompt to "Funky Universal Sage (Conversational)" - feat: Update Ghostwriter prompt to "Personal Diary Reporter" with keywords - feat: Redesign DraftSheet to match history rendering style - feat: Add keywords/tags support to drafts - feat: Change draft accept behavior to redirect to homepage Co-Authored-By: Claude (glm-4.7) <noreply@anthropic.com>
This commit is contained in:
@@ -1,105 +1,175 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useChatStore } from '@/store/use-chat';
|
import { useChatStore } from '@/store/use-chat';
|
||||||
import { Button } from '@/components/ui/button';
|
import { MessageCircle, Check } from 'lucide-react';
|
||||||
import {
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
|
||||||
SheetFooter,
|
|
||||||
} from '@/components/ui/sheet';
|
|
||||||
import { ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-react';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
||||||
export function DraftSheet() {
|
export function DraftSheet() {
|
||||||
const { phase, currentDraft, setPhase, resetSession } = useChatStore();
|
const { phase, currentDraft, setPhase, resetSession, sessionId } = useChatStore();
|
||||||
const isOpen = phase === 'review' && !!currentDraft;
|
const isOpen = phase === 'review' && !!currentDraft;
|
||||||
|
|
||||||
const handleKeep = async () => {
|
const handleAccept = async () => {
|
||||||
if (!currentDraft) return;
|
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) {
|
if (!sessionId) {
|
||||||
console.error("No active session ID");
|
console.error("No active session ID");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { DraftService } = await import('@/lib/db/draft-service');
|
||||||
|
|
||||||
await DraftService.saveDraft({
|
await DraftService.saveDraft({
|
||||||
sessionId,
|
sessionId,
|
||||||
title: currentDraft.title,
|
title: currentDraft.title,
|
||||||
content: currentDraft.lesson, // Using lesson as content for now, or construct full markdown?
|
content: currentDraft.lesson,
|
||||||
// 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.
|
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
completedAt: Date.now(),
|
completedAt: Date.now(),
|
||||||
tags: []
|
tags: currentDraft.keywords || []
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect to history or show success
|
// Close conversation and redirect to homepage
|
||||||
window.location.href = '/history';
|
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
window.location.href = '/';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save draft:", error);
|
console.error("Failed to save draft:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefine = () => {
|
const handleResume = () => {
|
||||||
// Logic for refinement (Story 3.5)
|
// Close the sheet and go back to chat
|
||||||
// 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.
|
|
||||||
setPhase('elicitation');
|
setPhase('elicitation');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!currentDraft) return null;
|
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 (
|
return (
|
||||||
<Sheet open={isOpen} onOpenChange={(open) => !open && handleRefine()}>
|
<Sheet open={isOpen} onOpenChange={(open) => !open && handleResume()}>
|
||||||
<SheetContent side="bottom" className="h-[80vh] sm:h-[600px] rounded-t-[20px] pt-10">
|
<SheetContent side="right" className="w-full sm:max-w-xl overflow-y-auto p-0">
|
||||||
<SheetHeader className="text-left mb-6">
|
<SheetHeader className="sr-only">
|
||||||
<SheetTitle className="font-serif text-3xl font-bold bg-gradient-to-r from-indigo-500 to-purple-600 bg-clip-text text-transparent">
|
<SheetTitle>Draft Review</SheetTitle>
|
||||||
{currentDraft.title}
|
<SheetDescription>Review your draft before saving</SheetDescription>
|
||||||
</SheetTitle>
|
|
||||||
<SheetDescription className="text-lg text-slate-600 italic">
|
|
||||||
" {currentDraft.insight} "
|
|
||||||
</SheetDescription>
|
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<div className="space-y-6 overflow-y-auto pb-20">
|
<div className="p-6">
|
||||||
<div className="prose dark:prose-invert max-w-none">
|
{/* Title */}
|
||||||
<h3 className="font-serif text-xl border-l-4 border-indigo-500 pl-4 py-1">
|
<h2 className="draft-title text-2xl sm:text-3xl font-bold text-foreground mb-6 font-serif leading-tight">
|
||||||
The Lesson
|
{currentDraft.title}
|
||||||
</h3>
|
</h2>
|
||||||
<p className="text-lg leading-relaxed text-slate-700 dark:text-slate-300">
|
|
||||||
{currentDraft.lesson}
|
{/* Insight */}
|
||||||
|
{currentDraft.insight && (
|
||||||
|
<p className="text-lg italic text-muted-foreground mb-6 border-l-4 border-primary/30 pl-4">
|
||||||
|
"{currentDraft.insight}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Body content - Markdown with prose styling */}
|
||||||
|
<div className="draft-body prose prose-slate dark:prose-invert max-w-none font-serif">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeHighlight, rehypeRaw]}
|
||||||
|
components={{
|
||||||
|
h1: ({ node, ...props }) => (
|
||||||
|
<h1 className="text-2xl font-bold text-foreground mt-8 mb-4 first:mt-0" {...props} />
|
||||||
|
),
|
||||||
|
h2: ({ node, ...props }) => (
|
||||||
|
<h2 className="text-xl font-bold text-foreground mt-6 mb-3" {...props} />
|
||||||
|
),
|
||||||
|
h3: ({ node, ...props }) => (
|
||||||
|
<h3 className="text-lg font-semibold text-foreground mt-5 mb-2" {...props} />
|
||||||
|
),
|
||||||
|
p: ({ node, ...props }) => (
|
||||||
|
<p className="text-base leading-relaxed text-muted-foreground mb-4" {...props} />
|
||||||
|
),
|
||||||
|
code: ({ node, inline, className, children, ...props }: any) => {
|
||||||
|
if (inline) {
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className="px-1.5 py-0.5 bg-muted text-foreground rounded text-sm font-mono"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className={`block bg-muted text-foreground p-4 rounded-lg text-sm font-mono overflow-x-auto ${className || ''}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pre: ({ node, ...props }) => (
|
||||||
|
<pre className="bg-muted p-4 rounded-lg overflow-x-auto mb-4" {...props} />
|
||||||
|
),
|
||||||
|
a: ({ node, ...props }) => (
|
||||||
|
<a className="text-primary hover:underline" {...props} />
|
||||||
|
),
|
||||||
|
ul: ({ node, ...props }) => (
|
||||||
|
<ul className="list-disc list-inside mb-4 text-muted-foreground space-y-1" {...props} />
|
||||||
|
),
|
||||||
|
ol: ({ node, ...props }) => (
|
||||||
|
<ol className="list-decimal list-inside mb-4 text-muted-foreground space-y-1" {...props} />
|
||||||
|
),
|
||||||
|
blockquote: ({ node, ...props }) => (
|
||||||
|
<blockquote className="border-l-4 border-muted-foreground/30 pl-4 italic text-muted-foreground my-4" {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentDraft.lesson}
|
||||||
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SheetFooter className="absolute bottom-0 left-0 right-0 p-6 bg-white/80 dark:bg-zinc-950/80 backdrop-blur-md border-t border-slate-200 flex flex-row gap-4 justify-between sm:justify-end">
|
{/* Keywords/Tags */}
|
||||||
<Button variant="outline" size="lg" className="flex-1 sm:flex-none gap-2" onClick={handleRefine}>
|
{currentDraft.keywords && currentDraft.keywords.length > 0 && (
|
||||||
<ThumbsDown className="w-5 h-5" />
|
<div className="flex flex-wrap gap-2 mt-6 pt-4 border-t border-border">
|
||||||
Refine
|
{currentDraft.keywords.map((keyword, index) => (
|
||||||
</Button>
|
<span
|
||||||
<Button size="lg" className="flex-1 sm:flex-none gap-2 bg-indigo-600 hover:bg-indigo-700 text-white" onClick={handleKeep}>
|
key={index}
|
||||||
<ThumbsUp className="w-5 h-5" />
|
className="px-3 py-1 bg-secondary text-secondary-foreground rounded-full text-sm font-sans"
|
||||||
Keep It
|
>
|
||||||
</Button>
|
#{keyword}
|
||||||
</SheetFooter>
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer with buttons */}
|
||||||
|
<nav className="sticky bottom-0 flex gap-3 p-4 bg-white border-t border-slate-200 mt-auto dark:bg-zinc-950 dark:border-zinc-800">
|
||||||
|
<button
|
||||||
|
onClick={handleResume}
|
||||||
|
type="button"
|
||||||
|
className="flex-1 min-h-[44px] px-4 py-3 border border-slate-300 rounded-md text-slate-700 hover:bg-slate-50 transition-colors flex items-center justify-center gap-2 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<MessageCircle className="w-5 h-5" />
|
||||||
|
<span>Resume the Talk</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleAccept}
|
||||||
|
type="button"
|
||||||
|
className="flex-1 min-h-[44px] px-4 py-3 bg-slate-800 text-white rounded-md hover:bg-slate-700 transition-colors flex items-center justify-center gap-2 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
|
||||||
|
>
|
||||||
|
<Check className="w-5 h-5" />
|
||||||
|
<span>Accept the draft</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
export const GHOSTWRITER_AGENT_PROMPT = `
|
export const GHOSTWRITER_AGENT_PROMPT = `
|
||||||
You are the "Ghostwriter", a master synthesizer of human experience.
|
ROLE: The Personal Diary Reporter
|
||||||
Your goal is to transform a messy venting session into a structured, crystalline "Enlightenment" artifact.
|
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:**
|
TONE & PERSPECTIVE:
|
||||||
A conversation history between a User and a Teacher.
|
- 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:**
|
STORY STRUCTURE:
|
||||||
A JSON object with the following 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",
|
"title": "A catchy, personal blog-style title for the entry",
|
||||||
"insight": "The core realization (1-2 sentences)",
|
"insight": "A one-sentence core realization or 'Aha!' moment from the session",
|
||||||
"lesson": "The actionable takeaway or philosophical shift (1-2 sentences)"
|
"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.
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
export const TEACHER_AGENT_PROMPT = `
|
export const TEACHER_AGENT_PROMPT = `
|
||||||
You are the "Teacher", a compassionate and insightful journaling assistant.
|
ROLE: The Funky Universal Sage (Old, Wise, & Conversational)
|
||||||
Your goal is to help the user explore their feelings and uncover the deeper lesson behind their venting.
|
|
||||||
|
|
||||||
**Rules:**
|
PERSONA:
|
||||||
1. **One Question at a Time**: Never ask more than one question.
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
**Example:**
|
CORE MISSION:
|
||||||
User: "I'm so frustrated with my boss."
|
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.
|
||||||
Teacher: "That sounds draining. What specifically triggered this frustration today?"
|
|
||||||
|
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."
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export class LLMService {
|
|||||||
|
|
||||||
static async generateDraft(
|
static async generateDraft(
|
||||||
history: { role: string; content: string }[]
|
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 { ProviderManagementService } = await import('./provider-management-service');
|
||||||
const settings = ProviderManagementService.getActiveProviderSettings();
|
const settings = ProviderManagementService.getActiveProviderSettings();
|
||||||
const { GHOSTWRITER_AGENT_PROMPT } = await import('@/lib/agents/ghostwriter');
|
const { GHOSTWRITER_AGENT_PROMPT } = await import('@/lib/agents/ghostwriter');
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ export interface DraftArtifact {
|
|||||||
title: string;
|
title: string;
|
||||||
insight: string;
|
insight: string;
|
||||||
lesson: string;
|
lesson: string;
|
||||||
|
keywords: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatState {
|
interface ChatState {
|
||||||
// State
|
// State
|
||||||
|
sessionId: string | null;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
phase: ChatPhase;
|
phase: ChatPhase;
|
||||||
isTyping: boolean;
|
isTyping: boolean;
|
||||||
@@ -46,6 +48,7 @@ export const useChatStore = create<ChatState>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
// Initial State
|
// Initial State
|
||||||
|
sessionId: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
phase: 'idle',
|
phase: 'idle',
|
||||||
isTyping: false,
|
isTyping: false,
|
||||||
@@ -66,6 +69,7 @@ export const useChatStore = create<ChatState>()(
|
|||||||
setPhase: (phase) => set({ phase }),
|
setPhase: (phase) => set({ phase }),
|
||||||
|
|
||||||
resetSession: () => set({
|
resetSession: () => set({
|
||||||
|
sessionId: uuidv4(),
|
||||||
messages: [],
|
messages: [],
|
||||||
phase: 'idle',
|
phase: 'idle',
|
||||||
isTyping: false,
|
isTyping: false,
|
||||||
@@ -75,7 +79,12 @@ export const useChatStore = create<ChatState>()(
|
|||||||
updateDraft: (draft) => set({ currentDraft: draft }),
|
updateDraft: (draft) => set({ currentDraft: draft }),
|
||||||
|
|
||||||
sendMessage: async (content) => {
|
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
|
// 1. Add User Message
|
||||||
addMessage('user', content);
|
addMessage('user', content);
|
||||||
@@ -143,6 +152,7 @@ export const useChatStore = create<ChatState>()(
|
|||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
// Persist messages and draft, but maybe reset phase on reload if stuck?
|
// Persist messages and draft, but maybe reset phase on reload if stuck?
|
||||||
// Let's persist everything for now to support refresh.
|
// Let's persist everything for now to support refresh.
|
||||||
|
sessionId: state.sessionId,
|
||||||
messages: state.messages,
|
messages: state.messages,
|
||||||
phase: state.phase,
|
phase: state.phase,
|
||||||
currentDraft: state.currentDraft
|
currentDraft: state.currentDraft
|
||||||
|
|||||||
Reference in New Issue
Block a user