From 6f365adfd7db522a531c38d03c28dcf5e7546ab5 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 27 Jan 2026 15:00:52 +0700 Subject: [PATCH] refactor(ui): improve header alignment to use grid and fix runtime provider validation --- package-lock.json | 62 +---- package.json | 1 + src/app/(main)/settings/page.tsx | 227 +++++++++---------- src/app/(session)/chat/page.tsx | 61 +---- src/app/page.tsx | 17 +- src/components/features/chat/chat-window.tsx | 51 ++++- src/components/layout/app-header.tsx | 101 +++++++++ src/components/layout/index.ts | 1 + src/services/provider-management-service.ts | 18 +- 9 files changed, 298 insertions(+), 241 deletions(-) create mode 100644 src/components/layout/app-header.tsx create mode 100644 src/components/layout/index.ts 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 */} -
-
-
-

Appearance

-
-
-

- 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 */} +
+
+
+

Appearance

-
+
+

+ 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 */} +
+
+
+
+

Configuration

- -
- +
+ +
+

+ 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 */} +
+
+
+
+

Account Security

+
+

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

+ +
+
+ + {/* Edit Provider Dialog */} + !open && closeDialogs()} + > + + + Edit Provider + +
+ +
+
+
- - - - {/* Account Security Section */} -
-
-
-
-

Account Security

-
-

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

- -
-
- - {/* 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 */} -
-

- My Journal -

- -
+ {/* 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() {
-
); 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 ( +
+
+
+ {/* Spacer for balance */} +
+ + {/* Title - Center */} +

+ My Journal +

+ + {/* Navigation & Menu - Right */} +
+ + {/* Menu - Right (Desktop) */} + + + {/* Mobile Menu Button */} + +
+
+ + {/* Mobile Menu Dropdown */} + {mobileMenuOpen && ( + + )} +
+
+ ); +} 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); + } }