Compare commits
8 Commits
f405f298aa
...
b400781b89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b400781b89 | ||
|
|
f81a1f0f2d | ||
|
|
6d1bf7275b | ||
|
|
5444e7a579 | ||
|
|
29b2d99a82 | ||
|
|
2de8cd64e3 | ||
|
|
9a27a11385 | ||
|
|
b869a443e2 |
@ -21,6 +21,7 @@ export async function GET() {
|
||||
temperature: '0.7',
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
fontSize: 15,
|
||||
enableThinking: false,
|
||||
saveChatHistory: true,
|
||||
});
|
||||
@ -36,6 +37,7 @@ export async function GET() {
|
||||
temperature: settings.temperature || '0.7',
|
||||
theme: settings.theme,
|
||||
language: settings.language,
|
||||
fontSize: settings.fontSize || 15,
|
||||
enableThinking: settings.enableThinking,
|
||||
saveChatHistory: settings.saveChatHistory,
|
||||
});
|
||||
@ -61,6 +63,7 @@ export async function PUT(request: Request) {
|
||||
temperature,
|
||||
theme,
|
||||
language,
|
||||
fontSize,
|
||||
enableThinking,
|
||||
saveChatHistory,
|
||||
} = body;
|
||||
@ -110,6 +113,11 @@ export async function PUT(request: Request) {
|
||||
updateData.language = language;
|
||||
}
|
||||
|
||||
if (fontSize !== undefined) {
|
||||
// 限制字体大小在 12-20 之间
|
||||
updateData.fontSize = Math.min(20, Math.max(12, fontSize));
|
||||
}
|
||||
|
||||
if (enableThinking !== undefined) {
|
||||
updateData.enableThinking = enableThinking;
|
||||
}
|
||||
@ -151,6 +159,7 @@ export async function PUT(request: Request) {
|
||||
temperature: updatedSettings?.temperature || '0.7',
|
||||
theme: updatedSettings?.theme,
|
||||
language: updatedSettings?.language,
|
||||
fontSize: updatedSettings?.fontSize || 15,
|
||||
enableThinking: updatedSettings?.enableThinking,
|
||||
saveChatHistory: updatedSettings?.saveChatHistory,
|
||||
});
|
||||
|
||||
@ -59,6 +59,9 @@
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-base: 0.2s ease;
|
||||
--transition-slow: 0.3s ease;
|
||||
|
||||
/* 字体大小 */
|
||||
--font-size-base: 15px;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@ -92,6 +95,7 @@ body {
|
||||
background-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: var(--font-size-base);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@ -151,6 +155,17 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn var(--transition-base);
|
||||
}
|
||||
@ -163,6 +178,10 @@ body {
|
||||
animation: slideDown var(--transition-fast);
|
||||
}
|
||||
|
||||
.animate-pop-up {
|
||||
animation: popUp 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
响应式设计
|
||||
======================================== */
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { SettingsProvider } from "@/components/providers/SettingsProvider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "cchcode - AI 智能助手",
|
||||
@ -17,7 +18,9 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className="antialiased">
|
||||
<SettingsProvider>
|
||||
{children}
|
||||
</SettingsProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, Download, Check, Loader2, Eye, EyeOff, RotateCcw } from 'lucide-react';
|
||||
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
||||
import { ArrowLeft, Download, Check, Loader2, Eye, EyeOff, RotateCcw, Moon, Sun, Sparkles, Trash2 } from 'lucide-react';
|
||||
import { Toggle } from '@/components/ui/Toggle';
|
||||
import { ModelCardSelector } from '@/components/ui/ModelCardSelector';
|
||||
import { FontSizePicker } from '@/components/ui/FontSizePicker';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { currentUser, chatHistories } from '@/data/mock';
|
||||
import { useSettings, useModels, useTools } from '@/hooks/useSettings';
|
||||
|
||||
// 默认系统提示词
|
||||
@ -44,7 +44,6 @@ const DEFAULT_SYSTEM_PROMPT = `你是一个专业、友好的 AI 助手。请遵
|
||||
- 承认不确定的地方,不要编造信息`;
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const { settings, loading, saving, updateSettings } = useSettings();
|
||||
const { models, loading: modelsLoading } = useModels();
|
||||
const { tools, loading: toolsLoading } = useTools();
|
||||
@ -105,6 +104,17 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 更新字体大小
|
||||
const handleFontSizeChange = async (fontSize: number) => {
|
||||
try {
|
||||
await updateSettings({ fontSize });
|
||||
// 实时应用到页面
|
||||
document.documentElement.style.setProperty('--font-size-base', `${fontSize}px`);
|
||||
} catch (error) {
|
||||
console.error('Failed to update font size:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换工具
|
||||
const handleToolToggle = async (toolId: string) => {
|
||||
const currentTools = settings?.defaultTools || [];
|
||||
@ -177,6 +187,17 @@ export default function SettingsPage() {
|
||||
setTemperature(value);
|
||||
};
|
||||
|
||||
// 切换主题
|
||||
const handleToggleTheme = async () => {
|
||||
const newTheme = settings?.theme === 'dark' ? 'light' : 'dark';
|
||||
try {
|
||||
await updateSettings({ theme: newTheme });
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
} catch (error) {
|
||||
console.error('Failed to update theme:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
@ -185,50 +206,41 @@ export default function SettingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const isDarkMode = settings?.theme === 'dark';
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* 侧边栏 */}
|
||||
<Sidebar
|
||||
user={currentUser}
|
||||
chatHistories={chatHistories}
|
||||
isOpen={sidebarOpen}
|
||||
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||
/>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<main
|
||||
className={cn(
|
||||
'flex-1 min-h-screen transition-all duration-300',
|
||||
sidebarOpen ? 'ml-[var(--sidebar-width)]' : 'ml-0'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<header className="h-[var(--header-height)] px-4 flex items-center">
|
||||
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
|
||||
</header>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-8 pb-8">
|
||||
<div className="max-w-[800px] mx-auto">
|
||||
{/* 返回链接 */}
|
||||
<div className="min-h-screen bg-[var(--color-bg-secondary)]">
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="sticky top-0 z-10 bg-[var(--color-bg-primary)] border-b border-[var(--color-border-light)]">
|
||||
<div className="max-w-[900px] mx-auto px-6 h-16 flex items-center justify-between">
|
||||
{/* 左侧:返回按钮和标题 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-[var(--color-text-secondary)] text-sm mb-4 hover:text-[var(--color-text-primary)] transition-colors"
|
||||
className="p-2 -ml-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] transition-colors"
|
||||
title="返回聊天"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
<span>返回聊天</span>
|
||||
<ArrowLeft size={20} />
|
||||
</Link>
|
||||
|
||||
{/* 页面标题 */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-semibold text-[var(--color-text-primary)] mb-2">
|
||||
设置
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
管理您的账户设置和偏好
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles size={24} className="text-[var(--color-primary)]" />
|
||||
<h1 className="text-xl font-semibold text-[var(--color-text-primary)]">Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:主题切换 */}
|
||||
<button
|
||||
onClick={handleToggleTheme}
|
||||
className="p-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] transition-colors"
|
||||
title={isDarkMode ? '切换到浅色模式' : '切换到深色模式'}
|
||||
>
|
||||
{isDarkMode ? <Sun size={20} /> : <Moon size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<main className="max-w-[900px] mx-auto px-6 py-8">
|
||||
{/* CCH 配置 */}
|
||||
<SettingsSection
|
||||
title="CCH 服务配置"
|
||||
@ -315,23 +327,36 @@ export default function SettingsPage() {
|
||||
title="AI 配置"
|
||||
description="配置默认模型和工具"
|
||||
>
|
||||
<SettingsItem
|
||||
label="默认模型"
|
||||
description="为新对话选择默认 AI 模型"
|
||||
>
|
||||
<select
|
||||
className="settings-select"
|
||||
{/* 模型卡片选择 */}
|
||||
<div className="px-5 py-4 border-b border-[var(--color-border-light)]">
|
||||
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-2">
|
||||
默认模型
|
||||
</div>
|
||||
<div className="text-xs text-[var(--color-text-tertiary)] mb-4">
|
||||
为新对话选择默认 AI 模型
|
||||
</div>
|
||||
<ModelCardSelector
|
||||
value={settings?.defaultModel || ''}
|
||||
onChange={(e) => handleModelChange(e.target.value)}
|
||||
onChange={handleModelChange}
|
||||
disabled={modelsLoading || saving}
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model.modelId} value={model.modelId}>
|
||||
{model.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SettingsItem>
|
||||
models={models}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 字体大小设置 */}
|
||||
<div className="px-5 py-4 border-b border-[var(--color-border-light)]">
|
||||
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-2">
|
||||
字体大小
|
||||
</div>
|
||||
<div className="text-xs text-[var(--color-text-tertiary)] mb-4">
|
||||
调整全局字体大小,实时预览效果
|
||||
</div>
|
||||
<FontSizePicker
|
||||
value={settings?.fontSize || 15}
|
||||
onChange={handleFontSizeChange}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingsItem
|
||||
label="启用思考模式"
|
||||
@ -361,7 +386,7 @@ export default function SettingsPage() {
|
||||
onClick={() => handleToolToggle(tool.toolId)}
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all',
|
||||
'inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all',
|
||||
settings?.defaultTools?.includes(tool.toolId)
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-secondary)]'
|
||||
@ -465,45 +490,6 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
{/* 偏好设置 */}
|
||||
<SettingsSection
|
||||
title="偏好设置"
|
||||
description="自定义您的体验"
|
||||
>
|
||||
<SettingsItem
|
||||
label="主题"
|
||||
description="选择您喜欢的主题"
|
||||
>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={settings?.theme || 'light'}
|
||||
onChange={(e) => handleThemeChange(e.target.value)}
|
||||
disabled={saving}
|
||||
>
|
||||
<option value="system">跟随系统</option>
|
||||
<option value="light">浅色</option>
|
||||
<option value="dark">深色</option>
|
||||
</select>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
label="语言"
|
||||
description="选择您的首选语言"
|
||||
>
|
||||
<select
|
||||
className="settings-select"
|
||||
value={settings?.language || 'zh-CN'}
|
||||
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||
disabled={saving}
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="zh-TW">繁體中文</option>
|
||||
<option value="ja">日本語</option>
|
||||
</select>
|
||||
</SettingsItem>
|
||||
</SettingsSection>
|
||||
|
||||
{/* 数据与隐私 */}
|
||||
<SettingsSection
|
||||
title="数据与隐私"
|
||||
@ -534,27 +520,12 @@ export default function SettingsPage() {
|
||||
label="清除所有聊天"
|
||||
description="删除所有对话历史"
|
||||
>
|
||||
<button className="btn-ghost text-red-600 hover:text-red-700">
|
||||
<button className="btn-ghost text-red-600 hover:text-red-700 inline-flex items-center gap-2">
|
||||
<Trash2 size={16} />
|
||||
清除
|
||||
</button>
|
||||
</SettingsItem>
|
||||
</SettingsSection>
|
||||
|
||||
{/* 危险区域 */}
|
||||
<SettingsSection
|
||||
title="危险区域"
|
||||
description="不可逆操作"
|
||||
variant="danger"
|
||||
>
|
||||
<SettingsItem
|
||||
label="删除账户"
|
||||
description="永久删除您的账户和所有数据"
|
||||
>
|
||||
<button className="btn-danger">删除账户</button>
|
||||
</SettingsItem>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
@ -572,7 +543,7 @@ function SettingsSection({ title, description, children, variant = 'default' }:
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'bg-white border rounded-xl mb-6 overflow-hidden',
|
||||
'bg-white border rounded-md mb-6 overflow-hidden',
|
||||
variant === 'danger' ? 'border-red-200' : 'border-[var(--color-border)]'
|
||||
)}
|
||||
>
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { Plus, ChevronDown, PanelLeft, Trash2, MoreHorizontal, Loader2 } from 'lucide-react';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { Plus, PanelLeft, Trash2, MoreHorizontal, Loader2 } from 'lucide-react';
|
||||
import { UserMenu } from '@/components/ui/UserMenu';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useConversations } from '@/hooks/useConversations';
|
||||
import { useSettings } from '@/hooks/useSettings';
|
||||
@ -164,19 +164,9 @@ export function Sidebar({ user, isOpen = true }: SidebarProps) {
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* 用户信息 Footer */}
|
||||
{/* 用户信息 Footer - 使用 UserMenu 弹出菜单 */}
|
||||
<footer className="p-4 border-t border-[var(--color-border-light)] mt-auto">
|
||||
<Link
|
||||
href="/settings"
|
||||
className="flex items-center gap-3 p-2 rounded-lg cursor-pointer hover:bg-[var(--color-bg-hover)] transition-colors"
|
||||
>
|
||||
<Avatar name={user.name} size="md" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-[var(--color-text-primary)] truncate">{user.email}</div>
|
||||
<div className="text-xs text-[var(--color-text-tertiary)] capitalize">{user.plan} plan</div>
|
||||
</div>
|
||||
<ChevronDown size={16} className="text-[var(--color-text-tertiary)]" />
|
||||
</Link>
|
||||
<UserMenu user={user} />
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
|
||||
32
src/components/providers/SettingsProvider.tsx
Normal file
32
src/components/providers/SettingsProvider.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { useSettings } from '@/hooks/useSettings';
|
||||
|
||||
interface SettingsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function SettingsProvider({ children }: SettingsProviderProps) {
|
||||
const { settings, loading } = useSettings();
|
||||
|
||||
// 应用全局设置
|
||||
useEffect(() => {
|
||||
if (!loading && settings) {
|
||||
// 应用字体大小
|
||||
if (settings.fontSize) {
|
||||
document.documentElement.style.setProperty(
|
||||
'--font-size-base',
|
||||
`${settings.fontSize}px`
|
||||
);
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
if (settings.theme) {
|
||||
document.documentElement.setAttribute('data-theme', settings.theme);
|
||||
}
|
||||
}
|
||||
}, [settings, loading]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
138
src/components/ui/FontSizePicker.tsx
Normal file
138
src/components/ui/FontSizePicker.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Minus, Plus } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface FontSizePickerProps {
|
||||
value: number;
|
||||
onChange: (size: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function FontSizePicker({
|
||||
value,
|
||||
onChange,
|
||||
min = 12,
|
||||
max = 20,
|
||||
disabled = false,
|
||||
}: FontSizePickerProps) {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
|
||||
// 同步外部值
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
// 实时预览 - 修改 CSS 变量
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty('--font-size-base', `${localValue}px`);
|
||||
}, [localValue]);
|
||||
|
||||
const handleDecrease = () => {
|
||||
if (localValue > min && !disabled) {
|
||||
const newValue = localValue - 1;
|
||||
setLocalValue(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIncrease = () => {
|
||||
if (localValue < max && !disabled) {
|
||||
const newValue = localValue + 1;
|
||||
setLocalValue(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePreview = () => {
|
||||
setPreviewVisible(!previewVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 减号按钮 */}
|
||||
<button
|
||||
onClick={handleDecrease}
|
||||
disabled={disabled || localValue <= min}
|
||||
className={cn(
|
||||
'w-12 h-12 flex items-center justify-center',
|
||||
'rounded-md border-2 border-[var(--color-border)]',
|
||||
'bg-[var(--color-bg-primary)]',
|
||||
'text-[var(--color-text-secondary)]',
|
||||
'hover:border-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]',
|
||||
'transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2',
|
||||
(disabled || localValue <= min) && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
aria-label="减小字体"
|
||||
>
|
||||
<Minus size={20} strokeWidth={2.5} />
|
||||
</button>
|
||||
|
||||
{/* 数字显示 */}
|
||||
<div className="flex flex-col items-center min-w-[60px]">
|
||||
<span className="text-3xl font-semibold text-[var(--color-text-primary)]">
|
||||
{localValue}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">px</span>
|
||||
</div>
|
||||
|
||||
{/* 加号按钮 */}
|
||||
<button
|
||||
onClick={handleIncrease}
|
||||
disabled={disabled || localValue >= max}
|
||||
className={cn(
|
||||
'w-12 h-12 flex items-center justify-center',
|
||||
'rounded-md border-2 border-[var(--color-border)]',
|
||||
'bg-[var(--color-bg-primary)]',
|
||||
'text-[var(--color-text-secondary)]',
|
||||
'hover:border-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]',
|
||||
'transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2',
|
||||
(disabled || localValue >= max) && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
aria-label="增大字体"
|
||||
>
|
||||
<Plus size={20} strokeWidth={2.5} />
|
||||
</button>
|
||||
|
||||
{/* Preview 按钮 */}
|
||||
<button
|
||||
onClick={togglePreview}
|
||||
className={cn(
|
||||
'px-4 h-12 rounded-md',
|
||||
'bg-[var(--color-bg-tertiary)]',
|
||||
'text-sm font-medium text-[var(--color-text-secondary)]',
|
||||
'hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)]',
|
||||
'transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2',
|
||||
previewVisible && 'bg-[var(--color-primary-light)] text-[var(--color-primary)]'
|
||||
)}
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 预览区域 */}
|
||||
{previewVisible && (
|
||||
<div
|
||||
className="p-4 rounded-lg bg-[var(--color-bg-tertiary)] border border-[var(--color-border-light)] animate-fade-in"
|
||||
>
|
||||
<p
|
||||
className="text-[var(--color-text-primary)] leading-relaxed"
|
||||
style={{ fontSize: `${localValue}px` }}
|
||||
>
|
||||
这是一段预览文字,用于展示当前字体大小的效果。
|
||||
<br />
|
||||
The quick brown fox jumps over the lazy dog.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
108
src/components/ui/ModelCardSelector.tsx
Normal file
108
src/components/ui/ModelCardSelector.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// 模型卡片配置
|
||||
const MODEL_CARDS = [
|
||||
{
|
||||
id: 'haiku',
|
||||
name: 'Haiku',
|
||||
description: 'Fast & efficient',
|
||||
modelIdPattern: 'haiku',
|
||||
},
|
||||
{
|
||||
id: 'sonnet',
|
||||
name: 'Sonnet',
|
||||
description: 'Balanced',
|
||||
modelIdPattern: 'sonnet',
|
||||
},
|
||||
{
|
||||
id: 'opus',
|
||||
name: 'Opus',
|
||||
description: 'Most capable',
|
||||
modelIdPattern: 'opus',
|
||||
},
|
||||
];
|
||||
|
||||
interface ModelCardSelectorProps {
|
||||
value: string;
|
||||
onChange: (modelId: string) => void;
|
||||
disabled?: boolean;
|
||||
models?: { modelId: string; displayName: string }[];
|
||||
}
|
||||
|
||||
export function ModelCardSelector({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
models = [],
|
||||
}: ModelCardSelectorProps) {
|
||||
// 根据当前选中的模型ID判断选中的卡片
|
||||
const getSelectedCard = (modelId: string): string => {
|
||||
const lowerModelId = modelId.toLowerCase();
|
||||
if (lowerModelId.includes('haiku')) return 'haiku';
|
||||
if (lowerModelId.includes('opus')) return 'opus';
|
||||
return 'sonnet'; // 默认 sonnet
|
||||
};
|
||||
|
||||
// 根据卡片类型找到对应的实际模型ID
|
||||
const findModelIdByCard = (cardId: string): string => {
|
||||
const model = models.find((m) =>
|
||||
m.modelId.toLowerCase().includes(cardId)
|
||||
);
|
||||
return model?.modelId || value;
|
||||
};
|
||||
|
||||
const selectedCard = getSelectedCard(value);
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
{MODEL_CARDS.map((card) => {
|
||||
const isSelected = selectedCard === card.id;
|
||||
return (
|
||||
<button
|
||||
key={card.id}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
const newModelId = findModelIdByCard(card.id);
|
||||
onChange(newModelId);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center',
|
||||
'w-[120px] h-[72px] rounded-md',
|
||||
'border-2 transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--color-primary)]',
|
||||
isSelected
|
||||
? 'bg-[var(--color-primary-light)] border-[var(--color-primary)]'
|
||||
: 'bg-[var(--color-bg-tertiary)] border-transparent hover:border-[var(--color-border)]',
|
||||
disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm font-semibold',
|
||||
isSelected
|
||||
? 'text-[var(--color-primary)]'
|
||||
: 'text-[var(--color-text-primary)]'
|
||||
)}
|
||||
>
|
||||
{card.name}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs mt-1',
|
||||
isSelected
|
||||
? 'text-[var(--color-primary)]'
|
||||
: 'text-[var(--color-text-tertiary)]'
|
||||
)}
|
||||
>
|
||||
{card.description}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
src/components/ui/UserMenu.tsx
Normal file
183
src/components/ui/UserMenu.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Moon, Sun, Settings, LogOut, ChevronUp, ChevronDown } from 'lucide-react';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { useSettings } from '@/hooks/useSettings';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { User } from '@/types';
|
||||
|
||||
interface UserMenuProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export function UserMenu({ user }: UserMenuProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
|
||||
// 点击外部关闭菜单
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// ESC 键关闭菜单
|
||||
useEffect(() => {
|
||||
function handleEscKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// 切换主题
|
||||
const handleToggleTheme = async () => {
|
||||
const newTheme = settings.theme === 'dark' ? 'light' : 'dark';
|
||||
try {
|
||||
await updateSettings({ theme: newTheme });
|
||||
// 可选:更新 document 的 class 来应用主题
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
} catch (error) {
|
||||
console.error('Failed to update theme:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 进入设置页面
|
||||
const handleGoToSettings = () => {
|
||||
setIsOpen(false);
|
||||
router.push('/settings');
|
||||
};
|
||||
|
||||
// 登出
|
||||
const handleLogout = () => {
|
||||
setIsOpen(false);
|
||||
// TODO: 实现实际的登出逻辑
|
||||
// 暂时跳转到首页
|
||||
console.log('Logging out...');
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
const isDarkMode = settings.theme === 'dark';
|
||||
|
||||
return (
|
||||
<div ref={menuRef} className="relative">
|
||||
{/* 触发器 - 用户信息区域 */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 w-full p-2 rounded-lg cursor-pointer transition-colors',
|
||||
'hover:bg-[var(--color-bg-hover)]',
|
||||
isOpen && 'bg-[var(--color-bg-hover)]'
|
||||
)}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Avatar name={user.name} size="md" />
|
||||
<div className="flex-1 min-w-0 text-left">
|
||||
<div className="text-sm text-[var(--color-text-primary)] truncate">
|
||||
{user.email}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--color-text-tertiary)] capitalize">
|
||||
{user.plan} plan
|
||||
</div>
|
||||
</div>
|
||||
{isOpen ? (
|
||||
<ChevronUp size={16} className="text-[var(--color-text-tertiary)] flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronDown size={16} className="text-[var(--color-text-tertiary)] flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 弹出菜单 - 向上弹出 */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute bottom-full left-0 right-0 mb-2',
|
||||
'bg-[var(--color-bg-primary)] rounded-xl',
|
||||
'border border-[var(--color-border-light)]',
|
||||
'shadow-[var(--shadow-dropdown)]',
|
||||
'overflow-hidden',
|
||||
'animate-pop-up',
|
||||
'z-50'
|
||||
)}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
>
|
||||
{/* 菜单项 */}
|
||||
<div className="py-1">
|
||||
{/* Dark mode 切换 */}
|
||||
<button
|
||||
onClick={handleToggleTheme}
|
||||
className={cn(
|
||||
'flex items-center gap-3 w-full px-4 py-3',
|
||||
'text-sm text-[var(--color-text-primary)]',
|
||||
'hover:bg-[var(--color-bg-hover)] transition-colors'
|
||||
)}
|
||||
role="menuitem"
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun size={18} className="text-[var(--color-text-secondary)]" />
|
||||
) : (
|
||||
<Moon size={18} className="text-[var(--color-text-secondary)]" />
|
||||
)}
|
||||
<span>{isDarkMode ? 'Light mode' : 'Dark mode'}</span>
|
||||
</button>
|
||||
|
||||
{/* Settings */}
|
||||
<button
|
||||
onClick={handleGoToSettings}
|
||||
className={cn(
|
||||
'flex items-center gap-3 w-full px-4 py-3',
|
||||
'text-sm text-[var(--color-text-primary)]',
|
||||
'hover:bg-[var(--color-bg-hover)] transition-colors'
|
||||
)}
|
||||
role="menuitem"
|
||||
>
|
||||
<Settings size={18} className="text-[var(--color-text-secondary)]" />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<div className="my-1 border-t border-[var(--color-border-light)]" />
|
||||
|
||||
{/* Log out */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'flex items-center gap-3 w-full px-4 py-3',
|
||||
'text-sm text-red-500',
|
||||
'hover:bg-red-50 transition-colors'
|
||||
)}
|
||||
role="menuitem"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
<span>Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
src/drizzle/migrations/0002_bizarre_sunfire.sql
Normal file
1
src/drizzle/migrations/0002_bizarre_sunfire.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "user_settings" ADD COLUMN "font_size" integer DEFAULT 15;
|
||||
603
src/drizzle/migrations/meta/0002_snapshot.json
Normal file
603
src/drizzle/migrations/meta/0002_snapshot.json
Normal file
@ -0,0 +1,603 @@
|
||||
{
|
||||
"id": "25c992de-4501-4c60-97bb-c5cbfd4ef130",
|
||||
"prevId": "dc7ae80d-12dc-4e13-9c7b-101b9f79b198",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.conversations": {
|
||||
"name": "conversations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'新对话'"
|
||||
},
|
||||
"summary": {
|
||||
"name": "summary",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tools": {
|
||||
"name": "tools",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'[]'::jsonb"
|
||||
},
|
||||
"enable_thinking": {
|
||||
"name": "enable_thinking",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "system_prompt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperature",
|
||||
"type": "varchar(10)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"message_count": {
|
||||
"name": "message_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"total_tokens": {
|
||||
"name": "total_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"is_archived": {
|
||||
"name": "is_archived",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"is_pinned": {
|
||||
"name": "is_pinned",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_message_at": {
|
||||
"name": "last_message_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"conversations_conversation_id_unique": {
|
||||
"name": "conversations_conversation_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"conversation_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.messages": {
|
||||
"name": "messages",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"message_id": {
|
||||
"name": "message_id",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "varchar(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"thinking_content": {
|
||||
"name": "thinking_content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"thinking_collapsed": {
|
||||
"name": "thinking_collapsed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"tool_calls": {
|
||||
"name": "tool_calls",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_results": {
|
||||
"name": "tool_results",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "varchar(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'completed'"
|
||||
},
|
||||
"error_message": {
|
||||
"name": "error_message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"feedback": {
|
||||
"name": "feedback",
|
||||
"type": "varchar(10)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"messages_message_id_unique": {
|
||||
"name": "messages_message_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"message_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.models": {
|
||||
"name": "models",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"model_id": {
|
||||
"name": "model_id",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"supports_tools": {
|
||||
"name": "supports_tools",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"supports_thinking": {
|
||||
"name": "supports_thinking",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"supports_vision": {
|
||||
"name": "supports_vision",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "max_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 8192
|
||||
},
|
||||
"context_window": {
|
||||
"name": "context_window",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 200000
|
||||
},
|
||||
"is_enabled": {
|
||||
"name": "is_enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"is_default": {
|
||||
"name": "is_default",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"models_model_id_unique": {
|
||||
"name": "models_model_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"model_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tools": {
|
||||
"name": "tools",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"tool_id": {
|
||||
"name": "tool_id",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"input_schema": {
|
||||
"name": "input_schema",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_enabled": {
|
||||
"name": "is_enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"is_default": {
|
||||
"name": "is_default",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"tools_tool_id_unique": {
|
||||
"name": "tools_tool_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"tool_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_settings": {
|
||||
"name": "user_settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"cch_url": {
|
||||
"name": "cch_url",
|
||||
"type": "varchar(512)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'http://localhost:13500'"
|
||||
},
|
||||
"cch_api_key": {
|
||||
"name": "cch_api_key",
|
||||
"type": "varchar(512)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cch_api_key_configured": {
|
||||
"name": "cch_api_key_configured",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"default_model": {
|
||||
"name": "default_model",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'claude-sonnet-4-20250514'"
|
||||
},
|
||||
"default_tools": {
|
||||
"name": "default_tools",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'[\"web_search\",\"code_execution\",\"web_fetch\"]'::jsonb"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "system_prompt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperature",
|
||||
"type": "varchar(10)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'0.7'"
|
||||
},
|
||||
"theme": {
|
||||
"name": "theme",
|
||||
"type": "varchar(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'light'"
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "varchar(10)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'zh-CN'"
|
||||
},
|
||||
"font_size": {
|
||||
"name": "font_size",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 15
|
||||
},
|
||||
"enable_thinking": {
|
||||
"name": "enable_thinking",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"save_chat_history": {
|
||||
"name": "save_chat_history",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,13 @@
|
||||
"when": 1765991206886,
|
||||
"tag": "0001_daffy_paladin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1766110727907,
|
||||
"tag": "0002_bizarre_sunfire",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -28,6 +28,7 @@ export const userSettings = pgTable('user_settings', {
|
||||
// 偏好设置
|
||||
theme: varchar('theme', { length: 20 }).default('light'),
|
||||
language: varchar('language', { length: 10 }).default('zh-CN'),
|
||||
fontSize: integer('font_size').default(15), // 全局字体大小 (12-20)
|
||||
enableThinking: boolean('enable_thinking').default(false),
|
||||
saveChatHistory: boolean('save_chat_history').default(true),
|
||||
// 时间戳
|
||||
|
||||
@ -11,6 +11,7 @@ export interface Settings {
|
||||
temperature: string;
|
||||
theme: string;
|
||||
language: string;
|
||||
fontSize: number;
|
||||
enableThinking: boolean;
|
||||
saveChatHistory: boolean;
|
||||
}
|
||||
@ -53,6 +54,7 @@ const defaultSettings: Settings = {
|
||||
temperature: '0.7',
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
fontSize: 15,
|
||||
enableThinking: false,
|
||||
saveChatHistory: true,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user