加了一堆模型和一堆功能
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user