refactor(web): router-based navigation, light theme, form config

- Replace Zustand activeView with React Router (NavLink + Routes)
- White/light modern theme with indigo accents
- Sidebar with Chinese labels under icons
- ConfigForm with individual form fields (no JSON textareas)
- Account switching with context injection into chat
- Fix duplicate conversation creation with useRef guard
- Asset gallery: smaller 6-column grid with date labels
- All components updated to light color scheme

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 03:48:14 +08:00
parent 2859fa3f2c
commit 348cc0c5b9
22 changed files with 506 additions and 220 deletions

View File

@@ -21,8 +21,8 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string) => v
};
return (
<div className="p-4 border-t border-zinc-800">
<div className="flex items-end gap-2 bg-zinc-900 rounded-lg border border-zinc-800 px-3 py-2">
<div className="p-4 border-t border-zinc-200 bg-white">
<div className="flex items-end gap-2 bg-zinc-50 rounded-xl border border-zinc-200 px-4 py-2.5 focus-within:border-indigo-300 transition-colors">
<textarea
ref={ref}
value={input}
@@ -30,11 +30,11 @@ export function ChatInput({ onSend, disabled }: { onSend: (content: string) => v
onKeyDown={handleKeyDown}
rows={1}
placeholder={disabled ? '等待回复中...' : '输入指令...'}
className="flex-1 bg-transparent text-sm resize-none outline-none placeholder:text-zinc-600"
className="flex-1 bg-transparent text-sm resize-none outline-none placeholder:text-zinc-400 text-zinc-700"
disabled={disabled}
/>
<Button size="icon" variant="ghost" className="h-8 w-8" onClick={handleSend} disabled={disabled}>
<Send size={14} />
<Button size="icon" variant="ghost" className="h-8 w-8 text-zinc-400 hover:text-indigo-600" onClick={handleSend} disabled={disabled}>
<Send size={16} />
</Button>
</div>
</div>

View File

@@ -10,14 +10,14 @@ export function ChatMessage({ message }: { message: Message }) {
<div className={cn('mb-4 flex', isUser ? 'justify-end' : 'justify-start')}>
<div
className={cn(
'max-w-[80%] rounded-lg px-4 py-2.5',
'max-w-[80%] rounded-xl px-4 py-2.5',
isUser
? 'bg-zinc-800 text-zinc-100'
: 'bg-zinc-900 text-zinc-300 border border-zinc-800'
? 'bg-indigo-600 text-white'
: 'bg-white text-zinc-700 border border-zinc-200 shadow-sm'
)}
>
{isEmpty ? (
<span className="text-zinc-600 italic text-xs">...</span>
<span className="text-zinc-400 italic text-xs">...</span>
) : isUser ? (
<p className="text-sm leading-relaxed whitespace-pre-wrap">{message.content}</p>
) : (

View File

@@ -1,17 +1,27 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAppStore } from '@/store';
import { useChat } from '@/hooks/useChat';
import { ChatMessage } from './ChatMessage';
import { ChatInput } from './ChatInput';
import { PipelineProgress } from './PipelineProgress';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import { RefreshCw, Loader2 } from 'lucide-react';
import { api } from '@/lib/api';
import type { Account } from '@/types';
export function ChatView() {
const { activeConversationId, setConversations, selectedAccountId } = useAppStore();
const { messages, connected, thinking, toolStatus, send, createConversation } = useChat(activeConversationId);
const { conversationId } = useParams<{ conversationId?: string }>();
const navigate = useNavigate();
const { setConversations, selectedAccountId, setSelectedAccountId } = useAppStore();
const { messages, connected, thinking, toolStatus, send, createConversation } = useChat(conversationId || null);
const [manifestPath, setManifestPath] = useState<string | null>(null);
const [accounts, setAccounts] = useState<Account[]>([]);
const creatingRef = useRef(false);
useEffect(() => {
api.listAccounts().then(setAccounts).catch(() => {});
}, []);
useEffect(() => {
fetch('/api/pipeline/conversations')
@@ -30,14 +40,34 @@ export function ChatView() {
}
}, [messages]);
const handleNewConversation = () => {
// Inject account context when account is selected
useEffect(() => {
if (selectedAccountId && conversationId && connected && messages.length === 0) {
const account = accounts.find((a) => a.id === selectedAccountId);
if (account) {
const ctx = `已选择账号「${account.name}」:${account.description || ''}。生图:${account.imageModel} 视频:${account.videoModel} 画幅:${account.defaultFormat}`;
send(ctx);
}
}
}, [selectedAccountId, conversationId, connected]);
const handleNewConversation = useCallback(() => {
if (creatingRef.current) return;
creatingRef.current = true;
createConversation('新对话', selectedAccountId || undefined);
setTimeout(() => {
fetch('/api/pipeline/conversations')
.then((r) => r.json())
.then(setConversations);
}, 300);
};
.then((list) => {
setConversations(list);
if (list.length > 0) {
navigate(`/chat/${list[0].id}`);
}
creatingRef.current = false;
});
}, 500);
}, [createConversation, selectedAccountId, setConversations, navigate]);
const handleResume = async () => {
if (!manifestPath) return;
@@ -52,26 +82,57 @@ export function ChatView() {
}
};
if (!activeConversationId) {
if (!conversationId) {
return (
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-zinc-500">
<p></p>
<button
onClick={handleNewConversation}
className="px-4 py-2 rounded-md bg-zinc-800 text-sm hover:bg-zinc-700 transition-colors"
>
</button>
<div className="flex-1 flex flex-col items-center justify-center gap-4">
<div className="text-4xl mb-2">💬</div>
<h2 className="text-lg font-semibold text-zinc-800"></h2>
<p className="text-sm text-zinc-500"></p>
<div className="flex gap-2 mt-2">
{accounts.length > 0 && (
<select
value={selectedAccountId || ''}
onChange={(e) => setSelectedAccountId(e.target.value || null)}
className="h-9 rounded-md border border-zinc-200 bg-white px-3 text-sm text-zinc-700"
>
<option value=""></option>
{accounts.map((a) => (
<option key={a.id} value={a.id}>{a.name}</option>
))}
</select>
)}
<button
onClick={handleNewConversation}
disabled={creatingRef.current}
className="px-4 py-2 rounded-lg bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
{creatingRef.current ? '创建中...' : '开始对话'}
</button>
</div>
</div>
);
}
return (
<div className="flex-1 flex flex-col">
<div className="px-4 py-2 border-b border-zinc-800 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-xs text-zinc-500">{connected ? '已连接' : '连接中...'}</span>
<div className="flex-1 flex flex-col bg-white">
<div className="px-4 py-2 border-b border-zinc-200 flex items-center justify-between">
<div className="flex items-center gap-3">
<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'}`} />
<span className="text-xs text-zinc-400">{connected ? '在线' : '连接中'}</span>
</div>
<select
value={selectedAccountId || ''}
onChange={(e) => {
setSelectedAccountId(e.target.value || null);
}}
className="h-7 rounded border border-zinc-200 bg-zinc-50 px-2 text-xs text-zinc-600"
>
<option value=""></option>
{accounts.map((a) => (
<option key={a.id} value={a.id}>{a.name}</option>
))}
</select>
</div>
{manifestPath && (
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={handleResume}>
@@ -87,7 +148,7 @@ export function ChatView() {
))}
{thinking && (
<div className="flex items-center gap-2 text-zinc-500 text-sm py-2">
<div className="flex items-center gap-2 text-zinc-400 text-sm py-2">
<Loader2 size={14} className="animate-spin" />
{toolStatus || '思考中...'}
</div>