小优化,产品愿景,视觉规范,交互和导航大版本
This commit is contained in:
@@ -1,8 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import type { ChatMessage, ImageAsset } from "@/lib/types";
|
||||
import { ImageGrid } from "./image-grid";
|
||||
import { messageEnter, assistantEnter } from "@/components/ui/motion-presets";
|
||||
|
||||
function StreamingLines({ text }: { text: string }) {
|
||||
const lines = useMemo(() => text.split("\n"), [text]);
|
||||
const total = lines.length;
|
||||
return (
|
||||
<div className="whitespace-pre-wrap text-sm leading-relaxed">
|
||||
{lines.map((line, i) => {
|
||||
const fromEnd = total - 1 - i;
|
||||
const opacity = fromEnd <= 0 ? 1 : fromEnd === 1 ? 0.85 : fromEnd === 2 ? 0.7 : 0.6;
|
||||
return (
|
||||
<span key={i} style={{ opacity, transition: "opacity 0.3s ease" }}>
|
||||
{line}
|
||||
{i < total - 1 && "\n"}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -21,7 +42,7 @@ function CopyButton({ text }: { text: string }) {
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="p-1 rounded-md text-[var(--text-secondary)] hover:text-[var(--accent)]
|
||||
hover:bg-[var(--accent)]/5 transition-colors cursor-pointer"
|
||||
hover:bg-[var(--accent)]/5 transition-colors cursor-pointer btn-hover-lift"
|
||||
title="复制文本"
|
||||
>
|
||||
{copied ? (
|
||||
@@ -56,17 +77,19 @@ export function ChatMessages({
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto px-3 md:px-5 py-4 md:py-6 space-y-4 md:space-y-5">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
<motion.div
|
||||
key={msg.id}
|
||||
variants={msg.role === "user" ? messageEnter : assistantEnter}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className={`group/msg flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
|
||||
style={{ animation: "slideUp 200ms ease-out" }}
|
||||
>
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<div
|
||||
className={`max-w-[90vw] md:max-w-[80vw] rounded-2xl px-4 md:px-5 py-3 ${
|
||||
className={`max-w-[90vw] md:max-w-[80vw] px-4 md:px-5 py-3 ${
|
||||
msg.role === "user"
|
||||
? "bg-[var(--accent)]/15 text-[var(--text-primary)] border border-[var(--accent)]/25"
|
||||
: "glass-panel text-[var(--text-primary)]"
|
||||
? "user-bubble bg-gradient-to-r from-[var(--accent)]/8 to-[var(--accent)]/15 text-[var(--text-primary)]"
|
||||
: "rounded-2xl surface-2 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]/40"
|
||||
}`}
|
||||
>
|
||||
{/* 多图参考(新格式) */}
|
||||
@@ -80,11 +103,11 @@ export function ChatMessages({
|
||||
alt={`参考图 ${i + 1}`}
|
||||
className="max-w-[120px] max-h-[90px] rounded-lg object-cover
|
||||
border border-[var(--accent)]/20
|
||||
shadow-[0_0_10px_rgba(77,184,164,0.08)]"
|
||||
shadow-[0_0_10px_rgba(46,139,122,0.08)]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--accent)]/60 mt-1 block">
|
||||
<span className="text-[10px] text-[var(--accent)]/60 mt-1 block typo-micro" style={{ textTransform: "none" }}>
|
||||
参考图({msg.refImageUrls.length} 张)
|
||||
</span>
|
||||
</div>
|
||||
@@ -97,7 +120,7 @@ export function ChatMessages({
|
||||
alt="参考图"
|
||||
className="max-w-[160px] max-h-[120px] rounded-lg object-cover
|
||||
border border-[var(--accent)]/20
|
||||
shadow-[0_0_10px_rgba(77,184,164,0.08)]"
|
||||
shadow-[0_0_10px_rgba(46,139,122,0.08)]"
|
||||
/>
|
||||
<span className="text-[10px] text-[var(--accent)]/60 mt-1 block">参考图</span>
|
||||
</div>
|
||||
@@ -107,7 +130,7 @@ export function ChatMessages({
|
||||
<>
|
||||
<ImageGrid images={msg.images} />
|
||||
{msg.modelName && (
|
||||
<div className="mt-1.5 text-[10px] text-[var(--accent)]/50">
|
||||
<div className="mt-1.5 text-[10px] text-[var(--accent)]/50 typo-caption">
|
||||
由 {msg.modelName} 生成
|
||||
</div>
|
||||
)}
|
||||
@@ -121,33 +144,43 @@ export function ChatMessages({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-start" style={{ animation: "slideUp 200ms ease-out" }}>
|
||||
<div className="max-w-[90%] md:max-w-[80%] rounded-2xl px-4 md:px-5 py-3 glass-panel">
|
||||
{statusText && (
|
||||
<div className="text-xs text-[var(--accent)] mb-2 flex items-center gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-[var(--accent)] animate-pulse
|
||||
shadow-[0_0_8px_rgba(77,184,164,0.4)]" />
|
||||
{statusText}
|
||||
</div>
|
||||
)}
|
||||
{streamingText && (
|
||||
<div className="whitespace-pre-wrap text-sm leading-relaxed">{streamingText}</div>
|
||||
)}
|
||||
{streamingImages.length > 0 && <ImageGrid images={streamingImages} />}
|
||||
{!streamingText && !statusText && (
|
||||
<div className="flex gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--accent)]/60 animate-bounce" />
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--accent)]/60 animate-bounce [animation-delay:0.1s]" />
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--accent)]/60 animate-bounce [animation-delay:0.2s]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* AI 正在回复(流式) */}
|
||||
<AnimatePresence>
|
||||
{isLoading && (
|
||||
<motion.div
|
||||
key="streaming"
|
||||
variants={assistantEnter}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
||||
className="flex justify-start"
|
||||
>
|
||||
<div className="max-w-[90%] md:max-w-[80%] rounded-2xl px-4 md:px-5 py-3 surface-2 border-l-2 border-l-[var(--accent)]/40">
|
||||
{statusText && (
|
||||
<div className="text-xs text-[var(--accent)] mb-2 flex items-center gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-[var(--accent)] animate-pulse
|
||||
shadow-[0_0_8px_rgba(46,139,122,0.4)]" />
|
||||
{statusText}
|
||||
</div>
|
||||
)}
|
||||
{streamingText && (
|
||||
<StreamingLines text={streamingText} />
|
||||
)}
|
||||
{streamingImages.length > 0 && <ImageGrid images={streamingImages} />}
|
||||
{!streamingText && !statusText && (
|
||||
<div className="flex items-end gap-1 h-5">
|
||||
<div className="wave-dot" style={{ animationDelay: "0s" }} />
|
||||
<div className="wave-dot" style={{ animationDelay: "0.15s" }} />
|
||||
<div className="wave-dot" style={{ animationDelay: "0.3s" }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user