refactor(ui): improve header alignment to use grid and fix runtime provider validation
This commit is contained in:
62
package-lock.json
generated
62
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^3.0.14",
|
"@ai-sdk/openai": "^3.0.14",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.18",
|
"@ai-sdk/openai-compatible": "^2.0.18",
|
||||||
|
"@fontsource/merriweather": "^5.2.11",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -1366,6 +1367,15 @@
|
|||||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -1916,18 +1926,6 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
@@ -4600,7 +4598,6 @@
|
|||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -4984,14 +4981,6 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"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": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
@@ -5206,14 +5195,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"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"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -10509,18 +10479,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/space-separated-tokens": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^3.0.14",
|
"@ai-sdk/openai": "^3.0.14",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.18",
|
"@ai-sdk/openai-compatible": "^2.0.18",
|
||||||
|
"@fontsource/merriweather": "^5.2.11",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Plus, ArrowLeft } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +22,7 @@ import { useSavedProviders } from "@/store/use-settings";
|
|||||||
import { ProviderManagementService } from "@/services/provider-management-service";
|
import { ProviderManagementService } from "@/services/provider-management-service";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
import { ThemeToggle } from "@/components/features/settings/theme-toggle";
|
import { ThemeToggle } from "@/components/features/settings/theme-toggle";
|
||||||
|
import { AppHeader } from "@/components/layout";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
@@ -51,137 +52,131 @@ export default function SettingsPage() {
|
|||||||
const editingProvider = providers.find((p) => p.id === editingProviderId);
|
const editingProvider = providers.find((p) => p.id === editingProviderId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background py-8 sm:py-12 px-4 sm:px-6 lg:px-8 w-full">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="max-w-2xl mx-auto space-y-8 sm:space-y-10">
|
{/* Header */}
|
||||||
|
<AppHeader />
|
||||||
|
|
||||||
{/* Header */}
|
{/* Main Content */}
|
||||||
<div className="space-y-4">
|
<div className="py-8 sm:py-12 px-4 sm:px-6 lg:px-8 w-full">
|
||||||
<Link
|
<div className="max-w-2xl mx-auto space-y-8 sm:space-y-10">
|
||||||
href="/"
|
|
||||||
className="inline-flex items-center text-sm font-medium text-muted-foreground hover:text-primary transition-colors mb-2"
|
{/* Page Title */}
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
Back to Home
|
|
||||||
</Link>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-foreground font-serif">Settings</h1>
|
<h1 className="text-4xl font-bold tracking-tight text-foreground font-serif">Settings</h1>
|
||||||
<p className="mt-2 text-lg text-muted-foreground">
|
<p className="mt-2 text-lg text-muted-foreground">
|
||||||
Manage your AI provider connections and preferences.
|
Manage your AI provider connections and preferences.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-8">
|
<div className="grid gap-8">
|
||||||
{/* General Settings */}
|
{/* General Settings */}
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex items-center gap-2 pb-2 border-b border-border">
|
<div className="flex items-center gap-2 pb-2 border-b border-border">
|
||||||
<div className="h-8 w-1 bg-yellow-400 rounded-full"></div>
|
<div className="h-8 w-1 bg-yellow-400 rounded-full"></div>
|
||||||
<h2 className="text-xl font-semibold text-foreground font-serif">Appearance</h2>
|
<h2 className="text-xl font-semibold text-foreground font-serif">Appearance</h2>
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-sm text-muted-foreground max-w-xl">
|
|
||||||
Choose your preferred theme for the journaling experience.
|
|
||||||
</p>
|
|
||||||
<ThemeToggle />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Active Provider Section */}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<div className="flex items-center gap-2 pb-2 border-b border-border">
|
|
||||||
<div className="h-8 w-1 bg-primary rounded-full"></div>
|
|
||||||
<h2 className="text-xl font-semibold text-foreground font-serif">Active Session Provider</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground max-w-xl">
|
|
||||||
Select which AI provider handles your current venting session. This setting applies immediately to new messages.
|
|
||||||
</p>
|
|
||||||
<ProviderSelector />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Manage Providers Section */}
|
|
||||||
<section className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between pb-2 border-b border-border">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-8 w-1 bg-slate-300 dark:bg-slate-700 rounded-full"></div>
|
|
||||||
<h2 className="text-xl font-semibold text-foreground font-serif">Configuration</h2>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm text-muted-foreground max-w-xl">
|
||||||
|
Choose your preferred theme for the journaling experience.
|
||||||
|
</p>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* Active Provider Section */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 pb-2 border-b border-border">
|
||||||
|
<div className="h-8 w-1 bg-primary rounded-full"></div>
|
||||||
|
<h2 className="text-xl font-semibold text-foreground font-serif">Active Session Provider</h2>
|
||||||
|
</div>
|
||||||
<p className="text-sm text-muted-foreground max-w-xl">
|
<p className="text-sm text-muted-foreground max-w-xl">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<ProviderList
|
<ProviderSelector />
|
||||||
onEditProvider={handleEditProvider}
|
</section>
|
||||||
onDeleteProvider={handleDeleteProvider}
|
|
||||||
onAddProvider={() => setIsAddDialogOpen(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add Provider Dialog (Triggered by ProviderList) */}
|
{/* Manage Providers Section */}
|
||||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
<section className="space-y-6">
|
||||||
<DialogContent className="sm:max-w-[550px] p-0 overflow-hidden bg-background border-border shadow-2xl">
|
<div className="flex items-center justify-between pb-2 border-b border-border">
|
||||||
<DialogHeader className="p-6 pb-2 bg-muted/50">
|
<div className="flex items-center gap-2">
|
||||||
<DialogTitle className="text-2xl font-serif text-foreground">Add New Provider</DialogTitle>
|
<div className="h-8 w-1 bg-slate-300 dark:bg-slate-700 rounded-full"></div>
|
||||||
</DialogHeader>
|
<h2 className="text-xl font-semibold text-foreground font-serif">Configuration</h2>
|
||||||
<div className="p-6 pt-2">
|
|
||||||
<ProviderForm
|
|
||||||
mode="add"
|
|
||||||
onSave={() => {
|
|
||||||
closeDialogs();
|
|
||||||
}}
|
|
||||||
onCancel={closeDialogs}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</div>
|
||||||
</Dialog>
|
|
||||||
</section>
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground max-w-xl">
|
||||||
|
Configure connection details for your AI models. Keys are stored locally in your browser.
|
||||||
|
</p>
|
||||||
|
<ProviderList
|
||||||
|
onEditProvider={handleEditProvider}
|
||||||
|
onDeleteProvider={handleDeleteProvider}
|
||||||
|
onAddProvider={() => setIsAddDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Provider Dialog (Triggered by ProviderList) */}
|
||||||
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[550px] p-0 overflow-hidden bg-background border-border shadow-2xl">
|
||||||
|
<DialogHeader className="p-6 pb-2 bg-muted/50">
|
||||||
|
<DialogTitle className="text-2xl font-serif text-foreground">Add New Provider</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="p-6 pt-2">
|
||||||
|
<ProviderForm
|
||||||
|
mode="add"
|
||||||
|
onSave={() => {
|
||||||
|
closeDialogs();
|
||||||
|
}}
|
||||||
|
onCancel={closeDialogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Account Security Section */}
|
||||||
|
<div className="grid gap-8 mt-10 border-t border-border pt-10">
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 pb-2 border-b border-border">
|
||||||
|
<div className="h-8 w-1 bg-red-400 rounded-full"></div>
|
||||||
|
<h2 className="text-xl font-semibold text-foreground font-serif">Account Security</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-xl">
|
||||||
|
Lock the application to prevent unauthorized access on this device.
|
||||||
|
</p>
|
||||||
|
<Button variant="destructive" onClick={async () => {
|
||||||
|
if (confirm('Are you sure you want to logout?')) {
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edit Provider Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={!!editingProviderId}
|
||||||
|
onOpenChange={(open: boolean) => !open && closeDialogs()}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-[550px] p-0 overflow-hidden bg-background border-border shadow-2xl">
|
||||||
|
<DialogHeader className="p-6 pb-2 bg-muted/50">
|
||||||
|
<DialogTitle className="text-2xl font-serif text-foreground">Edit Provider</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="p-6 pt-2">
|
||||||
|
<ProviderForm
|
||||||
|
mode="edit"
|
||||||
|
provider={editingProvider}
|
||||||
|
onSave={closeDialogs}
|
||||||
|
onCancel={closeDialogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Account Security Section */}
|
|
||||||
<div className="grid gap-8 mt-10 border-t border-border pt-10">
|
|
||||||
<section className="space-y-4">
|
|
||||||
<div className="flex items-center gap-2 pb-2 border-b border-border">
|
|
||||||
<div className="h-8 w-1 bg-red-400 rounded-full"></div>
|
|
||||||
<h2 className="text-xl font-semibold text-foreground font-serif">Account Security</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground max-w-xl">
|
|
||||||
Lock the application to prevent unauthorized access on this device.
|
|
||||||
</p>
|
|
||||||
<Button variant="destructive" onClick={async () => {
|
|
||||||
if (confirm('Are you sure you want to logout?')) {
|
|
||||||
await fetch('/api/auth/logout', { method: 'POST' });
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Edit Provider Dialog */}
|
|
||||||
<Dialog
|
|
||||||
open={!!editingProviderId}
|
|
||||||
onOpenChange={(open: boolean) => !open && closeDialogs()}
|
|
||||||
>
|
|
||||||
<DialogContent className="sm:max-w-[550px] p-0 overflow-hidden bg-background border-border shadow-2xl">
|
|
||||||
<DialogHeader className="p-6 pb-2 bg-muted/50">
|
|
||||||
<DialogTitle className="text-2xl font-serif text-foreground">Edit Provider</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="p-6 pt-2">
|
|
||||||
<ProviderForm
|
|
||||||
mode="edit"
|
|
||||||
provider={editingProvider}
|
|
||||||
onSave={closeDialogs}
|
|
||||||
onCancel={closeDialogs}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, Suspense } from 'react';
|
import { useEffect, Suspense } from 'react';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import { ChatWindow } from '@/components/features/chat/chat-window';
|
import { ChatWindow } from '@/components/features/chat/chat-window';
|
||||||
import { ChatInput } from '@/components/features/chat/chat-input';
|
import { ChatInput } from '@/components/features/chat/chat-input';
|
||||||
import { DraftSheet } from '@/components/features/journal/draft-sheet';
|
import { DraftSheet } from '@/components/features/journal/draft-sheet';
|
||||||
import { useChatStore } from '@/store/use-chat';
|
import { useChatStore } from '@/store/use-chat';
|
||||||
import { ArrowLeft, Bot, Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import { AppHeader } from '@/components/layout';
|
||||||
import { LLMService } from '@/services/llm-service';
|
|
||||||
import { ProviderManagementService } from '@/services/provider-management-service';
|
|
||||||
|
|
||||||
function ChatPageContent() {
|
function ChatPageContent() {
|
||||||
const { resetSession, phase } = useChatStore();
|
const { resetSession } = useChatStore();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Connection Status State
|
|
||||||
const [connectionStatus, setConnectionStatus] = useState<'checking' | 'connected' | 'error'>('checking');
|
|
||||||
|
|
||||||
// Check for "new" param to force fresh session
|
// Check for "new" param to force fresh session
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get('new') === 'true') {
|
if (searchParams.get('new') === 'true') {
|
||||||
@@ -28,53 +23,10 @@ function ChatPageContent() {
|
|||||||
}
|
}
|
||||||
}, [searchParams, router, resetSession]);
|
}, [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 (
|
return (
|
||||||
<div className="flex flex-col h-dvh bg-background relative">
|
<div className="flex flex-col h-dvh bg-background relative">
|
||||||
{/* Session Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 bg-white/80 backdrop-blur border-b border-slate-200 shrink-0 z-10 sticky top-0">
|
<AppHeader />
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Link href="/" className="text-slate-500 hover:text-slate-700 transition-colors">
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-2 font-medium text-slate-700">
|
|
||||||
<div className="relative">
|
|
||||||
<Bot className="w-5 h-5 text-indigo-600" />
|
|
||||||
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${connectionStatus === 'connected' ? 'bg-green-500' :
|
|
||||||
connectionStatus === 'checking' ? 'bg-yellow-400' : 'bg-red-500'
|
|
||||||
}`} />
|
|
||||||
</div>
|
|
||||||
<span className="font-serif">Teacher</span>
|
|
||||||
{phase === 'drafting' && <span className="text-xs text-indigo-500 animate-pulse ml-2">Simulating...</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chat Messages - Scrollable Area */}
|
{/* Chat Messages - Scrollable Area */}
|
||||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden relative">
|
<div className="flex-1 flex flex-col min-h-0 overflow-hidden relative">
|
||||||
@@ -102,4 +54,3 @@ export default function ChatPage() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
* Story 3.1: History Feed UI integration
|
* Story 3.1: History Feed UI integration
|
||||||
*
|
*
|
||||||
* The home screen IS the history feed. Shows:
|
* The home screen IS the history feed. Shows:
|
||||||
* - Header with "My Journal" title
|
|
||||||
* - History feed with lazy loading
|
* - History feed with lazy loading
|
||||||
* - FAB (Floating Action Button) to start new vent
|
* - FAB (Floating Action Button) to start new vent
|
||||||
* - HistoryDetailSheet for viewing past entries
|
* - HistoryDetailSheet for viewing past entries
|
||||||
@@ -19,25 +18,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { HistoryFeed, HistoryDetailSheet } from '@/components/features/journal';
|
import { HistoryFeed, HistoryDetailSheet } from '@/components/features/journal';
|
||||||
import { Plus, Settings } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { AppHeader } from '@/components/layout';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="sticky top-0 z-10 bg-white border-b border-slate-200 px-4 py-4 flex items-center justify-between">
|
<AppHeader />
|
||||||
<h1 className="text-2xl font-serif font-bold text-slate-800">
|
|
||||||
My Journal
|
|
||||||
</h1>
|
|
||||||
<Link
|
|
||||||
href="/settings"
|
|
||||||
className="p-2 text-slate-600 hover:text-slate-800 hover:bg-slate-100 rounded-lg transition-colors"
|
|
||||||
aria-label="Settings"
|
|
||||||
>
|
|
||||||
<Settings className="w-5 h-5" aria-hidden="true" />
|
|
||||||
</Link>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content - History Feed */}
|
{/* Main Content - History Feed */}
|
||||||
<main className="pb-24">
|
<main className="pb-24">
|
||||||
|
|||||||
@@ -4,13 +4,42 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import { ChatBubble } from './chat-bubble';
|
import { ChatBubble } from './chat-bubble';
|
||||||
import { TypingIndicator } from './typing-indicator';
|
import { TypingIndicator } from './typing-indicator';
|
||||||
import { useChatStore } from '@/store/use-chat';
|
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() {
|
export function ChatWindow() {
|
||||||
const { messages, isTyping } = useChatStore();
|
const { messages, isTyping } = useChatStore();
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
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
|
// Auto-scroll to bottom only when new messages arrive
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -48,18 +77,34 @@ export function ChatWindow() {
|
|||||||
<div className="flex-1 flex flex-col items-center justify-center text-center p-8 space-y-6">
|
<div className="flex-1 flex flex-col items-center justify-center text-center p-8 space-y-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-32 h-32 bg-gradient-to-br from-secondary to-muted rounded-full flex items-center justify-center">
|
<div className="w-32 h-32 bg-gradient-to-br from-secondary to-muted rounded-full flex items-center justify-center">
|
||||||
<BookOpen className="w-16 h-16 text-muted-foreground/50" aria-hidden="true" />
|
<Bot className="w-16 h-16 text-muted-foreground/50" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<Sparkles className="w-8 h-8 text-amber-400 absolute -top-2 -right-2" aria-hidden="true" />
|
<Sparkles className="w-8 h-8 text-amber-400 absolute -top-2 -right-2" aria-hidden="true" />
|
||||||
|
|
||||||
|
{/* Connection Status Indicator */}
|
||||||
|
<div className={`absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-white ${
|
||||||
|
connectionStatus === 'connected' ? 'bg-green-500' :
|
||||||
|
connectionStatus === 'checking' ? 'bg-yellow-400 animate-pulse' : 'bg-red-500'
|
||||||
|
}`} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 max-w-md">
|
<div className="space-y-2 max-w-md">
|
||||||
<h2 className="text-2xl font-bold font-serif text-foreground">
|
<h2 className="text-2xl font-bold font-serif text-foreground">
|
||||||
What's on your mind?
|
Teacher
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground font-sans">
|
<p className="text-muted-foreground font-sans">
|
||||||
I'm here to listen. Let it all out.
|
I'm here to listen. Let it all out.
|
||||||
</p>
|
</p>
|
||||||
|
{connectionStatus === 'connected' && (
|
||||||
|
<p className="text-sm text-green-600 dark:text-green-400">
|
||||||
|
Connected and ready
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{connectionStatus === 'error' && (
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
Please configure your AI provider in Settings
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
101
src/components/layout/app-header.tsx
Normal file
101
src/components/layout/app-header.tsx
Normal file
@@ -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 (
|
||||||
|
<header className="sticky top-0 z-50 bg-white/80 dark:bg-zinc-950/80 backdrop-blur border-b border-slate-200 dark:border-zinc-800">
|
||||||
|
<div className="px-4 py-3">
|
||||||
|
<div className="grid grid-cols-[1fr_auto_1fr] items-center">
|
||||||
|
{/* Spacer for balance */}
|
||||||
|
<div />
|
||||||
|
|
||||||
|
{/* Title - Center */}
|
||||||
|
<h1 className="text-xl font-serif font-bold text-slate-800 dark:text-zinc-100 h-9 flex items-center justify-center">
|
||||||
|
My Journal
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Navigation & Menu - Right */}
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
|
||||||
|
{/* Menu - Right (Desktop) */}
|
||||||
|
<nav className="hidden sm:flex items-center gap-1" aria-label="Main navigation">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = pathname === item.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className={`p-2 rounded-lg transition-colors ${isActive
|
||||||
|
? 'bg-slate-100 dark:bg-zinc-800 text-slate-800 dark:text-zinc-100'
|
||||||
|
: 'text-slate-600 dark:text-zinc-400 hover:text-slate-800 dark:hover:text-zinc-100 hover:bg-slate-100 dark:hover:bg-zinc-800'
|
||||||
|
}`}
|
||||||
|
aria-label={item.label}
|
||||||
|
>
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sm:hidden p-2 text-slate-600 dark:text-zinc-400 hover:text-slate-800 dark:hover:text-zinc-100 hover:bg-slate-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
aria-expanded={mobileMenuOpen}
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Dropdown */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<nav className="sm:hidden mt-3 pt-3 border-t border-slate-200 dark:border-zinc-800" aria-label="Mobile navigation">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = pathname === item.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${isActive
|
||||||
|
? 'bg-slate-100 dark:bg-zinc-800 text-slate-800 dark:text-zinc-100'
|
||||||
|
: 'text-slate-600 dark:text-zinc-400 hover:text-slate-800 dark:hover:text-zinc-100 hover:bg-slate-100 dark:hover:bg-zinc-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
<span className="font-medium">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/layout/index.ts
Normal file
1
src/components/layout/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { AppHeader } from './app-header';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSettingsStore } from '@/store/use-settings';
|
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
|
* Provider Management Service - Business logic for multi-provider management
|
||||||
@@ -109,4 +109,20 @@ export class ProviderManagementService {
|
|||||||
static hasAnyProvider(): boolean {
|
static hasAnyProvider(): boolean {
|
||||||
return useSettingsStore.getState().savedProviders.length > 0;
|
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<ConnectionValidationResult> {
|
||||||
|
const { LLMService } = await import('./llm-service');
|
||||||
|
return LLMService.validateConnection(baseUrl, apiKey, modelName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user