feat(ui): 实现深色/浅色主题切换功能
- 添加 CSS 变量定义浅色和深色主题色板 - 扩展 Tailwind 配置支持语义化颜色 (surface-*, fg-*, line-*, code) - 创建 useTheme hook 管理主题状态和持久化 - 创建 ThemeToggle 组件支持三种模式 (light/dark/system) - 迁移所有组件从硬编码 gray-* 到语义化颜色 - 支持系统主题偏好检测 (prefers-color-scheme) - 添加主题初始化脚本防止闪烁 (FOUC)
This commit is contained in:
@@ -42,7 +42,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
// 优先使用 parts 数组(保持原始顺序)
|
||||
if (message.parts && message.parts.length > 0) {
|
||||
return (
|
||||
<div className="message-content text-gray-200 space-y-3">
|
||||
<div className="message-content text-fg-secondary space-y-3">
|
||||
{message.parts.map((part) => {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
@@ -58,7 +58,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
return <ToolPartItem key={part.id} part={part} />;
|
||||
case 'reasoning':
|
||||
return (
|
||||
<div key={part.id} className="text-gray-400 italic border-l-2 border-gray-600 pl-3">
|
||||
<div key={part.id} className="text-fg-muted italic border-l-2 border-line pl-3">
|
||||
{part.text}
|
||||
</div>
|
||||
);
|
||||
@@ -76,7 +76,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
{!isUser && message.toolCalls && message.toolCalls.length > 0 && (
|
||||
<ToolCallsDisplay toolCalls={message.toolCalls} />
|
||||
)}
|
||||
<div className="message-content text-gray-200">
|
||||
<div className="message-content text-fg-secondary">
|
||||
{isUser ? (
|
||||
<div className="whitespace-pre-wrap break-words">{message.content ?? ''}</div>
|
||||
) : (
|
||||
@@ -97,12 +97,12 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
transition={smoothTransition}
|
||||
className={cn(
|
||||
'group flex gap-4 p-4 rounded-lg',
|
||||
isUser ? 'bg-gray-800' : 'bg-gray-800/50'
|
||||
isUser ? 'bg-surface-subtle' : 'bg-surface-subtle/50'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0',
|
||||
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 text-white',
|
||||
isUser ? 'bg-primary-600' : 'bg-green-600'
|
||||
)}
|
||||
>
|
||||
@@ -110,12 +110,12 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-400">
|
||||
<span className="text-sm text-fg-muted">
|
||||
{isUser ? 'You' : 'AI Assistant'}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded text-gray-500 hover:text-gray-300 hover:bg-gray-700 transition-all"
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded text-fg-subtle hover:text-fg-muted hover:bg-surface-muted transition-all"
|
||||
title="Copy message"
|
||||
>
|
||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
||||
@@ -138,14 +138,14 @@ export function StreamingMessage({ content }: StreamingMessageProps) {
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={smoothTransition}
|
||||
className="flex gap-4 p-4 rounded-lg bg-gray-800/50"
|
||||
className="flex gap-4 p-4 rounded-lg bg-surface-subtle/50"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600 text-white">
|
||||
<Bot size={18} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="text-sm text-gray-400 mb-1">AI Assistant</div>
|
||||
<div className="message-content text-gray-200">
|
||||
<div className="text-sm text-fg-muted mb-1">AI Assistant</div>
|
||||
<div className="message-content text-fg-secondary">
|
||||
<Markdown content={content} />
|
||||
<motion.span
|
||||
animate={{ opacity: [1, 0] }}
|
||||
@@ -164,13 +164,13 @@ export function TypingIndicator() {
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={smoothTransition}
|
||||
className="flex gap-4 p-4 rounded-lg bg-gray-800/50"
|
||||
className="flex gap-4 p-4 rounded-lg bg-surface-subtle/50"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600 text-white">
|
||||
<Bot size={18} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-gray-400 mb-1">AI Assistant</div>
|
||||
<div className="text-sm text-fg-muted mb-1">AI Assistant</div>
|
||||
<div className="flex items-center gap-1 h-6">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.span
|
||||
@@ -181,7 +181,7 @@ export function TypingIndicator() {
|
||||
repeat: Infinity,
|
||||
delay: i * 0.15,
|
||||
}}
|
||||
className="w-2 h-2 rounded-full bg-gray-400"
|
||||
className="w-2 h-2 rounded-full bg-fg-muted"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -207,26 +207,26 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
part.error !== undefined;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-700 rounded-lg overflow-hidden bg-gray-800/30">
|
||||
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
|
||||
{/* 头部:工具名称、状态、时长 */}
|
||||
<button
|
||||
onClick={() => hasDetails && setExpanded(!expanded)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-3 py-2 text-sm',
|
||||
hasDetails && 'hover:bg-gray-700/50 cursor-pointer',
|
||||
hasDetails && 'hover:bg-surface-muted/50 cursor-pointer',
|
||||
!hasDetails && 'cursor-default'
|
||||
)}
|
||||
>
|
||||
<Wrench size={14} className="text-gray-400 flex-shrink-0" />
|
||||
<span className="font-mono text-gray-200 flex-1 text-left truncate">
|
||||
<Wrench size={14} className="text-fg-muted flex-shrink-0" />
|
||||
<span className="font-mono text-fg-secondary flex-1 text-left truncate">
|
||||
{part.toolName}
|
||||
</span>
|
||||
{getStatusIcon(part.status)}
|
||||
{part.duration && (
|
||||
<span className="text-xs text-gray-500">{formatDuration(part.duration)}</span>
|
||||
<span className="text-xs text-fg-subtle">{formatDuration(part.duration)}</span>
|
||||
)}
|
||||
{hasDetails && (
|
||||
<span className="text-gray-500">
|
||||
<span className="text-fg-subtle">
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</span>
|
||||
)}
|
||||
@@ -240,14 +240,14 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border-t border-gray-700 overflow-hidden"
|
||||
className="border-t border-line overflow-hidden"
|
||||
>
|
||||
<div className="px-3 py-2 space-y-2 text-xs">
|
||||
{/* 参数 */}
|
||||
{Object.keys(part.arguments).length > 0 && (
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">Arguments:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-gray-300 max-h-48 overflow-y-auto">
|
||||
<div className="text-fg-subtle mb-1">Arguments:</div>
|
||||
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-fg-muted max-h-48 overflow-y-auto">
|
||||
{JSON.stringify(part.arguments, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -256,8 +256,8 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
{/* 结果 */}
|
||||
{part.result !== undefined && (
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">Result:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-green-300 max-h-48 overflow-y-auto">
|
||||
<div className="text-fg-subtle mb-1">Result:</div>
|
||||
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-green-400 max-h-48 overflow-y-auto">
|
||||
{typeof part.result === 'string'
|
||||
? part.result
|
||||
: JSON.stringify(part.result, null, 2)}
|
||||
@@ -269,7 +269,7 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
{part.error && (
|
||||
<div>
|
||||
<div className="text-red-400 mb-1">Error:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
|
||||
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
|
||||
{part.error}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -339,26 +339,26 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
||||
toolCall.error !== undefined;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-700 rounded-lg overflow-hidden bg-gray-800/30">
|
||||
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
|
||||
{/* 头部:工具名称、状态、时长 */}
|
||||
<button
|
||||
onClick={() => hasDetails && setExpanded(!expanded)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-3 py-2 text-sm',
|
||||
hasDetails && 'hover:bg-gray-700/50 cursor-pointer',
|
||||
hasDetails && 'hover:bg-surface-muted/50 cursor-pointer',
|
||||
!hasDetails && 'cursor-default'
|
||||
)}
|
||||
>
|
||||
<Wrench size={14} className="text-gray-400 flex-shrink-0" />
|
||||
<span className="font-mono text-gray-200 flex-1 text-left truncate">
|
||||
<Wrench size={14} className="text-fg-muted flex-shrink-0" />
|
||||
<span className="font-mono text-fg-secondary flex-1 text-left truncate">
|
||||
{toolCall.name}
|
||||
</span>
|
||||
{getStatusIcon(toolCall.status)}
|
||||
{toolCall.duration && (
|
||||
<span className="text-xs text-gray-500">{formatDuration(toolCall.duration)}</span>
|
||||
<span className="text-xs text-fg-subtle">{formatDuration(toolCall.duration)}</span>
|
||||
)}
|
||||
{hasDetails && (
|
||||
<span className="text-gray-500">
|
||||
<span className="text-fg-subtle">
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</span>
|
||||
)}
|
||||
@@ -372,14 +372,14 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border-t border-gray-700 overflow-hidden"
|
||||
className="border-t border-line overflow-hidden"
|
||||
>
|
||||
<div className="px-3 py-2 space-y-2 text-xs">
|
||||
{/* 参数 */}
|
||||
{Object.keys(toolCall.arguments).length > 0 && (
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">Arguments:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-gray-300 max-h-48 overflow-y-auto">
|
||||
<div className="text-fg-subtle mb-1">Arguments:</div>
|
||||
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-fg-muted max-h-48 overflow-y-auto">
|
||||
{JSON.stringify(toolCall.arguments, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -388,8 +388,8 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
||||
{/* 结果 */}
|
||||
{toolCall.result !== undefined && (
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">Result:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-green-300 max-h-48 overflow-y-auto">
|
||||
<div className="text-fg-subtle mb-1">Result:</div>
|
||||
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-green-400 max-h-48 overflow-y-auto">
|
||||
{typeof toolCall.result === 'string'
|
||||
? toolCall.result
|
||||
: JSON.stringify(toolCall.result, null, 2)}
|
||||
@@ -401,7 +401,7 @@ function ToolCallItem({ toolCall }: ToolCallItemProps) {
|
||||
{toolCall.error && (
|
||||
<div>
|
||||
<div className="text-red-400 mb-1">Error:</div>
|
||||
<pre className="bg-gray-900 rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
|
||||
<pre className="bg-surface-base rounded p-2 overflow-x-auto text-red-300 max-h-48 overflow-y-auto">
|
||||
{toolCall.error}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user