From db2e7d80178bd324de8acae45987eda3e8dcde5b Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 9 Nov 2025 15:57:26 +0000 Subject: [PATCH] feat: Add Save/Save Draft button to chat interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add dynamic button that shows "Save" when no draft exists - Changes to "Save Draft" when a pending draft is in progress - Uses floppy disk icon (IconDeviceFloppy) for visual consistency - Clicking "Save" creates empty draft and navigates to edit page - Clicking "Save Draft" navigates to edit page with existing draft - Reactive state tracking using useSelector for draft state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/chat/page.tsx | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/app/chat/page.tsx b/app/chat/page.tsx index fd71cba..ea64cb3 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -15,12 +15,13 @@ import { Tooltip, } from '@mantine/core'; import { useRef, useEffect, useState } from 'react'; -import { IconVolume, IconMicrophone, IconNotes } from '@tabler/icons-react'; +import { IconVolume, IconMicrophone, IconDeviceFloppy } from '@tabler/icons-react'; import { UserMenu } from '@/components/UserMenu'; import { useVoiceMode } from '@/hooks/useVoiceMode'; import { useAppMachine } from '@/hooks/useAppMachine'; import { notifications } from '@mantine/notifications'; import { useMediaQuery } from '@mantine/hooks'; +import { useSelector } from '@xstate/react'; /** * Get the voice button text based on the current state @@ -62,6 +63,10 @@ export default function ChatPage() { // State for creating node const [isCreatingNode, setIsCreatingNode] = useState(false); + // Check if we have a pending draft (using useSelector for reactivity) + const pendingNodeDraft = useSelector(appActor, (state) => state.context.pendingNodeDraft); + const hasPendingDraft = !!pendingNodeDraft; + // Use the clean voice mode hook const { state, send, transcript, error } = useVoiceMode({ messages, @@ -122,6 +127,34 @@ export default function ChatPage() { } }; + // Handler for Manual/Save button + const handleManualOrSave = () => { + if (hasPendingDraft && pendingNodeDraft) { + // If we have a draft, navigate to edit with it + appActor.send({ + type: 'NAVIGATE_TO_EDIT', + draft: pendingNodeDraft, + }); + } else { + // Create an empty draft for manual entry + const emptyDraft = { + title: '', + content: '', + conversationContext: messages.map((m) => { + if ('parts' in m && Array.isArray((m as any).parts)) { + return `${m.role}: ${(m as any).parts.find((p: any) => p.type === 'text')?.text || ''}`; + } + return `${m.role}: ${(m as any).content || ''}`; + }).join('\n'), + }; + + appActor.send({ + type: 'CREATE_NODE_FROM_CONVERSATION', + draft: emptyDraft, + }); + } + }; + // Add initial greeting message on first load useEffect(() => { if (messages.length === 0) { @@ -377,6 +410,15 @@ export default function ChatPage() { variant="filled" disabled={isVoiceActive} /> +