kit初版,模型引入,agent优化

This commit is contained in:
2026-04-13 00:41:23 +08:00
parent 9b053e302b
commit c435ab15cf
14097 changed files with 5032 additions and 2676248 deletions

View File

@@ -3,14 +3,31 @@
import { useCallback, useRef, useState } from "react";
import { ChatMessages } from "@/components/chat/chat-messages";
import { ChatInput } from "@/components/chat/chat-input";
import { sendChat, type Message } from "@/lib/api";
import { Sidebar } from "@/components/sidebar/sidebar";
import { TopNav } from "@/components/layout/top-nav";
import { ImageDetailPanel } from "@/components/detail/image-detail-panel";
import { useApp } from "@/lib/app-context";
import { sendChat, getImageUrl, uploadRefImage } from "@/lib/api";
import { generateId } from "@/lib/store";
import type { ChatMessage, ImageAsset, ApiMessage, AnnotationData } from "@/lib/types";
export default function Home() {
const [messages, setMessages] = useState<Message[]>([]);
const {
activeSession,
activeSessionId,
appendMessage,
addAsset,
updateSessionThumbnail,
detailImage,
sidebarCollapsed,
setSidebarCollapsed,
} = useApp();
const [isLoading, setIsLoading] = useState(false);
const [streamingText, setStreamingText] = useState("");
const [streamingImages, setStreamingImages] = useState<string[]>([]);
const [streamingImages, setStreamingImages] = useState<ImageAsset[]>([]);
const [statusText, setStatusText] = useState("");
const [pendingAnnotation, setPendingAnnotation] = useState<AnnotationData | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
@@ -23,22 +40,73 @@ export default function Home() {
};
const handleSend = useCallback(
async (text: string, refImage: File | null) => {
// 添加用户消息
const userMessage: Message = { role: "user", content: text };
const updatedMessages = [...messages, userMessage];
setMessages(updatedMessages);
async (text: string, refImageServerUrl: string | null, imageModel: string | null = null) => {
if (!activeSessionId || !activeSession) return;
let finalText = text;
let finalRefServerUrl = refImageServerUrl;
if (pendingAnnotation) {
const annotationDescs = pendingAnnotation.annotations
.filter((a) => a.text)
.map((a) => {
if (a.type === "rect") {
return `[区域 (${Math.round(a.x * 100)}%, ${Math.round(a.y * 100)}%) 大小 ${Math.round((a.w ?? 0) * 100)}%×${Math.round((a.h ?? 0) * 100)}%]: ${a.text}`;
}
return `[标注]: ${a.text}`;
})
.join("\n");
if (annotationDescs) {
finalText = `${text}\n\n--- 图片标注 ---\n${annotationDescs}`;
}
// 标注截图作为参考图:需要先上传再获取 URL
if (pendingAnnotation.snapshot && !refImageServerUrl) {
try {
const res = await fetch(pendingAnnotation.snapshot);
const blob = await res.blob();
const file = new File([blob], "annotation.png", { type: "image/png" });
const uploadResult = await uploadRefImage(file);
finalRefServerUrl = uploadResult.url;
} catch {
// 忽略转换/上传失败
}
}
setPendingAnnotation(null);
}
// 用户消息中的参考图预览:优先使用服务端路径(通过 getImageUrl 转为完整 URL
let previewUrl: string | undefined;
if (finalRefServerUrl) {
previewUrl = getImageUrl(finalRefServerUrl);
}
const userMessage: ChatMessage = {
id: generateId("msg-"),
role: "user",
content: finalText,
refImageUrl: previewUrl,
};
appendMessage(activeSessionId, userMessage);
setIsLoading(true);
setStreamingText("");
setStreamingImages([]);
setStatusText("");
scrollToBottom();
const apiMessages: ApiMessage[] = [
...activeSession.messages.map((m) => ({ role: m.role, content: m.content })),
{ role: userMessage.role, content: userMessage.content },
];
let assistantText = "";
let collectedImages: string[] = [];
let collectedImages: ImageAsset[] = [];
let usedModelName = "";
try {
for await (const event of sendChat(updatedMessages, refImage)) {
for await (const event of sendChat(apiMessages, finalRefServerUrl, imageModel)) {
switch (event.type) {
case "text_delta":
assistantText += event.data.text as string;
@@ -51,15 +119,52 @@ export default function Home() {
scrollToBottom();
break;
case "image_result":
collectedImages = [
...collectedImages,
...(event.data.images as string[]),
];
setStreamingImages(collectedImages);
case "image_result": {
const rawUrls = (event.data.images as string[]) || [];
const newUrls = rawUrls.filter((u) => u && !u.startsWith("["));
const prompt = (event.data.prompt_used as string) || "";
const modelName = (event.data.model_name as string) || "";
if (newUrls.length === 0) break;
const newAssets: ImageAsset[] = newUrls.map((url) => ({
id: generateId("img-"),
url,
prompt,
sessionId: activeSessionId,
tags: activeSession.tags ? [...activeSession.tags] : [],
favorited: false,
createdAt: Date.now(),
}));
newAssets.forEach((a) => addAsset(a));
if (newAssets.length > 0) {
updateSessionThumbnail(activeSessionId, newAssets[0].url);
}
collectedImages = [...collectedImages, ...newAssets];
setStreamingImages([...collectedImages]);
if (modelName) {
usedModelName = modelName;
setStatusText(`${modelName} 生成`);
} else {
setStatusText("");
}
scrollToBottom();
break;
}
case "tool_error": {
const errors = (event.data.errors as string[]) || [];
const modelName = (event.data.model_name as string) || "";
const errorDetail = errors.join("\n");
const modelHint = modelName ? ` (${modelName})` : "";
assistantText += `\n\n⚠ 图片生成失败${modelHint}:\n${errorDetail}`;
setStreamingText(assistantText);
setStatusText("");
scrollToBottom();
break;
}
case "error":
assistantText += `\n\n[错误: ${event.data.message}]`;
@@ -74,79 +179,127 @@ export default function Home() {
assistantText += `\n\n[请求失败: ${e instanceof Error ? e.message : "未知错误"}]`;
}
// 完成:将流式内容合并为正式消息
const assistantMessage: Message = {
const assistantMessage: ChatMessage = {
id: generateId("msg-"),
role: "assistant",
content: assistantText || "(生成完成)",
images: collectedImages.length > 0 ? collectedImages : undefined,
modelName: usedModelName || undefined,
};
setMessages([...updatedMessages, assistantMessage]);
appendMessage(activeSessionId, assistantMessage);
setIsLoading(false);
setStreamingText("");
setStreamingImages([]);
setStatusText("");
scrollToBottom();
},
[messages]
[activeSession, activeSessionId, appendMessage, addAsset, updateSessionThumbnail, pendingAnnotation]
);
const handleAnnotationComplete = useCallback((data: AnnotationData) => {
setPendingAnnotation(data);
}, []);
const messages = activeSession?.messages ?? [];
return (
<div className="h-screen flex flex-col">
{/* 标题栏 */}
<header className="flex-shrink-0 border-b border-[var(--border)] bg-[var(--bg-secondary)] px-6 py-3">
<h1 className="text-lg font-semibold">Art Agent</h1>
<p className="text-xs text-[var(--text-secondary)]">
AI · MVP
</p>
</header>
<TopNav />
{/* 消息区域 */}
<div ref={scrollRef} className="flex-1 overflow-y-auto">
{messages.length === 0 && !isLoading ? (
<div className="h-full flex items-center justify-center">
<div className="text-center space-y-3">
<div className="text-4xl">🎨</div>
<h2 className="text-xl font-medium text-[var(--text-primary)]">
使 Art Agent
</h2>
<p className="text-sm text-[var(--text-secondary)] max-w-md">
<br />
</p>
<div className="flex flex-wrap justify-center gap-2 mt-4">
{[
"画一个赛博朋克风格的退出按钮",
"设计一个卡通风格的金币图标",
"画一个奇幻风格的游戏角色立绘",
].map((hint) => (
<button
key={hint}
onClick={() => handleSend(hint, null)}
className="text-xs px-3 py-1.5 rounded-full
border border-[var(--border)] text-[var(--text-secondary)]
hover:border-[var(--accent)] hover:text-[var(--accent)]
transition-colors cursor-pointer"
>
{hint}
</button>
))}
</div>
<div className="flex-1 flex overflow-hidden">
<Sidebar />
<main className="flex-1 flex flex-col min-w-0 relative">
{/* 桌面端侧边栏展开按钮(移动端用顶栏汉堡菜单替代) */}
{sidebarCollapsed && (
<button
onClick={() => setSidebarCollapsed(false)}
className="absolute left-1 top-2 z-10 p-1.5 rounded-md
bg-[var(--bg-secondary)] border border-[var(--border)]
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
transition-colors cursor-pointer
hidden md:flex"
title="展开侧边栏"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M13 17l5-5-5-5M6 17l5-5-5-5" />
</svg>
</button>
)}
{/* 标注提示条 */}
{pendingAnnotation && (
<div className="flex-shrink-0 px-4 py-2 bg-[var(--accent)]/10 border-b border-[var(--accent)]/30
flex items-center gap-3">
<img
src={pendingAnnotation.snapshot}
alt="标注预览"
className="w-10 h-10 rounded object-cover border border-[var(--accent)]"
/>
<span className="text-xs text-[var(--accent)]">
{pendingAnnotation.annotations.length}
</span>
<button
onClick={() => setPendingAnnotation(null)}
className="ml-auto text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]
cursor-pointer"
>
</button>
</div>
)}
<div ref={scrollRef} className="flex-1 overflow-y-auto">
{messages.length === 0 && !isLoading ? (
<div className="h-full flex items-center justify-center">
<div className="text-center space-y-3">
<div className="text-4xl">🎨</div>
<h2 className="text-xl font-medium text-[var(--text-primary)]">
使 EPEEKit
</h2>
<p className="text-sm text-[var(--text-secondary)] max-w-md">
<br />
</p>
<div className="flex flex-wrap justify-center gap-2 mt-4">
{[
"画一个赛博朋克风格的退出按钮",
"设计一个卡通风格的金币图标",
"画一个奇幻风格的游戏角色立绘",
].map((hint) => (
<button
key={hint}
onClick={() => handleSend(hint, null, null)}
className="text-xs px-3 py-1.5 rounded-full
border border-[var(--border)] text-[var(--text-secondary)]
hover:border-[var(--accent)] hover:text-[var(--accent)]
transition-colors cursor-pointer"
>
{hint}
</button>
))}
</div>
</div>
</div>
) : (
<ChatMessages
messages={messages}
isLoading={isLoading}
streamingText={streamingText}
streamingImages={streamingImages}
statusText={statusText}
/>
)}
</div>
) : (
<ChatMessages
messages={messages}
isLoading={isLoading}
streamingText={streamingText}
streamingImages={streamingImages}
statusText={statusText}
/>
<ChatInput onSend={handleSend} disabled={isLoading} />
</main>
{detailImage && (
<ImageDetailPanel onAnnotationComplete={handleAnnotationComplete} />
)}
</div>
{/* 输入区域 */}
<ChatInput onSend={handleSend} disabled={isLoading} />
</div>
);
}