小优化,产品愿景,视觉规范,交互和导航大版本

This commit is contained in:
Nostars Developer
2026-04-16 18:11:35 +08:00
parent 5878f7d9f4
commit 10f9c0061a
51 changed files with 5010 additions and 847 deletions

View File

@@ -1,14 +1,17 @@
"use client";
import { useCallback, useEffect, useRef, useState, type DragEvent } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChatMessages } from "@/components/chat/chat-messages";
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";
import { AmbientParticles } from "@/components/ui/ambient-particles";
import { useApp } from "@/lib/app-context";
import { sendChat, getImageUrl, uploadRefImage } from "@/lib/api";
import { generateId } from "@/lib/store";
import { scrollBtnEnter, scrollBtnClick, staggerContainer, staggerItem } from "@/components/ui/motion-presets";
import type { ChatMessage, ImageAsset, ApiMessage, AnnotationData } from "@/lib/types";
export default function Home() {
@@ -29,6 +32,7 @@ export default function Home() {
const [streamingText, setStreamingText] = useState("");
const [streamingImages, setStreamingImages] = useState<ImageAsset[]>([]);
const [statusText, setStatusText] = useState("");
const [isGeneratingImage, setIsGeneratingImage] = useState(false);
const [pendingAnnotation, setPendingAnnotation] = useState<AnnotationData | null>(null);
const [lastRefImageUrls, setLastRefImageUrls] = useState<string[]>([]);
const lastRefPerSession = useRef<Map<string, string[]>>(new Map());
@@ -57,7 +61,6 @@ export default function Home() {
}
}, []);
// 实时记录当前会话的滚动位置 + 判断是否显示"回到底部"按钮
useEffect(() => {
const el = scrollRef.current;
if (!el || !activeSessionId) return;
@@ -72,7 +75,6 @@ export default function Home() {
return () => el.removeEventListener("scroll", handler);
}, [activeSessionId]);
// 会话切换:标记 switching等 DOM 更新后恢复位置 + 恢复参考图状态
useEffect(() => {
if (!activeSessionId) return;
if (prevSessionId.current && prevSessionId.current !== activeSessionId) {
@@ -152,6 +154,7 @@ export default function Home() {
};
appendMessage(activeSessionId, userMessage);
setIsLoading(true);
setIsGeneratingImage(false);
setStreamingText("");
setStreamingImages([]);
setStatusText("");
@@ -179,6 +182,7 @@ export default function Home() {
case "tool_start":
setStatusText(event.data.message as string);
setIsGeneratingImage(true);
scrollToBottom();
break;
@@ -207,6 +211,7 @@ export default function Home() {
collectedImages = [...collectedImages, ...newAssets];
setStreamingImages([...collectedImages]);
setIsGeneratingImage(false);
if (modelName) {
usedModelName = modelName;
setStatusText(`${modelName} 生成`);
@@ -225,6 +230,7 @@ export default function Home() {
assistantText += `\n\n⚠ 图片生成失败${modelHint}:\n${errorDetail}`;
setStreamingText(assistantText);
setStatusText("");
setIsGeneratingImage(false);
scrollToBottom();
break;
}
@@ -256,6 +262,7 @@ export default function Home() {
};
appendMessage(activeSessionId, assistantMessage);
setIsLoading(false);
setIsGeneratingImage(false);
setStreamingText("");
setStreamingImages([]);
setStatusText("");
@@ -268,8 +275,13 @@ export default function Home() {
setPendingAnnotation(data);
}, []);
const isEmptyState = messages.length === 0 && !isLoading;
return (
<div className="h-screen flex flex-col relative z-[1]">
{/* 环境粒子(萤火 + 云雾) */}
<AmbientParticles count={isEmptyState ? 28 : 18} />
<TopNav />
<div className="flex-1 flex overflow-hidden">
@@ -323,7 +335,7 @@ export default function Home() {
border border-[var(--border)]
text-[var(--text-secondary)] hover:text-[var(--accent)]
hover:border-[var(--accent)]/40
transition-all cursor-pointer
transition-all cursor-pointer btn-hover-lift
hidden md:flex"
title="展开侧边栏"
>
@@ -341,7 +353,7 @@ export default function Home() {
src={pendingAnnotation.snapshot}
alt="标注预览"
className="w-10 h-10 rounded-lg object-cover border border-[var(--accent)]/40
shadow-[0_0_8px_rgba(77,184,164,0.12)]"
shadow-[0_0_8px_rgba(46,139,122,0.12)]"
/>
<span className="text-xs text-[var(--accent)]">
{pendingAnnotation.annotations.length}
@@ -356,83 +368,103 @@ export default function Home() {
</div>
)}
<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-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 ref={scrollRef} className="flex-1 overflow-y-auto fog-scroll relative">
<div className="min-h-full">
{isEmptyState ? (
<div className="h-full flex items-center justify-center" style={{ minHeight: "calc(100vh - 200px)" }}>
<motion.div
className="text-center space-y-5"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
{/* Logo 发光效果 */}
<motion.div variants={staggerItem} 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(46,139,122,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>
</motion.div>
<motion.div variants={staggerItem}>
<h2 className="typo-display mb-2">
使 EPEEKit
</h2>
<p className="text-sm text-[var(--text-secondary)] max-w-md leading-relaxed mx-auto">
<br />
</p>
</motion.div>
<motion.div variants={staggerItem} className="flex flex-wrap justify-center gap-2 pt-2">
{[
"画一个赛博朋克风格的退出按钮",
"设计一个卡通风格的金币图标",
"画一个奇幻风格的游戏角色立绘",
].map((hint) => (
<button
key={hint}
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)]/50 hover:text-[var(--accent)]
hover:bg-[var(--accent)]/5
transition-all cursor-pointer btn-hover-lift"
>
{hint}
</button>
))}
</motion.div>
</motion.div>
</div>
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
使 EPEEKit
</h2>
<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-5">
{[
"画一个赛博朋克风格的退出按钮",
"设计一个卡通风格的金币图标",
"画一个奇幻风格的游戏角色立绘",
].map((hint) => (
<button
key={hint}
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)]/50 hover:text-[var(--accent)]
hover:bg-[var(--accent)]/5
transition-all cursor-pointer"
>
{hint}
</button>
))}
</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}
/>
)}
</div>
{showScrollBtn && (
<button
onClick={() => scrollToBottom()}
className="absolute bottom-16 right-4 z-10
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="回到底部"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
)}
{/* 回到底部 — 落叶归根(水滴形) */}
<AnimatePresence>
{showScrollBtn && (
<motion.button
key="scroll-btn"
variants={scrollBtnEnter}
initial="hidden"
animate="visible"
exit="exit"
onClick={() => {
scrollToBottom();
}}
className="absolute bottom-16 right-4 z-10
w-9 h-11 flex items-center justify-center
surface-2 text-[var(--text-secondary)]
hover:text-[var(--accent)] hover:border-[var(--accent)]/40
shadow-lg shadow-black/30
cursor-pointer"
style={{ borderRadius: "50% 50% 50% 50% / 35% 35% 65% 65%" }}
title="回到底部"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="mt-1">
<polyline points="6 9 12 15 18 9" />
</svg>
</motion.button>
)}
</AnimatePresence>
<ChatInput
ref={chatInputRef}
onSend={handleSend}
disabled={isLoading}
isGeneratingImage={isGeneratingImage}
lastRefServerUrls={lastRefImageUrls}
lastRefPreviewUrls={lastRefImageUrls.map((u) => getImageUrl(u))}
onClearLastRefImage={() => {