feat(web): 重构组件样式并添加确认对话框组件
- 优化组件视觉风格: 使用渐变色、阴影和圆角增强现代感 - 添加 ConfirmDialog 组件和 useConfirm hook 用于确认操作 - 改进聊天界面: 添加滚动到底部按钮、斜杠命令自动弹出 - 统一组件间距和颜色方案,提升 UI 一致性
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Send, Terminal, Image, Play, FileText, ArrowUp } from 'lucide-react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Terminal, Image, Play, FileText, ArrowUp } from 'lucide-react';
|
||||
|
||||
const SLASH_COMMANDS = [
|
||||
{ cmd: '/run', desc: '执行 pipeline 阶段', icon: Play },
|
||||
@@ -19,6 +19,16 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string) => v
|
||||
? SLASH_COMMANDS.filter((c) => c.cmd.startsWith(input.split(' ')[0]))
|
||||
: [];
|
||||
|
||||
// Auto-show/hide slash command menu
|
||||
useEffect(() => {
|
||||
if (input.startsWith('/') && matchingCmds.length > 0) {
|
||||
setShowCmds(true);
|
||||
setCmdIdx(0);
|
||||
} else {
|
||||
setShowCmds(false);
|
||||
}
|
||||
}, [input, matchingCmds.length]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || disabled) return;
|
||||
onSend(input.trim());
|
||||
|
||||
@@ -60,19 +60,21 @@ export function ChatMessage({ message, isLast, isThinking, onRegenerate, onConti
|
||||
<div className={cn('flex gap-3 mb-5 group/msg', isUser ? 'flex-row-reverse' : 'flex-row')}>
|
||||
{/* Avatar */}
|
||||
<div className={cn(
|
||||
'w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0 text-xs',
|
||||
isUser ? 'bg-indigo-600 text-white' : 'bg-zinc-100 text-zinc-500'
|
||||
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 text-xs',
|
||||
isUser
|
||||
? 'bg-gradient-to-br from-indigo-500 to-indigo-600 text-white shadow-sm'
|
||||
: 'bg-zinc-100 text-zinc-500'
|
||||
)}>
|
||||
{isUser ? <User size={13} /> : <Bot size={13} />}
|
||||
{isUser ? <User size={14} /> : <Bot size={14} />}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className={cn('flex-1 max-w-[78%]', isUser && 'flex flex-col items-end')}>
|
||||
{/* Thinking indicator */}
|
||||
{state === 'thinking' && !message.content && !message.reasoningContent && (
|
||||
<div className="rounded-2xl px-3.5 py-2.5 bg-zinc-50 border border-zinc-100 rounded-tl-md">
|
||||
<div className="rounded-2xl px-4 py-3 bg-gradient-to-br from-indigo-50 to-zinc-50 border border-indigo-100/50 rounded-tl-sm">
|
||||
<div className="flex items-center gap-2 text-zinc-400 text-sm">
|
||||
<Brain size={14} className="animate-pulse" />
|
||||
<Brain size={14} className="animate-pulse text-indigo-400" />
|
||||
<span>深度思考中...</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,7 +98,7 @@ export function ChatMessage({ message, isLast, isThinking, onRegenerate, onConti
|
||||
)}
|
||||
</button>
|
||||
{(reasoningOpen || state !== 'done') && (
|
||||
<div className="mt-1 px-3 py-2 rounded-lg bg-indigo-50/50 border border-indigo-100 text-xs text-zinc-500 leading-relaxed max-h-60 overflow-y-auto whitespace-pre-wrap">
|
||||
<div className="mt-1 px-3 py-2.5 rounded-xl bg-gradient-to-br from-indigo-50/80 to-violet-50/40 border border-indigo-100/50 text-xs text-zinc-500 leading-relaxed max-h-60 overflow-y-auto whitespace-pre-wrap">
|
||||
{message.reasoningContent}
|
||||
</div>
|
||||
)}
|
||||
@@ -106,16 +108,16 @@ export function ChatMessage({ message, isLast, isThinking, onRegenerate, onConti
|
||||
{/* Main content bubble */}
|
||||
{!(message.content === '(调用工具)' && state === 'done' && message.reasoningContent) && (
|
||||
<div className={cn(
|
||||
'rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed',
|
||||
'rounded-2xl px-4 py-2.5 text-sm leading-relaxed',
|
||||
isUser
|
||||
? 'bg-indigo-600 text-white rounded-tr-md'
|
||||
: 'bg-zinc-50 text-zinc-700 border border-zinc-100 rounded-tl-md'
|
||||
? 'bg-gradient-to-br from-indigo-500 to-indigo-600 text-white rounded-tr-sm shadow-sm'
|
||||
: 'bg-zinc-50/80 text-zinc-700 border border-zinc-100 rounded-tl-sm'
|
||||
)}>
|
||||
{state === 'streaming' && !message.content ? (
|
||||
<div className="flex items-center gap-1 text-zinc-400">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-zinc-300 rounded-full animate-pulse" />
|
||||
<span className="inline-block w-1.5 h-1.5 bg-zinc-300 rounded-full animate-pulse [animation-delay:100ms]" />
|
||||
<span className="inline-block w-1.5 h-1.5 bg-zinc-300 rounded-full animate-pulse [animation-delay:200ms]" />
|
||||
<div className="flex items-center gap-1.5 text-zinc-400">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-indigo-300 rounded-full animate-pulse" />
|
||||
<span className="inline-block w-1.5 h-1.5 bg-indigo-300 rounded-full animate-pulse [animation-delay:150ms]" />
|
||||
<span className="inline-block w-1.5 h-1.5 bg-indigo-300 rounded-full animate-pulse [animation-delay:300ms]" />
|
||||
</div>
|
||||
) : isUser ? (
|
||||
<p className="whitespace-pre-wrap">{message.content}</p>
|
||||
@@ -125,27 +127,27 @@ export function ChatMessage({ message, isLast, isThinking, onRegenerate, onConti
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions - visible on hover or when done */}
|
||||
{/* Actions */}
|
||||
{showActions && (
|
||||
<div className="flex gap-0.5 mt-1 opacity-0 group-hover/msg:opacity-100 transition-opacity px-1">
|
||||
<div className="flex gap-0.5 mt-1 opacity-0 group-hover/msg:opacity-100 transition-opacity duration-200 px-1">
|
||||
<button onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
<Copy size={10} />复制
|
||||
</button>
|
||||
<button onClick={() => onQuote?.(message.content)}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
<Quote size={10} />引用
|
||||
</button>
|
||||
<button onClick={handleDelete}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-red-500 hover:bg-red-50 transition-colors">
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] text-zinc-400 hover:text-red-500 hover:bg-red-50 transition-colors">
|
||||
<Trash2 size={10} />删除
|
||||
</button>
|
||||
<button onClick={() => onRegenerate?.(message.id)}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
<RefreshCw size={10} />重新生成
|
||||
</button>
|
||||
<button onClick={() => onContinue?.()}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
|
||||
<ArrowRight size={10} />继续
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ChatMessage } from './ChatMessage';
|
||||
import { ChatInput } from './ChatInput';
|
||||
import { PipelineProgress } from './PipelineProgress';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw, Loader2, StopCircle, X, MessageCircle } from 'lucide-react';
|
||||
import { RefreshCw, Loader2, StopCircle, X, MessageCircle, ArrowDown } from 'lucide-react';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Account, Message } from '@/types';
|
||||
|
||||
@@ -19,6 +19,7 @@ export function ChatView() {
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [quote, setQuote] = useState<string | null>(null);
|
||||
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
|
||||
const [showScrollBtn, setShowScrollBtn] = useState(false);
|
||||
const creatingRef = useRef(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const userScrolledUp = useRef(false);
|
||||
@@ -54,6 +55,7 @@ export function ChatView() {
|
||||
const onScroll = () => {
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
||||
userScrolledUp.current = !atBottom;
|
||||
setShowScrollBtn(!atBottom);
|
||||
};
|
||||
el.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => el.removeEventListener('scroll', onScroll);
|
||||
@@ -62,6 +64,7 @@ export function ChatView() {
|
||||
// Reset scroll state on conversation switch
|
||||
useEffect(() => {
|
||||
userScrolledUp.current = false;
|
||||
setShowScrollBtn(false);
|
||||
}, [conversationId]);
|
||||
|
||||
// After navigating to a new conversation, send the pending message once history is loaded
|
||||
@@ -137,9 +140,12 @@ export function ChatView() {
|
||||
if (!conversationId) {
|
||||
const recentConvs = conversations.slice(0, 5);
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center bg-white">
|
||||
<div className="flex-1 flex flex-col items-center justify-center bg-gradient-to-b from-white to-zinc-50/30">
|
||||
<div className="w-full max-w-xl px-4">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-50 to-violet-50 flex items-center justify-center mx-auto mb-4 shadow-sm">
|
||||
<MessageCircle size={24} className="text-indigo-400" strokeWidth={1.5} />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-zinc-800">开始新对话</h2>
|
||||
<p className="text-sm text-zinc-400 mt-1">输入消息开始创作,对话将自动创建</p>
|
||||
</div>
|
||||
@@ -150,7 +156,7 @@ export function ChatView() {
|
||||
<select
|
||||
value={selectedAccountId || ''}
|
||||
onChange={(e) => useAppStore.getState().setSelectedAccountId(e.target.value || null)}
|
||||
className="h-7 rounded-md border border-zinc-200 bg-white px-2.5 text-xs text-zinc-600"
|
||||
className="h-7 rounded-lg border border-zinc-100 bg-white px-2.5 text-xs text-zinc-600 hover:border-zinc-200 transition-colors"
|
||||
>
|
||||
<option value="">不指定</option>
|
||||
{accounts.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
|
||||
@@ -164,7 +170,7 @@ export function ChatView() {
|
||||
<button
|
||||
key={conv.id}
|
||||
onClick={() => navigate(`/chat/${conv.id}`)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left hover:bg-zinc-50 transition-colors group"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 rounded-xl text-left hover:bg-zinc-50/80 transition-colors group"
|
||||
>
|
||||
<MessageCircle size={13} className="text-zinc-300 flex-shrink-0" />
|
||||
<span className="text-sm text-zinc-600 truncate flex-1 group-hover:text-zinc-800">{conv.title}</span>
|
||||
@@ -184,19 +190,19 @@ export function ChatView() {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-white h-full">
|
||||
{/* Header bar */}
|
||||
<div className="px-4 py-2 border-b border-zinc-200 flex items-center justify-between flex-shrink-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${connected ? 'bg-green-500' : 'bg-red-400'}`} />
|
||||
<div className="px-4 py-2 border-b border-zinc-100 flex items-center justify-between flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${connected ? 'bg-emerald-400 shadow-sm shadow-emerald-200' : 'bg-red-400 animate-pulse'}`} />
|
||||
<span className="text-xs text-zinc-400">{connected ? '在线' : '连接中'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{manifestPath && (
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={handleResume}>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs rounded-lg" onClick={handleResume}>
|
||||
<RefreshCw size={12} className="mr-1" />断点续跑
|
||||
</Button>
|
||||
)}
|
||||
{thinking && (
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs text-red-500 border-red-200 hover:bg-red-50" onClick={handleStop}>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs text-red-500 border-red-200 hover:bg-red-50 rounded-lg" onClick={handleStop}>
|
||||
<StopCircle size={12} className="mr-1" />停止
|
||||
</Button>
|
||||
)}
|
||||
@@ -204,7 +210,7 @@ export function ChatView() {
|
||||
</div>
|
||||
|
||||
{/* Chat area */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Message list */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4">
|
||||
@@ -235,15 +241,29 @@ export function ChatView() {
|
||||
|
||||
{thinking && !pipeline && (
|
||||
<div className="flex items-center gap-2 text-zinc-400 text-sm py-2">
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
<Loader2 size={14} className="animate-spin text-indigo-400" />
|
||||
{toolStatus || '思考中...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Scroll to bottom FAB */}
|
||||
{showScrollBtn && (
|
||||
<button
|
||||
onClick={() => {
|
||||
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
|
||||
userScrolledUp.current = false;
|
||||
setShowScrollBtn(false);
|
||||
}}
|
||||
className="absolute bottom-4 left-1/2 -translate-x-1/2 w-9 h-9 rounded-full bg-white border border-zinc-200 shadow-lg shadow-zinc-200/60 flex items-center justify-center text-zinc-400 hover:text-zinc-700 hover:border-zinc-300 hover:shadow-xl transition-all"
|
||||
>
|
||||
<ArrowDown size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Quote bar */}
|
||||
{quote && (
|
||||
<div className="mx-4 px-3 py-1.5 bg-zinc-50 border border-zinc-200 rounded-lg flex items-center gap-2 text-xs text-zinc-500 flex-shrink-0">
|
||||
<div className="mx-4 px-3 py-1.5 bg-indigo-50/60 border border-indigo-100/50 rounded-xl flex items-center gap-2 text-xs text-zinc-500 flex-shrink-0">
|
||||
<span className="flex-1 truncate">引用: {quote}</span>
|
||||
<button onClick={() => setQuote(null)} className="text-zinc-400 hover:text-zinc-600"><X size={12} /></button>
|
||||
</div>
|
||||
|
||||
@@ -18,24 +18,24 @@ export function PipelineProgress({ phase, progress, currentItem, totalItems, sta
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-lg p-3 my-2">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-xs text-zinc-400">
|
||||
<div className="bg-gradient-to-br from-zinc-900 to-zinc-800 border border-zinc-700/50 rounded-xl p-3.5 my-2 shadow-lg shadow-zinc-900/20">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-zinc-400 font-medium">
|
||||
{phaseLabel[phase] || phase}
|
||||
{currentItem && totalItems ? ` (${currentItem}/${totalItems})` : ''}
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500">{progress}%</span>
|
||||
<span className="text-xs font-mono text-zinc-500">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div className="w-full h-1.5 bg-zinc-700/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-500',
|
||||
progress < 100 ? 'bg-blue-500' : 'bg-green-500'
|
||||
'h-full rounded-full transition-all duration-700 ease-out',
|
||||
progress < 100 ? 'bg-gradient-to-r from-blue-500 to-indigo-400' : 'bg-gradient-to-r from-emerald-500 to-green-400'
|
||||
)}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
{status && <p className="text-xs text-zinc-500 mt-1">{status}</p>}
|
||||
{status && <p className="text-xs text-zinc-500 mt-1.5">{status}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user