diff --git a/package-lock.json b/package-lock.json
index 0860e54..e3c60c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@ai-sdk/openai": "^3.0.14",
"@ai-sdk/openai-compatible": "^2.0.18",
+ "@fontsource/merriweather": "^5.2.11",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -1366,6 +1367,15 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
+ "node_modules/@fontsource/merriweather": {
+ "version": "5.2.11",
+ "resolved": "https://registry.npmjs.org/@fontsource/merriweather/-/merriweather-5.2.11.tgz",
+ "integrity": "sha512-ZiIMeUh5iT8d73o6xlSF8GKgjV5pgiFrufYc5jZTVAfExtWKqM2vQHnsqXSFMv4ELhAcjt6Vf+5T3oVGXhAizQ==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1916,18 +1926,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.11",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
- "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -4600,7 +4598,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -4984,14 +4981,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -5206,14 +5195,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -10489,17 +10470,6 @@
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
- "node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "optional": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -10509,18 +10479,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
diff --git a/package.json b/package.json
index edc2abf..d70ea18 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@ai-sdk/openai": "^3.0.14",
"@ai-sdk/openai-compatible": "^2.0.18",
+ "@fontsource/merriweather": "^5.2.11",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
diff --git a/src/app/(main)/settings/page.tsx b/src/app/(main)/settings/page.tsx
index 4b23a4f..befd982 100644
--- a/src/app/(main)/settings/page.tsx
+++ b/src/app/(main)/settings/page.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
-import { Plus, ArrowLeft } from "lucide-react";
+import { Plus } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
@@ -22,6 +22,7 @@ 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";
+import { AppHeader } from "@/components/layout";
export default function SettingsPage() {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
@@ -51,137 +52,131 @@ export default function SettingsPage() {
const editingProvider = providers.find((p) => p.id === editingProviderId);
return (
-
-
+
+ {/* Header */}
+
- {/* Header */}
-
-
-
- Back to Home
-
+ {/* Main Content */}
+
+
+
+ {/* Page Title */}
Settings
Manage your AI provider connections and preferences.
-
-
- {/* General Settings */}
-
-
-
-
- Choose your preferred theme for the journaling experience.
-
-
-
-
-
- {/* Active Provider Section */}
-
-
-
-
Active Session Provider
-
-
- Select which AI provider handles your current venting session. This setting applies immediately to new messages.
-
-
-
-
- {/* Manage Providers Section */}
-
-
-
-
-
Configuration
+
+ {/* General Settings */}
+
+
+
+ Choose your preferred theme for the journaling experience.
+
+
+
+
-
+ {/* Active Provider Section */}
+
+
+
+
Active Session Provider
+
- Configure connection details for your AI models. Keys are stored locally in your browser.
+ Select which AI provider handles your current venting session. This setting applies immediately to new messages.
- setIsAddDialogOpen(true)}
- />
-
+
+
- {/* Add Provider Dialog (Triggered by ProviderList) */}
-
-
-
- Add New Provider
-
-
-
{
- closeDialogs();
- }}
- onCancel={closeDialogs}
- />
+ {/* Manage Providers Section */}
+
+
+
+
+
+ Configure connection details for your AI models. Keys are stored locally in your browser.
+
+
setIsAddDialogOpen(true)}
+ />
+
+
+ {/* Add Provider Dialog (Triggered by ProviderList) */}
+
+
+
+ Add New Provider
+
+
+
{
+ closeDialogs();
+ }}
+ onCancel={closeDialogs}
+ />
+
+
+
+
+
+
+ {/* Account Security Section */}
+
+
+
+
+ Lock the application to prevent unauthorized access on this device.
+
+ {
+ if (confirm('Are you sure you want to logout?')) {
+ await fetch('/api/auth/logout', { method: 'POST' });
+ window.location.href = '/login';
+ }
+ }}>
+ Logout
+
+
+
+
+ {/* Edit Provider Dialog */}
+ !open && closeDialogs()}
+ >
+
+
+ Edit Provider
+
+
+
+
-
-
-
- {/* Account Security Section */}
-
-
-
-
- Lock the application to prevent unauthorized access on this device.
-
- {
- if (confirm('Are you sure you want to logout?')) {
- await fetch('/api/auth/logout', { method: 'POST' });
- window.location.href = '/login';
- }
- }}>
- Logout
-
-
-
-
- {/* Edit Provider Dialog */}
-
!open && closeDialogs()}
- >
-
-
- Edit Provider
-
-
-
-
-
);
}
diff --git a/src/app/(session)/chat/page.tsx b/src/app/(session)/chat/page.tsx
index 341ff1a..5e4f53b 100644
--- a/src/app/(session)/chat/page.tsx
+++ b/src/app/(session)/chat/page.tsx
@@ -1,24 +1,19 @@
"use client";
-import { useEffect, useState, Suspense } from 'react';
+import { useEffect, Suspense } 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 { 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';
+import { Loader2 } from "lucide-react";
+import { AppHeader } from '@/components/layout';
function ChatPageContent() {
- const { resetSession, phase } = useChatStore();
+ const { resetSession } = useChatStore();
const searchParams = useSearchParams();
const router = useRouter();
- // Connection Status State
- const [connectionStatus, setConnectionStatus] = useState<'checking' | 'connected' | 'error'>('checking');
-
// Check for "new" param to force fresh session
useEffect(() => {
if (searchParams.get('new') === 'true') {
@@ -28,53 +23,10 @@ function ChatPageContent() {
}
}, [searchParams, router, resetSession]);
- // Check Connection Status
- useEffect(() => {
- const checkConnection = async () => {
- setConnectionStatus('checking');
- const settings = ProviderManagementService.getActiveProviderSettings();
-
- if (!settings.apiKey) {
- setConnectionStatus('error');
- return;
- }
-
- const result = await LLMService.validateConnection(
- settings.baseUrl,
- settings.apiKey,
- settings.modelName
- );
-
- if (result.isValid) {
- setConnectionStatus('connected');
- } else {
- setConnectionStatus('error');
- }
- };
-
- checkConnection();
- }, []);
-
return (
- {/* Session Header */}
-
-
-
-
-
-
-
-
Teacher
- {phase === 'drafting' &&
Simulating... }
-
-
-
+ {/* Header */}
+
{/* Chat Messages - Scrollable Area */}
@@ -102,4 +54,3 @@ export default function ChatPage() {
);
}
-
diff --git a/src/app/page.tsx b/src/app/page.tsx
index f941fbd..2bea820 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -6,7 +6,6 @@
* Story 3.1: History Feed UI integration
*
* The home screen IS the history feed. Shows:
- * - Header with "My Journal" title
* - History feed with lazy loading
* - FAB (Floating Action Button) to start new vent
* - HistoryDetailSheet for viewing past entries
@@ -19,25 +18,15 @@
*/
import { HistoryFeed, HistoryDetailSheet } from '@/components/features/journal';
-import { Plus, Settings } from 'lucide-react';
+import { Plus } from 'lucide-react';
import Link from 'next/link';
+import { AppHeader } from '@/components/layout';
export default function HomePage() {
return (
{/* Header */}
-
+
{/* Main Content - History Feed */}
diff --git a/src/components/features/chat/chat-window.tsx b/src/components/features/chat/chat-window.tsx
index e51977a..f808e56 100644
--- a/src/components/features/chat/chat-window.tsx
+++ b/src/components/features/chat/chat-window.tsx
@@ -4,13 +4,42 @@ import { useEffect, useRef, useState } from 'react';
import { ChatBubble } from './chat-bubble';
import { TypingIndicator } from './typing-indicator';
import { useChatStore } from '@/store/use-chat';
-import { BookOpen, Sparkles } from 'lucide-react';
+import { Bot, Sparkles } from 'lucide-react';
+import { ProviderManagementService } from '@/services/provider-management-service';
export function ChatWindow() {
const { messages, isTyping } = useChatStore();
const bottomRef = useRef(null);
const containerRef = useRef(null);
const [isUserScrolling, setIsUserScrolling] = useState(false);
+ const [connectionStatus, setConnectionStatus] = useState<'checking' | 'connected' | 'error'>('checking');
+
+ // Check connection status
+ useEffect(() => {
+ const checkConnection = async () => {
+ setConnectionStatus('checking');
+ const settings = ProviderManagementService.getActiveProviderSettings();
+
+ if (!settings.apiKey) {
+ setConnectionStatus('error');
+ return;
+ }
+
+ const result = await ProviderManagementService.validateConnection(
+ settings.baseUrl,
+ settings.apiKey,
+ settings.modelName
+ );
+
+ if (result.isValid) {
+ setConnectionStatus('connected');
+ } else {
+ setConnectionStatus('error');
+ }
+ };
+
+ checkConnection();
+ }, []);
// Auto-scroll to bottom only when new messages arrive
useEffect(() => {
@@ -48,18 +77,34 @@ export function ChatWindow() {
-
+
+
+ {/* Connection Status Indicator */}
+
- What's on your mind?
+ Teacher
I'm here to listen. Let it all out.
+ {connectionStatus === 'connected' && (
+
+ Connected and ready
+
+ )}
+ {connectionStatus === 'error' && (
+
+ Please configure your AI provider in Settings
+
+ )}
);
diff --git a/src/components/layout/app-header.tsx b/src/components/layout/app-header.tsx
new file mode 100644
index 0000000..3de89e5
--- /dev/null
+++ b/src/components/layout/app-header.tsx
@@ -0,0 +1,101 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import Link from 'next/link';
+import { Menu, X, BookOpen, Bot, Settings } from 'lucide-react';
+import { useState } from 'react';
+
+interface NavItem {
+ href: string;
+ label: string;
+ icon: typeof BookOpen;
+}
+
+const navItems: NavItem[] = [
+ { href: '/', label: 'History', icon: BookOpen },
+ { href: '/chat', label: 'Teacher', icon: Bot },
+ { href: '/settings', label: 'Settings', icon: Settings },
+];
+
+export function AppHeader() {
+ const pathname = usePathname();
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+
+ return (
+
+ );
+}
diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts
new file mode 100644
index 0000000..1126d28
--- /dev/null
+++ b/src/components/layout/index.ts
@@ -0,0 +1 @@
+export { AppHeader } from './app-header';
diff --git a/src/services/provider-management-service.ts b/src/services/provider-management-service.ts
index f50fb6d..ed44b2b 100644
--- a/src/services/provider-management-service.ts
+++ b/src/services/provider-management-service.ts
@@ -1,5 +1,5 @@
import { useSettingsStore } from '@/store/use-settings';
-import type { ProviderProfile, ProviderSettings } from '@/types/settings';
+import type { ProviderProfile, ProviderSettings, ConnectionValidationResult } from '@/types/settings';
/**
* Provider Management Service - Business logic for multi-provider management
@@ -109,4 +109,20 @@ export class ProviderManagementService {
static hasAnyProvider(): boolean {
return useSettingsStore.getState().savedProviders.length > 0;
}
+
+ /**
+ * Validate connection to LLM provider
+ * @param baseUrl - The API base URL
+ * @param apiKey - The API key for authentication
+ * @param modelName - The model name to test
+ * @returns Promise resolving to ConnectionValidationResult
+ */
+ static async validateConnection(
+ baseUrl: string,
+ apiKey: string,
+ modelName: string
+ ): Promise {
+ const { LLMService } = await import('./llm-service');
+ return LLMService.validateConnection(baseUrl, apiKey, modelName);
+ }
}