2026-05-07 03:48:14 +08:00
|
|
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
|
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
2026-05-07 02:41:01 +08:00
|
|
|
import { useAppStore } from '@/store';
|
|
|
|
|
import { useChat } from '@/hooks/useChat';
|
|
|
|
|
import { ChatMessage } from './ChatMessage';
|
|
|
|
|
import { ChatInput } from './ChatInput';
|
|
|
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
2026-05-07 02:46:16 +08:00
|
|
|
import { Button } from '@/components/ui/button';
|
2026-05-07 03:22:15 +08:00
|
|
|
import { RefreshCw, Loader2 } from 'lucide-react';
|
2026-05-07 03:48:14 +08:00
|
|
|
import { api } from '@/lib/api';
|
|
|
|
|
import type { Account } from '@/types';
|
2026-05-07 02:41:01 +08:00
|
|
|
|
2026-05-07 02:24:43 +08:00
|
|
|
export function ChatView() {
|
2026-05-07 03:48:14 +08:00
|
|
|
const { conversationId } = useParams<{ conversationId?: string }>();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { setConversations, selectedAccountId, setSelectedAccountId } = useAppStore();
|
|
|
|
|
const { messages, connected, thinking, toolStatus, send, createConversation } = useChat(conversationId || null);
|
2026-05-07 02:46:16 +08:00
|
|
|
const [manifestPath, setManifestPath] = useState<string | null>(null);
|
2026-05-07 03:48:14 +08:00
|
|
|
const [accounts, setAccounts] = useState<Account[]>([]);
|
|
|
|
|
const creatingRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
api.listAccounts().then(setAccounts).catch(() => {});
|
|
|
|
|
}, []);
|
2026-05-07 02:41:01 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetch('/api/pipeline/conversations')
|
|
|
|
|
.then((r) => r.json())
|
|
|
|
|
.then(setConversations)
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}, [messages]);
|
|
|
|
|
|
2026-05-07 02:46:16 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
const toolMsgs = messages.filter((m) => m.role === 'tool');
|
|
|
|
|
if (toolMsgs.length > 0) {
|
|
|
|
|
try {
|
|
|
|
|
const lastTool = JSON.parse(toolMsgs[toolMsgs.length - 1].content);
|
|
|
|
|
if (lastTool.manifest) setManifestPath(lastTool.manifest);
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
}, [messages]);
|
|
|
|
|
|
2026-05-07 03:48:14 +08:00
|
|
|
// 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;
|
|
|
|
|
|
2026-05-07 02:46:16 +08:00
|
|
|
createConversation('新对话', selectedAccountId || undefined);
|
2026-05-07 02:41:01 +08:00
|
|
|
setTimeout(() => {
|
|
|
|
|
fetch('/api/pipeline/conversations')
|
|
|
|
|
.then((r) => r.json())
|
2026-05-07 03:48:14 +08:00
|
|
|
.then((list) => {
|
|
|
|
|
setConversations(list);
|
|
|
|
|
if (list.length > 0) {
|
|
|
|
|
navigate(`/chat/${list[0].id}`);
|
|
|
|
|
}
|
|
|
|
|
creatingRef.current = false;
|
|
|
|
|
});
|
|
|
|
|
}, 500);
|
|
|
|
|
}, [createConversation, selectedAccountId, setConversations, navigate]);
|
2026-05-07 02:41:01 +08:00
|
|
|
|
2026-05-07 02:46:16 +08:00
|
|
|
const handleResume = async () => {
|
|
|
|
|
if (!manifestPath) return;
|
|
|
|
|
try {
|
|
|
|
|
await fetch('/api/pipeline/resume', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ manifest: manifestPath }),
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Resume failed:', e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-07 03:48:14 +08:00
|
|
|
if (!conversationId) {
|
2026-05-07 02:41:01 +08:00
|
|
|
return (
|
2026-05-07 03:48:14 +08:00
|
|
|
<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>
|
2026-05-07 02:41:01 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-07 03:48:14 +08:00
|
|
|
<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>
|
2026-05-07 02:46:16 +08:00
|
|
|
</div>
|
|
|
|
|
{manifestPath && (
|
|
|
|
|
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={handleResume}>
|
|
|
|
|
<RefreshCw size={12} className="mr-1" />
|
|
|
|
|
断点续跑
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-05-07 02:41:01 +08:00
|
|
|
</div>
|
2026-05-07 03:22:15 +08:00
|
|
|
|
2026-05-07 02:41:01 +08:00
|
|
|
<ScrollArea className="flex-1 px-4 py-4">
|
|
|
|
|
{messages.map((msg) => (
|
|
|
|
|
<ChatMessage key={msg.id} message={msg} />
|
|
|
|
|
))}
|
2026-05-07 03:22:15 +08:00
|
|
|
|
|
|
|
|
{thinking && (
|
2026-05-07 03:48:14 +08:00
|
|
|
<div className="flex items-center gap-2 text-zinc-400 text-sm py-2">
|
2026-05-07 03:22:15 +08:00
|
|
|
<Loader2 size={14} className="animate-spin" />
|
|
|
|
|
{toolStatus || '思考中...'}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-07 02:41:01 +08:00
|
|
|
</ScrollArea>
|
2026-05-07 03:22:15 +08:00
|
|
|
|
|
|
|
|
<ChatInput onSend={send} disabled={thinking} />
|
2026-05-07 02:41:01 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-05-07 02:24:43 +08:00
|
|
|
}
|