feat(ui): 实现深色/浅色主题切换功能
- 添加 CSS 变量定义浅色和深色主题色板 - 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code) - 创建 useTheme hook 管理主题状态和持久化 - 创建 ThemeToggle 组件支持三种模式 (light/dark/system) - 迁移所有组件从硬编码 gray-* 到语义化颜色 - 支持系统主题偏好检测 (prefers-color-scheme) - 添加主题初始化脚本防止闪烁 (FOUC)
This commit is contained in:
@@ -255,7 +255,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
const LoadingSkeleton = () => (
|
||||
<div className="space-y-3 p-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-3 p-3 bg-gray-900/50 rounded-lg">
|
||||
<div key={i} className="flex items-center gap-3 p-3 bg-surface-base/50 rounded-lg">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
@@ -279,18 +279,18 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
layout
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-gray-900/50 rounded-lg overflow-hidden"
|
||||
className="bg-surface-base/50 rounded-lg overflow-hidden"
|
||||
>
|
||||
{/* Provider Header */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 p-3',
|
||||
'hover:bg-gray-900/80 transition-colors cursor-pointer'
|
||||
'hover:bg-surface-base/80 transition-colors cursor-pointer'
|
||||
)}
|
||||
onClick={() => toggleExpanded(provider.id)}
|
||||
>
|
||||
{/* Expand Icon */}
|
||||
<button className="text-gray-500 hover:text-gray-300">
|
||||
<button className="text-fg-subtle hover:text-fg-secondary">
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</button>
|
||||
|
||||
@@ -304,7 +304,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-200">{provider.name}</span>
|
||||
<span className="font-medium text-fg-secondary">{provider.name}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs px-2 py-0.5 rounded-full',
|
||||
@@ -314,7 +314,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{provider.builtin ? 'Built-in' : 'Custom'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 flex items-center gap-2">
|
||||
<div className="text-xs text-fg-subtle flex items-center gap-2">
|
||||
<span>{provider.modelCount} models</span>
|
||||
{provider.hasApiKey ? (
|
||||
<span className="text-green-400 flex items-center gap-1">
|
||||
@@ -349,7 +349,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
size="sm"
|
||||
onClick={() => handleTestConnection(provider.id)}
|
||||
disabled={isTesting}
|
||||
className="text-gray-400 hover:text-gray-300"
|
||||
className="text-fg-muted hover:text-fg-secondary"
|
||||
title="Test Connection"
|
||||
>
|
||||
{isTesting ? (
|
||||
@@ -364,7 +364,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setEditingProviderId(provider.id)}
|
||||
className="text-gray-400 hover:text-gray-300"
|
||||
className="text-fg-muted hover:text-fg-secondary"
|
||||
title="Configure"
|
||||
>
|
||||
<Settings size={14} />
|
||||
@@ -395,29 +395,29 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="px-4 pb-3 pt-1 border-t border-gray-700/50 space-y-3">
|
||||
<div className="px-4 pb-3 pt-1 border-t border-line/50 space-y-3">
|
||||
{detail ? (
|
||||
<>
|
||||
{/* Base URL */}
|
||||
{detail.baseUrl && (
|
||||
<div className="text-xs">
|
||||
<span className="text-gray-400">Base URL:</span>{' '}
|
||||
<code className="text-gray-300 bg-gray-800 px-1 rounded">{detail.baseUrl}</code>
|
||||
<span className="text-fg-muted">Base URL:</span>{' '}
|
||||
<code className="text-fg-secondary bg-surface-subtle px-1 rounded">{detail.baseUrl}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API Key Env Var */}
|
||||
{detail.apiKeyEnvVar && (
|
||||
<div className="text-xs">
|
||||
<span className="text-gray-400">API Key Env:</span>{' '}
|
||||
<code className="text-gray-300 bg-gray-800 px-1 rounded">{detail.apiKeyEnvVar}</code>
|
||||
<span className="text-fg-muted">API Key Env:</span>{' '}
|
||||
<code className="text-fg-secondary bg-surface-subtle px-1 rounded">{detail.apiKeyEnvVar}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Models */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400 flex items-center gap-1">
|
||||
<span className="text-xs text-fg-muted flex items-center gap-1">
|
||||
<Cpu size={12} />
|
||||
Models ({detail.models.length + detail.config.customModels.length})
|
||||
</span>
|
||||
@@ -440,13 +440,13 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{detail.models.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="flex items-center justify-between text-xs p-2 bg-gray-800/50 rounded"
|
||||
className="flex items-center justify-between text-xs p-2 bg-surface-subtle/50 rounded"
|
||||
>
|
||||
<div>
|
||||
<span className="text-gray-200">{model.name}</span>
|
||||
<span className="text-gray-500 ml-2">({model.id})</span>
|
||||
<span className="text-fg-secondary">{model.name}</span>
|
||||
<span className="text-fg-subtle ml-2">({model.id})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<div className="flex items-center gap-2 text-fg-subtle">
|
||||
{model.capabilities?.vision && (
|
||||
<span title="Vision" className="text-[10px]">Vision</span>
|
||||
)}
|
||||
@@ -461,11 +461,11 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{detail.config.customModels.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="flex items-center justify-between text-xs p-2 bg-gray-800/50 rounded border border-green-500/20"
|
||||
className="flex items-center justify-between text-xs p-2 bg-surface-subtle/50 rounded border border-green-500/20"
|
||||
>
|
||||
<div>
|
||||
<span className="text-gray-200">{model.name}</span>
|
||||
<span className="text-gray-500 ml-2">({model.id})</span>
|
||||
<span className="text-fg-secondary">{model.name}</span>
|
||||
<span className="text-fg-subtle ml-2">({model.id})</span>
|
||||
<span className="text-green-400 ml-2 text-[10px]">custom</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -516,7 +516,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
transition={smoothTransition}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={cn(
|
||||
'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col',
|
||||
'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col',
|
||||
responsive
|
||||
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
|
||||
: 'rounded-lg w-full max-w-2xl mx-4'
|
||||
@@ -525,19 +525,19 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{/* Header */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between border-b border-gray-700',
|
||||
'flex items-center justify-between border-b border-line',
|
||||
responsive ? 'px-4 md:px-6 py-4' : 'px-6 py-4'
|
||||
)}
|
||||
>
|
||||
{responsive && (
|
||||
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-gray-600 rounded-full md:hidden" />
|
||||
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-10 h-1 bg-surface-emphasis rounded-full md:hidden" />
|
||||
)}
|
||||
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Server size={20} className="text-primary-400" />
|
||||
Model Providers
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-fg-subtle">
|
||||
{providers.length} providers ({builtinProviders.length} built-in, {customProviders.length} custom)
|
||||
</p>
|
||||
</div>
|
||||
@@ -577,7 +577,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{loading ? (
|
||||
<LoadingSkeleton />
|
||||
) : providers.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-fg-subtle">
|
||||
<Server size={48} className="mb-4 opacity-50" />
|
||||
<p className="text-center">No providers available</p>
|
||||
</div>
|
||||
@@ -590,7 +590,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{/* Built-in Providers */}
|
||||
{builtinProviders.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
|
||||
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
|
||||
<Server size={12} />
|
||||
Built-in Providers
|
||||
</h3>
|
||||
@@ -604,7 +604,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
|
||||
{/* Custom Providers */}
|
||||
<div>
|
||||
<h3 className="text-xs font-medium text-gray-400 uppercase tracking-wide mb-2 flex items-center gap-2">
|
||||
<h3 className="text-xs font-medium text-fg-muted uppercase tracking-wide mb-2 flex items-center gap-2">
|
||||
<Globe size={12} />
|
||||
Custom Providers
|
||||
</h3>
|
||||
@@ -615,7 +615,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-gray-500 text-sm">
|
||||
<div className="text-center py-6 text-fg-subtle text-sm">
|
||||
<p>No custom providers yet</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -636,12 +636,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
{/* Footer */}
|
||||
<div
|
||||
className={cn(
|
||||
'border-t border-gray-700 text-xs text-gray-500 text-center',
|
||||
'border-t border-line text-xs text-fg-subtle text-center',
|
||||
responsive ? 'px-4 py-3 safe-area-pb' : 'px-6 py-3'
|
||||
)}
|
||||
>
|
||||
Config stored in{' '}
|
||||
<code className="font-mono bg-gray-900 px-1 rounded">~/.ai-terminal-assistant/providers.json</code>
|
||||
<code className="font-mono bg-surface-base px-1 rounded">~/.ai-terminal-assistant/providers.json</code>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -660,12 +660,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
|
||||
className="bg-surface-subtle rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">Add Custom Provider</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">ID (e.g., ollama)</label>
|
||||
<label className="text-xs text-fg-muted">ID (e.g., ollama)</label>
|
||||
<Input
|
||||
value={newProvider.id || ''}
|
||||
onChange={(e) => setNewProvider((p) => ({ ...p, id: e.target.value }))}
|
||||
@@ -673,7 +673,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">Name</label>
|
||||
<label className="text-xs text-fg-muted">Name</label>
|
||||
<Input
|
||||
value={newProvider.name || ''}
|
||||
onChange={(e) => setNewProvider((p) => ({ ...p, name: e.target.value }))}
|
||||
@@ -681,7 +681,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">Base URL (OpenAI compatible)</label>
|
||||
<label className="text-xs text-fg-muted">Base URL (OpenAI compatible)</label>
|
||||
<Input
|
||||
value={newProvider.baseUrl || ''}
|
||||
onChange={(e) => setNewProvider((p) => ({ ...p, baseUrl: e.target.value }))}
|
||||
@@ -689,7 +689,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">API Key Env Var (optional)</label>
|
||||
<label className="text-xs text-fg-muted">API Key Env Var (optional)</label>
|
||||
<Input
|
||||
value={newProvider.apiKeyEnvVar || ''}
|
||||
onChange={(e) => setNewProvider((p) => ({ ...p, apiKeyEnvVar: e.target.value }))}
|
||||
@@ -725,12 +725,12 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
|
||||
className="bg-surface-subtle rounded-lg p-6 w-full max-w-md mx-4 space-y-4"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">Add Custom Model</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">Model ID</label>
|
||||
<label className="text-xs text-fg-muted">Model ID</label>
|
||||
<Input
|
||||
value={newModel.id || ''}
|
||||
onChange={(e) => setNewModel((m) => ({ ...m, id: e.target.value }))}
|
||||
@@ -738,7 +738,7 @@ export function ProvidersPanel({ onClose, responsive = false }: ProvidersPanelPr
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400">Display Name</label>
|
||||
<label className="text-xs text-fg-muted">Display Name</label>
|
||||
<Input
|
||||
value={newModel.name || ''}
|
||||
onChange={(e) => setNewModel((m) => ({ ...m, name: e.target.value }))}
|
||||
|
||||
Reference in New Issue
Block a user