加了一堆模型和一堆功能

This commit is contained in:
2026-04-16 00:36:21 +08:00
parent 47c0863bab
commit 40de992516
34 changed files with 3111 additions and 718 deletions

View File

@@ -1,8 +1,8 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
import { ChatMessages } from "@/components/chat/chat-messages";
import { ChatInput } from "@/components/chat/chat-input";
import { ChatInput, type ChatInputHandle } from "@/components/chat/chat-input";
import { Sidebar } from "@/components/sidebar/sidebar";
import { TopNav } from "@/components/layout/top-nav";
import { ImageDetailPanel } from "@/components/detail/image-detail-panel";
@@ -30,6 +30,11 @@ export default function Home() {
const [streamingImages, setStreamingImages] = useState<ImageAsset[]>([]);
const [statusText, setStatusText] = useState("");
const [pendingAnnotation, setPendingAnnotation] = useState<AnnotationData | null>(null);
const [lastRefImageUrls, setLastRefImageUrls] = useState<string[]>([]);
const lastRefPerSession = useRef<Map<string, string[]>>(new Map());
const [mainDragging, setMainDragging] = useState(false);
const dragCounter = useRef(0);
const chatInputRef = useRef<ChatInputHandle>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollPositions = useRef<Map<string, number>>(new Map());
const prevSessionId = useRef<string | null>(null);
@@ -67,13 +72,14 @@ export default function Home() {
return () => el.removeEventListener("scroll", handler);
}, [activeSessionId]);
// 会话切换:标记 switching等 DOM 更新后恢复位置
// 会话切换:标记 switching等 DOM 更新后恢复位置 + 恢复参考图状态
useEffect(() => {
if (!activeSessionId) return;
if (prevSessionId.current && prevSessionId.current !== activeSessionId) {
isSwitching.current = true;
}
prevSessionId.current = activeSessionId;
setLastRefImageUrls(lastRefPerSession.current.get(activeSessionId) ?? []);
}, [activeSessionId]);
useEffect(() => {
@@ -95,11 +101,11 @@ export default function Home() {
}, [activeSessionId, messages.length]);
const handleSend = useCallback(
async (text: string, refImageServerUrl: string | null, imageModel: string | null = null) => {
async (text: string, refImageServerUrls: string[], imageModel: string | null = null) => {
if (!activeSessionId || !activeSession) return;
let finalText = text;
let finalRefServerUrl = refImageServerUrl;
let finalRefServerUrls = [...refImageServerUrls];
if (pendingAnnotation) {
const annotationDescs = pendingAnnotation.annotations
@@ -116,14 +122,13 @@ export default function Home() {
finalText = `${text}\n\n--- 图片标注 ---\n${annotationDescs}`;
}
// 标注截图作为参考图:需要先上传再获取 URL
if (pendingAnnotation.snapshot && !refImageServerUrl) {
if (pendingAnnotation.snapshot && finalRefServerUrls.length === 0) {
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;
finalRefServerUrls = [uploadResult.url];
} catch {
// 忽略转换/上传失败
}
@@ -132,17 +137,18 @@ export default function Home() {
setPendingAnnotation(null);
}
// 用户消息中的参考图预览:优先使用服务端路径(通过 getImageUrl 转为完整 URL
let previewUrl: string | undefined;
if (finalRefServerUrl) {
previewUrl = getImageUrl(finalRefServerUrl);
if (finalRefServerUrls.length > 0) {
lastRefPerSession.current.set(activeSessionId, finalRefServerUrls);
setLastRefImageUrls(finalRefServerUrls);
}
const previewUrls = finalRefServerUrls.map((u) => getImageUrl(u));
const userMessage: ChatMessage = {
id: generateId("msg-"),
role: "user",
content: finalText,
refImageUrl: previewUrl,
refImageUrls: previewUrls.length > 0 ? previewUrls : undefined,
};
appendMessage(activeSessionId, userMessage);
setIsLoading(true);
@@ -160,8 +166,10 @@ export default function Home() {
let collectedImages: ImageAsset[] = [];
let usedModelName = "";
const llmModel = activeSession?.llmModel ?? null;
try {
for await (const event of sendChat(apiMessages, finalRefServerUrl, imageModel, activeSessionId)) {
for await (const event of sendChat(apiMessages, finalRefServerUrls.length > 0 ? finalRefServerUrls : null, imageModel, activeSessionId, llmModel)) {
switch (event.type) {
case "text_delta":
assistantText += event.data.text as string;
@@ -261,21 +269,61 @@ export default function Home() {
}, []);
return (
<div className="h-screen flex flex-col">
<div className="h-screen flex flex-col relative z-[1]">
<TopNav />
<div className="flex-1 flex overflow-hidden">
<Sidebar />
<main className="flex-1 flex flex-col min-w-0 relative">
{/* 桌面端侧边栏展开按钮(移动端用顶栏汉堡菜单替代) */}
<main
className="flex-1 flex flex-col min-w-0 relative"
onDragEnter={(e: DragEvent) => {
e.preventDefault();
dragCounter.current++;
if (e.dataTransfer.types.includes("Files")) setMainDragging(true);
}}
onDragOver={(e: DragEvent) => e.preventDefault()}
onDragLeave={(e: DragEvent) => {
e.preventDefault();
dragCounter.current--;
if (dragCounter.current <= 0) { dragCounter.current = 0; setMainDragging(false); }
}}
onDrop={(e: DragEvent) => {
e.preventDefault();
dragCounter.current = 0;
setMainDragging(false);
const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith("image/"));
files.forEach((f) => chatInputRef.current?.uploadFile(f));
}}
>
{/* 拖拽覆盖层 */}
{mainDragging && (
<div className="absolute inset-0 z-50 flex items-center justify-center
bg-[var(--bg-primary)]/80 backdrop-blur-sm
border-2 border-dashed border-[var(--accent)]/50 rounded-xl m-2
pointer-events-none">
<div className="text-center space-y-2">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none"
stroke="var(--accent)" strokeWidth="1.5" className="mx-auto opacity-70">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
<p className="text-sm text-[var(--accent)] font-medium"></p>
</div>
</div>
)}
{/* 桌面端侧边栏展开按钮 */}
{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
className="absolute left-2 top-2 z-10 p-1.5 rounded-lg
bg-[var(--bg-secondary)]/80 backdrop-blur-sm
border border-[var(--border)]
text-[var(--text-secondary)] hover:text-[var(--accent)]
hover:border-[var(--accent)]/40
transition-all cursor-pointer
hidden md:flex"
title="展开侧边栏"
>
@@ -287,12 +335,13 @@ export default function Home() {
{/* 标注提示条 */}
{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">
<div className="flex-shrink-0 px-5 py-2.5 bg-[var(--accent)]/5 border-b border-[var(--accent)]/20
flex items-center gap-3 backdrop-blur-sm">
<img
src={pendingAnnotation.snapshot}
alt="标注预览"
className="w-10 h-10 rounded object-cover border border-[var(--accent)]"
className="w-10 h-10 rounded-lg object-cover border border-[var(--accent)]/40
shadow-[0_0_8px_rgba(77,184,164,0.12)]"
/>
<span className="text-xs text-[var(--accent)]">
{pendingAnnotation.annotations.length}
@@ -300,27 +349,37 @@ export default function Home() {
<button
onClick={() => setPendingAnnotation(null)}
className="ml-auto text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]
cursor-pointer"
cursor-pointer transition-colors"
>
</button>
</div>
)}
<div ref={scrollRef} className="flex-1 overflow-y-auto">
<div ref={scrollRef} className="flex-1 overflow-y-auto fog-scroll">
{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)]">
<div className="text-center space-y-4">
{/* Logo 发光效果 */}
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl
bg-[var(--accent)]/10 border border-[var(--accent)]/20
shadow-[0_0_30px_rgba(77,184,164,0.12)]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 22L12.5 10h2.2L19 22h-2.3l-1.1-3h-4.2l-1.1 3H8Zm3.7-5h3.1l-1.5-4.6h-.1L11.7 17Z" fill="var(--accent)" />
<circle cx="23" cy="12" r="3.5" stroke="var(--accent)" strokeWidth="1.8" fill="none" />
<path d="M23 15.5v5" stroke="var(--accent)" strokeWidth="1.8" strokeLinecap="round" />
<circle cx="23" cy="22.5" r="1" fill="var(--accent)" />
</svg>
</div>
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
使 EPEEKit
</h2>
<p className="text-sm text-[var(--text-secondary)] max-w-md">
<p className="text-sm text-[var(--text-secondary)] max-w-md leading-relaxed">
<br />
</p>
<div className="flex flex-wrap justify-center gap-2 mt-4">
<div className="flex flex-wrap justify-center gap-2 mt-5">
{[
"画一个赛博朋克风格的退出按钮",
"设计一个卡通风格的金币图标",
@@ -328,11 +387,12 @@ export default function Home() {
].map((hint) => (
<button
key={hint}
onClick={() => handleSend(hint, null, null)}
className="text-xs px-3 py-1.5 rounded-full
onClick={() => handleSend(hint, [], null)}
className="text-xs px-4 py-2 rounded-xl
border border-[var(--border)] text-[var(--text-secondary)]
hover:border-[var(--accent)] hover:text-[var(--accent)]
transition-colors cursor-pointer"
hover:border-[var(--accent)]/50 hover:text-[var(--accent)]
hover:bg-[var(--accent)]/5
transition-all cursor-pointer"
>
{hint}
</button>
@@ -355,10 +415,10 @@ export default function Home() {
<button
onClick={() => scrollToBottom()}
className="absolute bottom-16 right-4 z-10
w-8 h-8 rounded-full flex items-center justify-center
bg-[var(--bg-tertiary)] border border-[var(--border)]
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
hover:border-[var(--accent)] shadow-lg
w-9 h-9 rounded-xl flex items-center justify-center
glass-panel text-[var(--text-secondary)]
hover:text-[var(--accent)] hover:border-[var(--accent)]/40
shadow-lg shadow-black/30
transition-all duration-200 cursor-pointer
animate-[fadeIn_150ms_ease-out]"
title="回到底部"
@@ -369,7 +429,21 @@ export default function Home() {
</button>
)}
<ChatInput onSend={handleSend} disabled={isLoading} />
<ChatInput
ref={chatInputRef}
onSend={handleSend}
disabled={isLoading}
lastRefServerUrls={lastRefImageUrls}
lastRefPreviewUrls={lastRefImageUrls.map((u) => getImageUrl(u))}
onClearLastRefImage={() => {
if (activeSessionId) lastRefPerSession.current.delete(activeSessionId);
setLastRefImageUrls([]);
}}
onFileDrop={() => {
dragCounter.current = 0;
setMainDragging(false);
}}
/>
</main>
{detailImage && (