diff --git a/.env.example b/.env.example index 7e4dab3..0f16266 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,5 @@ FEATURE_FLAG_NEW_UI=true OPENAI_API_KEY=your_openai_api_key_here LLM_MODEL=gpt-4o-mini LLM_TEMPERATURE=0.7 +# Security +APP_PASSWORD=password diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index 416b271..010062f 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -1,6 +1,6 @@ -# generated: 2026-01-24 -# project: Test01 -# project_key: TEST01 +# generated: 2026-01-27 +# project: Brachnha Insights +# project_key: BRACHNHA # tracking_system: file-system # story_location: /home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts @@ -33,41 +33,43 @@ # - SM typically creates next story after previous one is 'done' to incorporate learnings # - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended) -generated: 2026-01-24 -project: Test01 -project_key: TEST01 +generated: 2026-01-27 +project: Brachnha Insights +project_key: BRACHNHA tracking_system: file-system story_location: /home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts development_status: - # Epic 1: "Active Listening" - Core Chat & Teacher Agent + # Epic 1: Gatekeeper Security epic-1: done - 1-1-local-first-setup-chat-storage: done - 1-2-chat-interface-implementation: done - 1-3-teacher-agent-logic-intent-detection: done - 1-4-fast-track-mode: done + 1-1-security-middleware-lock-screen: done + 1-2-server-side-validation-app-password: done + 1-3-session-persistence: done epic-1-retrospective: done - # Epic 2: "The Magic Mirror" - Ghostwriter & Draft Refinement + # Epic 2: Project Calibration epic-2: done - 2-1-ghostwriter-agent-markdown-generation: done - 2-2-draft-view-ui-the-slide-up: done - 2-3-refinement-loop-regeneration: done - 2-4-export-copy-actions: done + 2-1-settings-feature-shell: done + 2-2-provider-management-crud: done + 2-3-secure-credentials-storage: done + 2-4-connection-validation: done + 2-5-active-provider-switcher: done epic-2-retrospective: done - # Epic 3: "My Legacy" - History, Offline Sync & PWA Polish + # Epic 3: The Venting Ritual epic-3: done - 3-1-history-feed-ui: done - 3-2-deletion-management: done - 3-3-offline-sync-queue: done - 3-4-pwa-install-prompt-manifest: done - epic-3-retrospective: optional + 3-1-chat-interface-state: done + 3-2-teacher-agent-elicitation-logic: done + 3-3-ghostwriter-agent-draft-generation: done + 3-4-draft-review-ui-slide-up: done + 3-5-regeneration-loop-refinement: done + epic-3-retrospective: done - # Epic 4: "Power User Settings" - BYOD & Configuration + # Epic 4: Journey Management epic-4: done - 4-1-api-provider-configuration-ui: done - 4-2-connection-validation: done - 4-3-model-selection-configuration: done - 4-4-provider-switching: done - epic-4-retrospective: optional + 4-1-history-feed-ui: done + 4-2-detailed-artifact-view: done + 4-3-action-menu-export-delete: done + 4-4-offline-sync-queue: done + 4-5-data-export-utility: done + epic-4-retrospective: done diff --git a/_bmad-output/planning-artifacts/epics.md b/_bmad-output/planning-artifacts/epics.md index e97c0d2..818677c 100644 --- a/_bmad-output/planning-artifacts/epics.md +++ b/_bmad-output/planning-artifacts/epics.md @@ -1,450 +1,373 @@ --- -stepsCompleted: - - step-01-validate-prerequisites.md - - step-02-design-epics.md - - step-03-create-stories.md - - step-04-final-validation.md +stepsCompleted: ['step-01-validate-prerequisites', 'step-02-design-epics', 'step-03-create-stories', 'step-04-final-validation'] inputDocuments: - - file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/prd.md - - file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/architecture.md - - file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md + - /home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/prd.md + - /home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/architecture.md + - /home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md --- -# Test01 - Epic Breakdown +# Brachnha - Epic Breakdown ## Overview -This document provides the complete epic and story breakdown for Test01, decomposing the requirements from the PRD, UX Design if it exists, and Architecture requirements into implementable stories. +This document provides the complete epic and story breakdown for Brachnha, decomposing the requirements from the PRD, UX Design, and Architecture requirements into implementable stories. ## Requirements Inventory ### Functional Requirements -FR-01: System can detect "Venting" vs. "Insight" intent from initial user input. -FR-02: "Teacher Agent" can generate probing questions to elicit specific missing details based on the user's initial input. -FR-03: "Ghostwriter Agent" can transform the structured interview data into a grammatically correct and structured "Enlightenment" artifact (e.g., Markdown post). -FR-04: Users can "Regenerate" the outcome with specific critique (e.g., "Make it less corporate", "Focus more on the technical solution"). -FR-05: System provides a "Fast Track" option to bypass the interview and go straight to generation for advanced users. -FR-06: Users can view a chronological feed of past "Enlightenments" (history). -FR-07: Users can "One-Click Copy" the formatted text to clipboard. -FR-08: Users can delete past entries. -FR-09: Users can edit the generated draft manually before exporting. -FR-10: Users can access the app and view history while offline. -FR-11: Users can complete a full "Venting Session" offline; system queues generation for reconnection. -FR-12: System actively prompts users to "Add to Home Screen" (A2HS) upon meeting engagement criteria. -FR-13: System stores all chat history locally (persistent client-side storage) by default. -FR-14: Users can export their entire history as a JSON/Markdown file. +FR1: System can detect "Venting" vs. "Insight" intent from initial user input. +FR2: "Teacher Agent" can generate probing questions to elicit specific missing details based on the user's initial input. +FR3: "Ghostwriter Agent" can transform the structured interview data into a grammatically correct and structured "Enlightenment" artifact (e.g., Markdown post). +FR4: Users can "Regenerate" the outcome with specific critique (e.g., "Make it less corporate", "Focus more on the technical solution"). +FR5: System provides a "Fast Track" option to bypass the interview and go straight to generation for advanced users. +FR6: Users can view a chronological feed of past "Enlightenments" (history). +FR7: Users can "One-Click Copy" the formatted text to clipboard. +FR8: Users can delete past entries. +FR9: Users can edit the generated draft manually before exporting. +FR10: Users can access the app and view history while offline. +FR11: Users can complete a full "Venting Session" offline; system queues generation for reconnection. +FR12: System actively prompts users to "Add to Home Screen" (A2HS) upon meeting engagement criteria. +FR13: System stores all chat history locally (persistent client-side storage) by default. +FR14: Users can export their entire history as a JSON/Markdown file. +FR15: Users can configure a custom OpenAI-compatible Base URL (e.g., `https://api.deepseek.com/v1`). +FR16: Users can securely save API Credentials (stored in local storage, never transmitted to backend). +FR17: Users can specify the Model Name (e.g., `gpt-4o`, `deepseek-chat`). +FR18: System validates the connection to the custom provider upon saving. +FR19: Users can switch between configured providers globally. +FR20: System presents a lock screen upon initial load if not authenticated. +FR21: System validates user-entered password against server-side `APP_PASSWORD`. +FR22: Authenticated session persists (via secure cookie) to prevent frequent logouts on personal devices. ### NonFunctional Requirements -NFR-01 (Chat Latency): The "Teacher" agent must generate the first follow-up question within < 3 seconds to maintain conversational flow. -NFR-02 (App Load Time): The app must be interactive (Time to Interactive) in < 1.5 seconds on 4G networks. -NFR-03 (Data Sovereignty): User chat logs are stored 100% Client-Side (persistent client-side storage) in the MVP. No user content is sent to the cloud except for the temporary API inference call. -NFR-04 (Inference Privacy): Data sent to the LLM API must be stateless (not used for training). -NFR-05 (Offline Behavior): The app shell and local history must remain accessible in Aeroplane Mode. Active Chat interactions will be unavailable offline as they require live LLM access. -NFR-06 (Data Persistence): Drafts must be auto-saved locally every 2 seconds to prevent data loss. -NFR-07 (Visual Accessibility): Dark Mode is the default. Contrast ratios must meet WCAG AA standards to reduce eye strain for late-night users. +NFR1: (Chat Latency) The "Teacher" agent must generate the first follow-up question within **< 3 seconds** to maintain conversational flow. +NFR2: (App Load Time) The app must be interactive (Time to Interactive) in **< 1.5 seconds** on 4G networks. +NFR3: (Data Sovereignty) User chat logs AND API Keys are stored **100% Client-Side** (persistent client-side storage). No user content or keys are sent to any middle-man server. +NFR4: (Inference Privacy) Data sent to the user-configured LLM API must be stateless (not used for training, subject to provider terms). +NFR5: (Offline Behavior) The app shell and local history must remain accessible in Aeroplane Mode. Active Chat interactions will be unavailable offline as they require live LLM access. +NFR6: (Data Persistence) Drafts must be auto-saved locally every **2 seconds** to prevent data loss. +NFR7: (Visual Accessibility) Dark Mode is the default. Contrast ratios must meet **WCAG AA** standards to reduce eye strain for late-night users. +NFR8: (Secure Key Storage) API Keys must be encrypted at rest or stored in secure local storage capabilities where possible, and never included in exports/logs. +NFR9: (Gatekeeper Security) The app must restrict access to the UI via a simple, high-protection login screen backed by a server-side `APP_PASSWORD` environment variable. This protects personal deployments (VPS) from unauthorized public access. ### Additional Requirements -- [Arch] Use Next.js 14+ App Router + ShadCN UI starter template -- [Arch] Implement "Local-First" architecture with Dexie.js (IndexedDB) -- [Arch] Implement Vercel Edge Functions for secure LLM API proxy -- [Arch] Use Zustand for global state management -- [Arch] Implement Service Worker for offline support and sync queue -- [UX] Implement "Morning Mist" theme with Inter (UI) and Merriweather (Content) fonts -- [UX] Implement "Chat" vs "Draft" view split pattern/slide-up sheet -- [UX] Ensure mobile-first responsive design (375px+) with centered container for desktop -- [UX] Adhere to WCAG AA accessibility standards (contrast, focus, zoom) +- [Architecture] Use Next.js 14+ (App Router) with ShadCN UI and Tailwind CSS. +- [Architecture] Use Zustand v5 for Global State Management. +- [Architecture] Use Dexie.js v4.2.1 for Client-Side Database (IndexedDB). +- [Architecture] Use Service Workers for Offline capabilities. +- [Architecture] Implement "Logic Sandwich" Service Layer Pattern (UI -> Store -> Service -> DB). +- [Architecture] Vercel Edge Runtime for API Routes (Proxy). +- [UX] Mobile-First Design targeting 375px+ screens; Desktop centered max 600px. +- [UX] "Morning Mist" Theme (Pastel/Calm colors). +- [UX] Custom Chat Bubbles (Telegram-style). +- [UX] Slide-Up Draft View for "Magic Moment". +- [UX] Accessibility: WCAG AA Compliance, High Refresh Rate support. ### FR Coverage Map -FR-01: Epic 1 - Initial intent detection logic in the main chat loop. -FR-02: Epic 1 - Teacher agent logic and prompt engineering for elicitation. -FR-03: Epic 2 - Ghostwriter agent logic and Markdown artifact generation. -FR-04: Epic 2 - Regeneration workflow for draft refinement. -FR-05: Epic 1 - Option to skip straight to generation (Fast Track). -FR-06: Epic 3 - History feed UI and data retrieval. -FR-07: Epic 2 - Copy to clipboard functionality in draft view. -FR-08: Epic 3 - Deletion management in history feed. -FR-09: Epic 2 - Manual editing capabilities for generated drafts. -FR-10: Epic 3 - Offline history access via IndexedDB. -FR-11: Epic 3 - Offline/Online sync queue for venting sessions. -FR-12: Epic 3 - PWA installation prompt logic. -FR-13: Epic 1 - Chat storage infrastructure (Dexie.js). -FR-14: Epic 3 - Data export functionality. -FR-15: Epic 4 (Story 4.1) - Custom API URL configuration. -FR-16: Epic 4 (Story 4.1) - Secure local credential storage. -FR-17: Epic 4 (Story 4.3) - Model selection logic. -FR-18: Epic 4 (Story 4.2) - Connection validation. -FR-19: Epic 4 (Story 4.4) - Provider switching logic. +FR1: Epic 3 - Venting Intent Detection +FR2: Epic 3 - Teacher Agent Elicitation +FR3: Epic 3 - Ghostwriter Artifact Generation +FR4: Epic 3 - Regeneration & Critique +FR5: Epic 3 - Fast Track Mode +FR6: Epic 4 - History Feed +FR7: Epic 4 - One-Click Copy +FR8: Epic 4 - Delete Entry +FR9: Epic 4 - Manual Editing +FR10: Epic 4 - Offline Access +FR11: Epic 4 - Offline Queueing +FR12: Epic 4 - A2HS Prompt +FR13: Epic 4 - Local Storage Persistence +FR14: Epic 4 - Data Export +FR15: Epic 2 - Custom Base URL +FR16: Epic 2 - Secure Credential Storage +FR17: Epic 2 - Model Selection +FR18: Epic 2 - Connection Validation +FR19: Epic 2 - Provider Switching +FR20: Epic 1 - Lock Screen UI +FR21: Epic 1 - Password Validation +FR22: Epic 1 - Session Persistence ## Epic List -### Epic 1: "Active Listening" - Core Chat & Teacher Agent -**Goal:** Enable users to start a session, "vent" their raw thoughts, and have the system "Active Listen" (store chat) and "Teach" (probe for details) using a local-first architecture. -**User Outcome:** Users can open the app, chat safely (locally), and get probing questions from the AI. -**FRs covered:** FR-01, FR-02, FR-05, FR-13 -**NFRs:** NFR-01, NFR-03, NFR-04 +### Epic 1: Gatekeeper Security -### Epic 2: "The Magic Mirror" - Ghostwriter & Draft Refinement -**Goal:** Transform the structured chat context into a tangible "Enlightenment" artifact (the post) that users can review, refine, and export. -**User Outcome:** Users get a high-quality post from their vent, which they can edit and ultimately copy for publishing. -**FRs covered:** FR-03, FR-04, FR-07, FR-09 -**NFRs:** NFR-07 (Visuals), NFR-04 +Establish a secure perimeter for the application to prevent unauthorized access in public deployment scenarios (VPS). -### Epic 3: "My Legacy" - History, Offline Action Replay & PWA Polish -**Goal:** Turn single sessions into a persistent "Journal" of growth, ensuring the app works flawlessly offline and behaves like a native app. -**User Outcome:** Users can view past wins, use the app on the subway (offline), and install it to their home screen. -**FRs covered:** FR-06, FR-08, FR-10, FR-11, FR-12, FR-14 -**NFRs:** NFR-02, NFR-05, NFR-06 +### Story 1.1: Security Middleware & Lock Screen -### Epic 4: "Power User Settings" - BYOD & Configuration -**Goal:** Enable users to bring their own Intelligence (BYOD) by configuring custom API providers, models, and keys, satisfying the "Privacy-First" and "Vendor Independence" requirements. -**User Outcome:** Users can configure and switch between different AI providers with their own API keys, ensuring data privacy and vendor flexibility. -**FRs covered:** FR-15, FR-16, FR-17, FR-18, FR-19 -**NFRs:** NFR-03 (Data Sovereignty), NFR-08 (Secure Key Storage) - - -## Epic 1: "Active Listening" - Core Chat & Teacher Agent - -**Goal:** Enable users to start a session, "vent" their raw thoughts, and have the system "Active Listen" (store chat) and "Teach" (probe for details) using a local-first architecture. - -### Story 1.1: Local-First Setup & Chat Storage - -As a user, -I want my chat sessions to be saved locally on my device, -So that my data is private and accessible offline. +As a Personal User, +I want the application to block all access until I log in, +So that my private journal remains secure on the public web. **Acceptance Criteria:** -**Given** a new user visits the app -**When** they load the page -**Then** a Dexie.js database is initialized with the correct schema -**And** no data is sent to the server without explicit action +**Given** I am an unauthenticated user accessing any route +**When** I load the page +**Then** I should be redirected to `/login` +**And** I should see a simple "Enter Password" screen +**And** I should not see any application UI or data -**Given** the user sends a message -**When** the message is sent -**Then** it is stored in the `chatLogs` table in IndexedDB with a timestamp -**And** is immediately displayed in the UI +### Story 1.2: Server-Side Validation (APP_PASSWORD) -**Given** the user reloads the page -**When** the page loads -**Then** the previous chat history is retrieved from IndexedDB and displayed correctly -**And** the session state is restored - -**Given** the device is offline -**When** the user opens the app -**Then** the app loads successfully and shows stored history from the local database - -### Story 1.2: Chat Interface Implementation - -As a user, -I want a clean, familiar chat interface, -So that I can focus on venting without fighting the UI. +As a System Admin (User), +I want to secure the app with a server-side environment variable, +So that I don't need to manage a database of users. **Acceptance Criteria:** -**Given** a user is on the main chat screen -**When** they look at the UI -**Then** they see a "Morning Mist" themed interface with distinct bubbles for User (Right) and AI (Left) -**And** the design matches the "Telegram-style" visual specification +**Given** I have set `APP_PASSWORD` in my `.env` +**When** I enter the matching password into the login form +**Then** The server should validate it +**And** Return a secure HTTP-only cookie +**And** Allow access to the app -**Given** the user is typing -**When** they press "Send" -**Then** the input field clears and the message appears in the chat -**And** the view scrolls to the bottom +**Given** I enter the wrong password +**When** I submit the form +**Then** I should see an invalid password error -**Given** the user is on a mobile device -**When** they view the chat -**Then** the layout is responsive and all touch targets are at least 44px -**And** the text size is legible (Inter font) +### Story 1.3: Session Persistence -**Given** the AI is processing -**When** the user waits -**Then** a "Teacher is typing..." indicator is visible -**And** the UI remains responsive - -### Story 1.3: Teacher Agent Logic & Intent Detection - -As a user, -I want the AI to understand if I'm venting or sharing an insight, -So that it responds appropriately. +As a Daily User, +I want my login to be remembered for 30 days, +So that I don't have to type the password every time I open the app on my phone. **Acceptance Criteria:** -**Given** a user sends a first message -**When** the AI processes it -**Then** it classifies the intent as "Venting" or "Insight" -**And** stores this context in the session state +**Given** I have successfully logged in +**When** I close and reopen the browser +**Then** I should remain logged in without re-entering the password -**Given** the intent is "Venting" -**When** the AI responds -**Then** it validates the emotion first -**And** asks a probing question to uncover the underlying lesson +**Given** I click "Logout" in settings +**When** I confirm +**Then** My session cookie should be destroyed +**And** I should be redirected to the login screen -**Given** the AI is generating a response -**When** the request is sent -**Then** it makes a direct client-side request to the configured Provider -**And** the user's stored API key is retrieved from local secure storage +### Epic 2: Project Calibration (BYOD Setup) -**Given** the API response takes time -**When** the user waits -**Then** the response time is optimized to be under 3 seconds for the first token (if streaming) +Enable users to configure and manage their own AI provider connections, ensuring privacy and operational capability. -### Story 1.4: Fast Track Mode +### Story 2.1: Settings Feature Shell + +As a User, +I want a dedicated settings area, +So that I can manage my application preferences and configurations. + +**Acceptance Criteria:** + +**Given** I am on the home screen +**When** I tap the "Settings" icon +**Then** A settings sheet or page should open +**And** I should see navigation tabs (General, AI Providers) +**And** I should see a Theme Toggle (Light/Dark) + +### Story 2.2: Provider Management (CRUD) As a Power User, -I want to bypass the interview questions, -So that I can generate a post immediately if I already have the insight. +I want to add my own custom LLM provider (like DeepSeek or OpenAI), +So that I can control the cost and intelligence behind the app. **Acceptance Criteria:** -**Given** a user is in the chat -**When** they toggle "Fast Track" or press a specific "Just Draft It" button -**Then** the AI skips the probing phase -**And** proceeds directly to the "Ghostwriter" generation phase (transition to Epic 2 workflow) +**Given** I am in the AI Providers settings tab +**When** I tap "Add Provider" +**Then** I should see a form for Name, Base URL, API Key, and Model Name +**And** I should be able to save this configuration to my local device -**Given** "Fast Track" is active -**When** the user sends their input -**Then** the system interprets it as the final insight -**And** immediately triggers the draft generation +**Given** I have an existing provider +**When** I edit it +**Then** The changes should be saved locally +### Story 2.3: Secure Credentials Storage -## Epic 2: "The Magic Mirror" - Ghostwriter & Draft Refinement - -**Goal:** Transform the structured chat context into a tangible "Enlightenment" artifact (the post) that users can review, refine, and export. - -### Story 2.1: Ghostwriter Agent & Markdown Generation - -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. +As a Privacy-Conscious User, +I want my API keys to be stored securely on my device, +So that they are never exposed to a third-party server. **Acceptance Criteria:** -**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) +**Given** I save a new provider with an API Key +**When** The data is persisted to localStorage +**Then** The API Key should be obfuscated (e.g., Base64 or encrypted) +**And** It should NOT be visible in plain text in the storage inspector +**And** It should never be logged in the console -**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 +### Story 2.4: Connection Validation -### Story 2.2: Draft View UI (The Slide-Up) - -As a user, -I want to view the generated draft in a clean, reading-focused interface, -So that I can review it without the distraction of the chat. +As a User, +I want to know if my API key works before I save it, +So that I don't get errors later when trying to chat. **Acceptance Criteria:** -**Given** the draft generation is complete -**When** the result is ready -**Then** a "Sheet" or modal slides up from the bottom -**And** it displays the post in "Medium-style" typography (Merriweather font) +**Given** I am adding a provider +**When** I fill in the details and tap "Test Connection" +**Then** The system should make a call to the provider's API (e.g., list models) +**And** Show a "Success" or "Error" message appropriately +**And** Block saving if the validation fails (optional, but recommended warning) -**Given** the draft view is open -**When** the user scrolls -**Then** the reading experience is comfortable with appropriate whitespace -**And** the "Thumbs Up" and "Thumbs Down" actions are sticky or easily accessible +### Story 2.5: Active Provider Switcher -### Story 2.3: Refinement Loop (Regeneration) - -As a user, -I want to provide feedback if the draft isn't right, -So that I can get a better version. +As a User, +I want to easily switch between my configured providers, +So that I can use a cheaper model for simple tasks and a smarter one for complex vents. **Acceptance Criteria:** -**Given** the user is viewing a draft -**When** they click "Thumbs Down" -**Then** the draft sheet closes and returns to the Chat UI -**And** the AI proactively asks "What should we change?" +**Given** I have multiple providers configured +**When** I select a different provider as "Active" +**Then** All future chat requests should use that provider's credentials +**And** The UI should reflect the currently active provider -**Given** the user provides specific critique (e.g., "Make it shorter") -**When** they send the feedback -**Then** the "Ghostwriter" regenerates the draft respecting the new constraint -**And** the new draft replaces the old one in the Draft View +### Epic 3: The Venting Ritual (Core) -### Story 2.4: Export & Copy Actions +Implement the core dual-agent pipeline that transforms user stress into structured insights via a guided chat interface. -As a user, -I want to copy the text or save the post, -So that I can publish it on LinkedIn or save it for later. +### Story 3.1: Chat Interface & State + +As a User, +I want a familiar chat interface, +So that I can express myself naturally without learning a new tool. **Acceptance Criteria:** -**Given** the user likes the draft -**When** they click "Thumbs Up" or "Copy" -**Then** the full Markdown text is copied to the clipboard -**And** a success toast/animation confirms the action +**Given** I open the app +**When** I am on the home screen +**Then** I should see a chat input at the bottom +**And** I should simply tap to start typing +**And** My messages should appear in "User Bubbles" (Right aligned) +**And** AI responses should appear in "AI Bubbles" (Left aligned) with a typing indicator -**Given** the draft is finalized -**When** the user saves it -**Then** it is marked as "Completed" in the local database -**And** the user is returned to the Home/History screen +### Story 3.2: Teacher Agent (Elicitation Logic) - -## Epic 3: "My Legacy" - History, Offline Sync & PWA Polish - -**Goal:** Turn single sessions into a persistent "Journal" of growth, ensuring the app works flawlessly offline and behaves like a native app. - -### Story 3.1: History Feed UI - -As a user, -I want to see a list of my past growing moments, -So that I can reflect on my journey. +As a Learner, +I want the AI to ask me probing questions, +So that I can uncover the deeper lesson behind my frustration. **Acceptance Criteria:** -**Given** the user is on the Home screen -**When** they view the feed -**Then** they see a chronological list of past "Completed" sessions (Title, Date, Tags) -**And** the list supports lazy loading/pagination for performance +**Given** I send a message like "I feel stupid" +**When** The "Teacher" agent processes it +**Then** It should NOT just say "It's okay" +**And** It SHOULD ask a follow-up question like "What specifically made you feel that way?" +**And** It should maintain a supportive, non-judgmental tone -**Given** the user clicks a history card -**When** the card opens -**Then** the full "Enlightenment" artifact allows for reading -**And** the "Copy" action is available +### Story 3.3: Ghostwriter Agent (Draft Generation) -### Story 3.2: Deletion & Management - -As a user, -I want to delete old entries, -So that I can control my private data. +As a User, +I want a focused "Drafting" moment, +So that I know when the venting is over and the value is created. **Acceptance Criteria:** -**Given** the user is viewing a past entry -**When** they select "Delete" -**Then** they are prompted with a confirmation dialog (Destructive Action) -**And** the action cannot be undone +**Given** I have answered the Teacher's questions +**When** Sufficient context is gathered OR I tap "Draft It" +**Then** The system should trigger the "Ghostwriter" agent +**And** It should consume the chat history +**And** It should generate a structured markdown artifact (Title, Insight, Lesson) -**Given** the deletion is confirmed -**When** the action completes -**Then** the entry is permanently removed from IndexedDB -**And** the History Feed updates immediately to remove the item +### Story 3.4: Draft Review UI (Slide-Up) -### Story 3.3: Offline Action Replay - -As a user, -I want my actions to be queued when offline, -So that I don't lose work on the subway. +As a User, +I want to see the generated insight clearly, +So that I can feel a sense of accomplishment. **Acceptance Criteria:** -**Given** the device is offline -**When** the user performs an LLM-dependent action (e.g., Send message, Regenerate draft) -**Then** the action is added to a persistent "Action Queue" in Dexie -**And** the UI shows a subtle "Offline - Queued" indicator +**Given** The Ghostwriter has finished +**When** The draft is ready +**Then** A "Slide-Up" sheet (or modal) should appear +**And** It should display the content with nice typography (Serif headers) +**And** It should have "Thumbs Up" (Keep) and "Thumbs Down" (Refine) buttons -**Given** connection is restored -**When** the app detects the network -**Then** the Sync Manager replays queued actions to the LLM API -**And** the indicator updates to "Processed" +### Story 3.5: Regeneration Loop (Refinement) -### Story 3.4: PWA Install Prompt & Manifest - -As a user, -I want to install the app to my home screen, -So that it feels like a native app. +As a User, +I want to critique the draft if it's wrong, +So that the final result feels authentic to me. **Acceptance Criteria:** -**Given** the user visits the web app -**When** the browser parses the site -**Then** it finds a valid `manifest.json` with correct icons, name ("Test01"), and `display: standalone` settings +**Given** I see a draft I don't like +**When** I tap "Thumbs Down" +**Then** The sheet should close +**And** The AI should ask "What needs to be changed?" +**And** My response should trigger a regeneration of the draft -**Given** the user has engaged with the app (e.g., completed 1 session) -**When** the browser supports it (beforeinstallprompt event) -**Then** a custom "Install App" UI element appears (non-intrusive) -**And** clicking it triggers the native install prompt +### Epic 4: Journey Management (History & Offline) -**Given** the app is installed -**When** it launches from Home Screen -**Then** it opens without the browser URL bar (Standalone mode) +Provide long-term value through history management, offline reliability, and data portability. +### Story 4.1: History Feed UI -## Epic 4: "Power User Settings" - BYOD & Configuration - -**Goal:** Enable users to bring their own Intelligence (BYOD) by configuring custom API providers, models, and keys, satisfying the "Privacy-First" and "Vendor Independence" requirements. - -### Story 4.1: API Provider Configuration UI - -As a user, -I want to enter my own API Key and Base URL, -So that I can use my own LLM account (e.g., DeepSeek, OpenAI). +As a User, +I want to browse my past "Legacy Logs", +So that I can reflect on my growth over time. **Acceptance Criteria:** -**Given** the user navigates to "Settings" -**When** they select "AI Provider" -**Then** they see a form to enter: "Base URL" (Default: OpenAI), "API Key", and "Model Name" +**Given** I tap the "History" tab +**When** The list loads +**Then** I should see a chronological list of cards +**And** Each card should show Date, Title, and a short summary +**And** It should support infinite scroll or pagination -**Given** the user enters a key -**When** they save -**Then** the key is stored in `localStorage` with basic encoding (not plain text) -**And** it is NEVER sent to the app backend (Client-Side only) +### Story 4.2: Detailed Artifact View -**Given** the user has saved a provider -**When** they return to chat -**Then** the new settings are active immediately - -### Story 4.2: Connection Validation - -As a user, -I want to know if my key works, -So that I don't get errors in the middle of a chat. +As a User, +I want to read a specific past insight, +So that I can reuse the content for my blog or resume. **Acceptance Criteria:** -**Given** the user enters new credentials -**When** they click "Connect" or "Save" -**Then** the system sends a tiny "Hello" request to the provider -**And** shows "Connected ✅" if successful, or the error message if failed +**Given** I am on the History Feed +**When** I tap a card +**Then** The "Detailed View" (similar to the Draft View) should open +**And** I should see the full formatted content +**And** I should NOT be able to "Regenerate" (it is read-only history) -### Story 4.3: Model Selection & Configuration +### Story 4.3: Action Menu (Export/Delete) -As a user, -I want to specify which AI model to use, -So that I can choose between different capabilities (e.g., fast vs. smart). +As a User, +I want to manage my individual entries, +So that I can delete bad ones or copy good ones. **Acceptance Criteria:** -**Given** the user is in the API Provider settings -**When** they view the form -**Then** they see a "Model Name" field with examples (e.g., "gpt-4o", "deepseek-chat") +**Given** I am viewing a History Card +**When** I tap the "..." menu +**Then** I should see "Copy to Clipboard" and "Delete" +**And** Tapping "Delete" should prompt for confirmation +**And** Confirming should remove it from the database immediately -**Given** the user enters a custom model name -**When** they save -**Then** the model name is stored alongside the API key and base URL -**And** all future LLM requests use this model identifier +### Story 4.4: Offline Sync Queue -**Given** the user doesn't specify a model -**When** they save provider settings -**Then** a sensible default is used (e.g., "gpt-3.5-turbo" for OpenAI endpoints) - -### Story 4.4: Provider Switching - -As a user, -I want to switch between different saved providers, -So that I can use different AI services for different needs. +As a Commuter, +I want to vent even when I have no signal (Offline), +So that I don't lose the thought. **Acceptance Criteria:** -**Given** the user has configured multiple providers -**When** they open Settings -**Then** they see a list of saved providers with labels (e.g., "OpenAI GPT-4", "DeepSeek Chat") +**Given** I am offline (Airplane Mode) +**When** I attempt to start a chat or send a message +**Then** The UI should allow it +**And** The system should queue the action in `syncQueue` (IndexedDB) +**And** It should show a "Waiting for connection..." status +**When** Connection is restored +**Then** The queue should auto-process -**Given** the user selects a different provider -**When** they confirm the switch -**Then** the app immediately uses the new provider for all LLM requests -**And** the active provider is persisted in local storage +### Story 4.5: Data Export Utility -**Given** the user starts a new chat session -**When** they send messages -**Then** the currently active provider is used -**And** the provider selection is maintained across page reloads +As a User, +I want to download all my data, +So that I have a backup independent of this browser. + +**Acceptance Criteria:** + +**Given** I am in Settings +**When** I tap "Export All Data" +**Then** The system should gather all Chat Logs and Drafts +**And** Generate a downloadable JSON or Markdown file +**And** Trigger the browser download prompt diff --git a/_bmad-output/planning-artifacts/prd.md b/_bmad-output/planning-artifacts/prd.md index baed7a8..6a75234 100644 --- a/_bmad-output/planning-artifacts/prd.md +++ b/_bmad-output/planning-artifacts/prd.md @@ -30,16 +30,16 @@ editHistory: changes: 'Added "Bring Your Own AI" (BYOD) Support: Custom Providers, API Key Management, and Settings.' --- -# Product Requirements Document - Test01 +# Product Requirements Document - Brachnha **Author:** Max **Date:** 2026-01-20 ## Executive Summary -**Product Vision:** "Test01" (The Pocket Mentor) is a Progressive Web App (PWA) designed to transform the daily struggles of learning into a polished "Legacy Log" of insights. It targets bootcamp graduates and self-learners who need to document their growth for recruiters but lack the energy to write from scratch. +**Product Vision:** "Brachnha" (The Pocket Mentor) is a Progressive Web App (PWA) designed to transform the daily struggles of learning into a polished "Journey Log" of insights. It targets bootcamp graduates and self-learners who need to document their growth for recruiters but lack the energy to write from scratch. -**Core Innovation:** Unlike passive note apps or raw AI writers, Test01 uses a **Dual-Agent Pipeline** ("Teacher" + "Ghostwriter"). It actively interviews the user to extract the "Lesson" from the "Complaint" ("Venting"), then synthesizing it into high-quality personal branding content. +**Core Innovation:** Unlike passive note apps or raw AI writers, Brachnha uses a **Dual-Agent Pipeline** ("Teacher" + "Ghostwriter"). It actively interviews the user to extract the "Lesson" from the "Complaint" ("Venting"), then synthesizing it into high-quality personal branding content. **Key Value:** Turns "I feel stupid today" into "Here is what I learned today." @@ -117,6 +117,10 @@ The goal is to prove that the *experience* of "guided enlightenment" is cleaner, * **Risk:** Users find the "Teacher" questions annoying/blocking. * **Mitigation:** Implement a "Fast Track" / "Just Write It" button in the UI to skip the interview if the user is ready. +**Security Risks:** +* **Risk:** Public deployment on VPS exposes personal journal. +* **Mitigation:** Implement "Gatekeeper" Authentication (NFR-09) to lock the app via `APP_PASSWORD`. + **Usability Risks:** * **Risk:** "Bring Your Own AI" configuration is too complex for non-technical users. * **Mitigation:** Provide clear, step-by-step guides for getting API keys. Pre-fill common provider templates (DeepSeek, OpenAI) so users only paste the key. @@ -128,7 +132,7 @@ The goal is to prove that the *experience* of "guided enlightenment" is cleaner, ```mermaid sequenceDiagram participant User as Alex (Learner) - participant UI as Test01 App + participant UI as Brachnha App participant Teacher as Teacher Agent participant Ghost as Ghostwriter Agent @@ -151,7 +155,7 @@ sequenceDiagram ### Journey 1: The "Legacy Log" (Primary Success) * **User:** Alex (The Exhausted Learner). * **Scene:** Alex finishes a deep study session at 11 PM. He's tired but feels a "click" of understanding after hours of struggle. -* **Action:** Opens Test01 to capture the win, not just to vent, but to immortalize the lesson: *"I finally get why dependency injection matters."* +* **Action:** Opens Brachnha to capture the win, not just to vent, but to immortalize the lesson: *"I finally get why dependency injection matters."* * **System Response:** The "Teacher" agent validates the insight and probes deeper: *"That's a huge breakthrough. What was the 'before' and 'after' mental model in your head?"* * **Transformation:** Alex articulates the specific shift in his thinking. * **Result:** The "Ghostwriter" agent drafts: *"The Moment Dependency Injection Clicked for Me."* @@ -208,9 +212,9 @@ sequenceDiagram * **Guided Transformation:** The UX pattern of transforming a raw, negative "Complaint" into a structured, positive "Insight" via a conversational interview is a novel interaction model for note-taking apps. ### Market Context & Competitive Landscape -* **vs. Passive Note Apps (Notion/Obsidian):** These require the user to do all the cognitive heaving lifting (synthesis). Test01 is "Active" and pulls the synthesis out of the user. -* **vs. Raw AI Writers (ChatGPT):** ChatGPT requires specific prompting and intent. Test01 acts as a partner that helps the user discover their intent ("What did I actually learn?"). -* **vs. Social Schedulers (Buffer/Hootsuite):** These manage distribution. Test01 manages *Creation* and *Ideation*. +* **vs. Passive Note Apps (Notion/Obsidian):** These require the user to do all the cognitive heaving lifting (synthesis). Brachnha is "Active" and pulls the synthesis out of the user. +* **vs. Raw AI Writers (ChatGPT):** ChatGPT requires specific prompting and intent. Brachnha acts as a partner that helps the user discover their intent ("What did I actually learn?"). +* **vs. Social Schedulers (Buffer/Hootsuite):** These manage distribution. Brachnha manages *Creation* and *Ideation*. ### Validation Approach * **The "Edit Distance" Metric:** Success is measured by how little the user has to edit the final draft. If the "Teacher" interview is effective, the "Ghostwriter" draft should be >90% ready. High edit rates indicate a failure in the elicitation phase. @@ -218,7 +222,7 @@ sequenceDiagram ## Web App Specific Requirements ### Project-Type Overview -Test01 is a **Progressive Web App (PWA)**. It must deliver a native-app-like experience in the browser, specifically designed for mobile usage during "in-between moments" (commuting, breaks). +Brachnha is a **Progressive Web App (PWA)**. It must deliver a native-app-like experience in the browser, specifically designed for mobile usage during "in-between moments" (commuting, breaks). ### Technical Architecture Considerations * **PWA Mechanics:** @@ -270,6 +274,11 @@ Test01 is a **Progressive Web App (PWA)**. It must deliver a native-app-like exp * **FR-18:** System validates the connection to the custom provider upon saving. * **FR-19:** Users can switch between configured providers globally. +### Security & Access Control +* **FR-20 (Gatekeeper):** System presents a lock screen upon initial load if not authenticated. +* **FR-21:** System validates user-entered password against server-side `APP_PASSWORD`. +* **FR-22:** Authenticated session persists (via secure cookie) to prevent frequent logouts on personal devices. + ## Non-Functional Requirements ### Performance & Responsiveness @@ -284,6 +293,7 @@ Test01 is a **Progressive Web App (PWA)**. It must deliver a native-app-like exp ### Reliability & Offline * **NFR-05 (Offline Behavior):** The app shell and local history must remain accessible in Aeroplane Mode. **Note:** Active Chat interactions will be unavailable offline as they require live LLM access. * **NFR-06 (Data Persistence):** Drafts must be auto-saved locally every **2 seconds** to prevent data loss. +* **NFR-09 (Gatekeeper Security):** The app must restrict access to the UI via a simple, high-protection login screen backed by a server-side `APP_PASSWORD` environment variable. This protects personal deployments (VPS) from unauthorized public access. ### Accessibility * **NFR-07 (Visual Accessibility):** Dark Mode is the default. Contrast ratios must meet **WCAG AA** standards to reduce eye strain for late-night users. diff --git a/_bmad-output/project-context.md b/_bmad-output/project-context.md index e045fc2..4944919 100644 --- a/_bmad-output/project-context.md +++ b/_bmad-output/project-context.md @@ -1,5 +1,5 @@ --- -project_name: 'Brachnha Insights' +project_name: 'Brachnha' user_name: 'Max' date: '2026-01-21' sections_completed: ['technology_stack', 'implementation_rules', 'naming_conventions', 'project_structure'] diff --git a/package-lock.json b/package-lock.json index 2562024..d6c3c8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ai-sdk/openai-compatible": "^2.0.18", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", "@testing-library/user-event": "^14.6.1", @@ -24,6 +25,7 @@ "lucide-react": "^0.562.0", "next": "16.1.4", "next-pwa": "^5.6.0", + "next-themes": "^0.4.6", "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^10.1.0", @@ -2504,6 +2506,44 @@ "npm": ">=10" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3357,6 +3397,73 @@ } } }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -3441,6 +3548,21 @@ } } }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", @@ -3468,6 +3590,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -3572,6 +3723,96 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", @@ -3661,6 +3902,37 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", @@ -3764,6 +4036,48 @@ } } }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -10957,6 +11271,16 @@ "next": ">=9.0.0" } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index cd2ca4e..85404ee 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@ai-sdk/openai-compatible": "^2.0.18", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", "@testing-library/user-event": "^14.6.1", @@ -27,6 +28,7 @@ "lucide-react": "^0.562.0", "next": "16.1.4", "next-pwa": "^5.6.0", + "next-themes": "^0.4.6", "react": "19.2.3", "react-dom": "19.2.3", "react-markdown": "^10.1.0", diff --git a/playwright.config.ts b/playwright.config.ts index 57c9be6..ea5479d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', - testIgnore: '**/component/**', + testIgnore: ['**/component/**', '**/unit/**'], fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, diff --git a/src/app/(main)/history/page.tsx b/src/app/(main)/history/page.tsx new file mode 100644 index 0000000..38455f1 --- /dev/null +++ b/src/app/(main)/history/page.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { HistoryFeed } from '@/components/features/journal/HistoryFeed'; +import { HistoryDetailSheet } from '@/components/features/journal/HistoryDetailSheet'; +import { useHistoryStore } from '@/lib/store/history-store'; + +export default function HistoryPage() { + const selectedDraft = useHistoryStore((s) => s.selectedDraft); + const closeDetail = useHistoryStore((s) => s.closeDetail); + + return ( +
+
+

