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:
Max
2026-01-27 13:28:59 +07:00
parent 8fe6eb2547
commit 02ac9762e4
5 changed files with 204 additions and 94 deletions

View File

@@ -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 (
<Sheet open={isOpen} onOpenChange={(open) => !open && handleRefine()}>
<SheetContent side="bottom" className="h-[80vh] sm:h-[600px] rounded-t-[20px] pt-10">
<SheetHeader className="text-left mb-6">
<SheetTitle className="font-serif text-3xl font-bold bg-gradient-to-r from-indigo-500 to-purple-600 bg-clip-text text-transparent">
{currentDraft.title}
</SheetTitle>
<SheetDescription className="text-lg text-slate-600 italic">
" {currentDraft.insight} "
</SheetDescription>
<Sheet open={isOpen} onOpenChange={(open) => !open && handleResume()}>
<SheetContent side="right" className="w-full sm:max-w-xl overflow-y-auto p-0">
<SheetHeader className="sr-only">
<SheetTitle>Draft Review</SheetTitle>
<SheetDescription>Review your draft before saving</SheetDescription>
</SheetHeader>
<div className="space-y-6 overflow-y-auto pb-20">
<div className="prose dark:prose-invert max-w-none">
<h3 className="font-serif text-xl border-l-4 border-indigo-500 pl-4 py-1">
The Lesson
</h3>
<p className="text-lg leading-relaxed text-slate-700 dark:text-slate-300">
{currentDraft.lesson}
<div className="p-6">
{/* Title */}
<h2 className="draft-title text-2xl sm:text-3xl font-bold text-foreground mb-6 font-serif leading-tight">
{currentDraft.title}
</h2>
{/* Insight */}
{currentDraft.insight && (
<p className="text-lg italic text-muted-foreground mb-6 border-l-4 border-primary/30 pl-4">
"{currentDraft.insight}"
</p>
)}
{/* 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>
{/* Keywords/Tags */}
{currentDraft.keywords && currentDraft.keywords.length > 0 && (
<div className="flex flex-wrap gap-2 mt-6 pt-4 border-t border-border">
{currentDraft.keywords.map((keyword, index) => (
<span
key={index}
className="px-3 py-1 bg-secondary text-secondary-foreground rounded-full text-sm font-sans"
>
#{keyword}
</span>
))}
</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">
<Button variant="outline" size="lg" className="flex-1 sm:flex-none gap-2" onClick={handleRefine}>
<ThumbsDown className="w-5 h-5" />
Refine
</Button>
<Button size="lg" className="flex-1 sm:flex-none gap-2 bg-indigo-600 hover:bg-indigo-700 text-white" onClick={handleKeep}>
<ThumbsUp className="w-5 h-5" />
Keep It
</Button>
</SheetFooter>
{/* 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>
</Sheet>
);