Your Journey

+
+ + + + {/* Detail Sheet for viewing history items */} + {selectedDraft && ( + + )} +
+ ); +} diff --git a/src/app/(main)/settings/page.tsx b/src/app/(main)/settings/page.tsx index 53aa8c8..4b23a4f 100644 --- a/src/app/(main)/settings/page.tsx +++ b/src/app/(main)/settings/page.tsx @@ -21,6 +21,7 @@ import { ProviderForm } from "@/components/features/settings/provider-form"; import { useSavedProviders } from "@/store/use-settings"; import { ProviderManagementService } from "@/services/provider-management-service"; import { toast } from "@/hooks/use-toast"; +import { ThemeToggle } from "@/components/features/settings/theme-toggle"; export default function SettingsPage() { const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); @@ -57,27 +58,41 @@ export default function SettingsPage() {
Back to Home
-

Settings

-

+

Settings

+

Manage your AI provider connections and preferences.

+ {/* General Settings */} +
+
+
+

Appearance

+
+
+

+ Choose your preferred theme for the journaling experience. +

+ +
+
+ {/* Active Provider Section */}
-
+
-

Active Session Provider

+

Active Session Provider

-

+

Select which AI provider handles your current venting session. This setting applies immediately to new messages.

@@ -85,15 +100,15 @@ export default function SettingsPage() { {/* Manage Providers Section */}
-
+
-
-

Configuration

+
+

Configuration

-

+

Configure connection details for your AI models. Keys are stored locally in your browser.

- - - Add New Provider + + + Add New Provider
+ + + {/* Account Security Section */} +
+
+
+
+

Account Security

+
+

+ Lock the application to prevent unauthorized access on this device. +

+ +
+
+ {/* Edit Provider Dialog */} !open && closeDialogs()} > - - - Edit Provider + + + Edit Provider
+ ); } diff --git a/src/app/(session)/chat/page.tsx b/src/app/(session)/chat/page.tsx index b8394e6..6cb5cf7 100644 --- a/src/app/(session)/chat/page.tsx +++ b/src/app/(session)/chat/page.tsx @@ -4,23 +4,15 @@ import { useEffect, useState } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import { ChatWindow } from '@/components/features/chat/chat-window'; import { ChatInput } from '@/components/features/chat/chat-input'; -import { useSessionStore, useActiveSessionId, useTeacherStatus } from '@/store/use-session'; -import { ChatService } from '@/services/chat-service'; -import { toast } from 'sonner'; -import { Button } from "@/components/ui/button"; -import { DraftViewSheet } from "@/components/features/draft/DraftViewSheet"; -import { useChatStore } from "@/lib/store/chat-store"; -import { Loader2, ArrowLeft, Sparkles, Bot } from "lucide-react"; +import { DraftSheet } from '@/components/features/journal/draft-sheet'; +import { useChatStore } from '@/store/use-chat'; +import { ArrowLeft, Bot, Loader2 } from "lucide-react"; import Link from "next/link"; import { LLMService } from '@/services/llm-service'; import { ProviderManagementService } from '@/services/provider-management-service'; export default function ChatPage() { - const activeSessionId = useActiveSessionId(); - const teacherStatus = useTeacherStatus(); - const { setActiveSession } = useSessionStore((s) => s.actions); - const isDrafting = useChatStore((s) => s.isDrafting); - + const { resetSession, phase } = useChatStore(); const searchParams = useSearchParams(); const router = useRouter(); @@ -30,14 +22,11 @@ export default function ChatPage() { // Check for "new" param to force fresh session useEffect(() => { if (searchParams.get('new') === 'true') { - // Clear current session to trigger re-initialization - setActiveSession(null); - // Clear chat UI state - useChatStore.setState({ messages: [], currentDraft: null, showDraftView: false }); + resetSession(); // Clean URL router.replace('/chat'); } - }, [searchParams, router, setActiveSession]); + }, [searchParams, router, resetSession]); // Check Connection Status useEffect(() => { @@ -66,62 +55,10 @@ export default function ChatPage() { checkConnection(); }, []); - // Initialize Session on Mount - useEffect(() => { - const initSession = async () => { - // If activeSessionId is null (either initial load or just cleared by above effect) - if (!activeSessionId) { - try { - const newSessionId = await ChatService.createSession(); - setActiveSession(newSessionId); - } catch (error) { - console.error("Failed to create session:", error); - toast.error("Failed to start session. Check your database."); - } - } - }; - initSession(); - }, [activeSessionId, setActiveSession]); - - const handleSend = async (message: string) => { - if (!activeSessionId) return; - - try { - await ChatService.sendMessage(message, activeSessionId); - } catch (error: any) { - console.error(error); - if (error.message === 'AI Provider not configured') { - toast.error("Please configure your AI Provider in Settings", { - action: { - label: "Go to Settings", - onClick: () => window.location.href = '/settings' - } - }); - } else { - toast.error("Failed to send message. Please check connection."); - } - } - }; - - const handleFinishSession = async () => { - if (!activeSessionId) return; - - try { - toast.info("Generating your learning summary..."); - // Ensure store has latest messages for this session - await useChatStore.getState().hydrate(activeSessionId); - // Trigger Ghostwriter - await useChatStore.getState().generateDraft(activeSessionId); - } catch (error) { - console.error("Failed to generate draft:", error); - toast.error("Failed to generate summary. Please try again."); - } - }; - return ( -
+
{/* Session Header */} -
+
@@ -130,44 +67,25 @@ export default function ChatPage() {
- Teacher + Teacher + {phase === 'drafting' && Simulating...}
-
{/* Chat Messages - Scrollable Area */} - {/* Fix: Added min-h-0 and relative for proper nested scrolling */}
- +
- + {/* Chat Input - Fixed at Bottom */} -
- +
+
); diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..82f78b2 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +export async function POST(request: Request) { + try { + const { password } = await request.json(); + const appPassword = process.env.APP_PASSWORD; + + if (!appPassword) { + console.error('APP_PASSWORD is not set in environment variables'); + return NextResponse.json( + { error: 'Server configuration error' }, + { status: 500 } + ); + } + + if (password === appPassword) { + // Create a persistent session (30 days) + (await cookies()).set('auth-token', 'authenticated', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 30, // 30 days + path: '/', + }); + + return NextResponse.json({ success: true }); + } + + return NextResponse.json( + { error: 'Invalid password' }, + { status: 401 } + ); + } catch (error) { + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..2505d68 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +export async function POST() { + (await cookies()).delete('auth-token'); + return NextResponse.json({ success: true }); +} diff --git a/src/app/globals.css b/src/app/globals.css index 2e72a02..14c5b95 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -95,32 +95,52 @@ --chart-5: oklch(0.769 0.188 70.08); } -/* Dark Mode - Evening Mist */ +/* Dark Mode - Twilight Velvet */ .dark { - --background: oklch(0.15 0 0); - --foreground: oklch(0.98 0 0); - --card: oklch(0.20 0 0); - --card-foreground: oklch(0.98 0 0); - --popover: oklch(0.20 0 0); - --popover-foreground: oklch(0.98 0 0); - --primary: oklch(0.70 0.02 270); - --primary-foreground: oklch(0.15 0 0); - --secondary: oklch(0.25 0 0); - --secondary-foreground: oklch(0.98 0 0); - --muted: oklch(0.25 0 0); - --muted-foreground: oklch(0.70 0 0); - --accent: oklch(0.25 0 0); - --accent-foreground: oklch(0.98 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.985 0 0); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.55 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); + /* Background - Deep Space (Velvet Black) */ + --background: oklch(0.11 0.03 280); + /* Foreground - Stardust White */ + --foreground: oklch(0.94 0.02 280); + + /* Card - Velvet Shadow (#2A2A3D) */ + --card: oklch(0.22 0.03 280); + --card-foreground: oklch(0.94 0.02 280); + + /* Popover - Matching card */ + --popover: oklch(0.22 0.03 280); + --popover-foreground: oklch(0.94 0.02 280); + + /* Primary - Indigo Glow */ + --primary: oklch(0.75 0.08 270); + --primary-foreground: oklch(0.11 0.03 280); + + /* Secondary - Slightly lighter than card */ + --secondary: oklch(0.28 0.04 280); + --secondary-foreground: oklch(0.94 0.02 280); + + /* Muted - Matches card background for subtle integration */ + --muted: oklch(0.22 0.03 280); + --muted-foreground: oklch(0.70 0.04 280); + + /* Accent - Hover states */ + --accent: oklch(0.28 0.04 280); + --accent-foreground: oklch(0.94 0.02 280); + + /* Destructive - Muted Red */ + --destructive: oklch(0.55 0.15 25); + --destructive-foreground: oklch(0.94 0.02 280); + + /* Borders - Subtle purple border */ + --border: oklch(0.28 0.04 280); + --input: oklch(0.28 0.04 280); + --ring: oklch(0.75 0.08 270); + + /* Chart colors - Adapted for dark background */ + --chart-1: oklch(0.70 0.15 280); + --chart-2: oklch(0.65 0.15 320); + --chart-3: oklch(0.60 0.15 240); + --chart-4: oklch(0.75 0.15 200); + --chart-5: oklch(0.70 0.15 40); } @layer base { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 787de14..c54daaa 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter, Merriweather } from "next/font/google"; import "./globals.css"; import { OfflineIndicator } from "../components/features/common"; import { InstallPrompt } from "../components/features/pwa/install-prompt"; +import { ThemeProvider } from "@/components/theme-provider"; const inter = Inter({ variable: "--font-inter", @@ -44,13 +45,20 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} - - + + {children} + + + ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..d4ca134 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'; +import { Lock } from 'lucide-react'; + +export default function LoginPage() { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ password }), + }); + + if (response.ok) { + router.push('/'); + router.refresh(); // Refresh to update middleware state + } else { + const data = await response.json(); + setError(data.error || 'Invalid password'); + } + } catch (err) { + setError('An error occurred. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + +
+ +
+ Gatekeeper + + Enter the application password to continue + +
+
+ +
+ + setPassword(e.target.value)} + required + className="text-center tracking-widest" + /> +
+ {error && ( +
+ {error} +
+ )} +
+ + + +
+
+
+ ); +} diff --git a/src/components/features/chat/chat-input.tsx b/src/components/features/chat/chat-input.tsx index 88092df..f0f8f93 100644 --- a/src/components/features/chat/chat-input.tsx +++ b/src/components/features/chat/chat-input.tsx @@ -1,16 +1,13 @@ "use client"; -import { useState, useRef, useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; -import { Send, StopCircle } from 'lucide-react'; +import { Send, StopCircle, Sparkles } from 'lucide-react'; +import { useChatStore } from '@/store/use-chat'; -interface ChatInputProps { - onSend: (message: string) => void; - isLoading: boolean; -} - -export function ChatInput({ onSend, isLoading }: ChatInputProps) { +export function ChatInput() { + const { sendMessage, isTyping, phase, generateDraft } = useChatStore(); const [input, setInput] = useState(''); const textareaRef = useRef(null); @@ -22,10 +19,11 @@ export function ChatInput({ onSend, isLoading }: ChatInputProps) { } }, [input]); - const handleSend = () => { - if (!input.trim() || isLoading) return; - onSend(input); - setInput(''); + const handleSend = async () => { + if (!input.trim() || isTyping) return; + const msg = input; + setInput(''); // Clear immediately for UX + await sendMessage(msg); }; const handleKeyDown = (e: React.KeyboardEvent) => { @@ -36,26 +34,41 @@ export function ChatInput({ onSend, isLoading }: ChatInputProps) { }; return ( -
-
+
+