- 布局集成 AuthProvider 和 Toaster 组件 - 更新应用标题为 LionCode - 侧边栏集成用户信息展示 - 设置页面支持已登录用户 - 用户菜单添加登出功能 - 优化全局样式
218 lines
7.3 KiB
TypeScript
218 lines
7.3 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { Copy, ThumbsUp, ThumbsDown, RefreshCw, ChevronDown, ChevronUp, Brain, Loader2, AlertCircle, Check } from 'lucide-react';
|
||
import { Avatar } from '@/components/ui/Avatar';
|
||
import { AILogo } from '@/components/ui/AILogo';
|
||
import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer';
|
||
import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult';
|
||
import { cn } from '@/lib/utils';
|
||
import type { Message, User, ToolResult } from '@/types';
|
||
|
||
interface MessageBubbleProps {
|
||
message: Message;
|
||
user?: User;
|
||
thinkingContent?: string;
|
||
isStreaming?: boolean;
|
||
error?: string;
|
||
/** 代码执行产生的图片(Base64) */
|
||
images?: string[];
|
||
/** Pyodide 加载状态 */
|
||
pyodideStatus?: {
|
||
stage: 'loading' | 'ready' | 'error';
|
||
message: string;
|
||
progress?: number;
|
||
};
|
||
}
|
||
|
||
export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, pyodideStatus }: MessageBubbleProps) {
|
||
const isUser = message.role === 'user';
|
||
const [thinkingExpanded, setThinkingExpanded] = useState(false);
|
||
const [copied, setCopied] = useState(false);
|
||
|
||
// 复制消息内容
|
||
const handleCopy = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(message.content);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 2000);
|
||
} catch (error) {
|
||
console.error('Failed to copy:', error);
|
||
}
|
||
};
|
||
|
||
if (isUser) {
|
||
return (
|
||
<div className="flex justify-end items-start gap-3 mb-8 animate-fade-in">
|
||
<div className="max-w-[70%]">
|
||
<div className="bg-[var(--color-message-user)] text-[var(--color-text-primary)] px-4 py-3 rounded-[18px] text-base leading-relaxed">
|
||
{message.content}
|
||
</div>
|
||
</div>
|
||
{user && <Avatar name={user.name} size="md" />}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex items-start gap-4 mb-8 animate-fade-in">
|
||
{/* AI 图标 */}
|
||
<div className="flex-shrink-0 mt-4">
|
||
<AILogo size={28} />
|
||
</div>
|
||
|
||
{/* 消息内容 */}
|
||
<div className="flex-1 max-w-full">
|
||
{/* 思考内容 */}
|
||
{thinkingContent && (
|
||
<div className="mb-3">
|
||
<button
|
||
onClick={() => setThinkingExpanded(!thinkingExpanded)}
|
||
className="inline-flex items-center gap-2 px-3 py-2 bg-purple-50 text-purple-700 rounded-lg text-sm hover:bg-purple-100 transition-colors"
|
||
>
|
||
<Brain size={16} />
|
||
<span>思考过程</span>
|
||
{thinkingExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||
</button>
|
||
{thinkingExpanded && (
|
||
<div className="mt-2 p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
||
<pre className="text-sm text-purple-800 whitespace-pre-wrap font-mono">
|
||
{thinkingContent}
|
||
</pre>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="mb-3 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
|
||
<AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
|
||
<div className="text-sm text-red-700">{error}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 主要内容 */}
|
||
<div className="bg-[var(--color-message-assistant-bg)] border border-[var(--color-message-assistant-border)] rounded-2xl px-5 py-4 shadow-sm">
|
||
<div className="text-sm text-[var(--color-text-primary)] leading-[1.75]">
|
||
{message.content ? (
|
||
<MarkdownRenderer content={message.content} />
|
||
) : isStreaming ? (
|
||
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)]">
|
||
<Loader2 size={16} className="animate-spin" />
|
||
<span>正在思考...</span>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* 工具调用结果 - 代码执行图片展示 */}
|
||
{message.toolResults && message.toolResults.length > 0 && (
|
||
<div className="mt-4">
|
||
{message.toolResults.map((result, index) => (
|
||
<ToolResultDisplay key={index} result={result} />
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Pyodide 加载状态 */}
|
||
{pyodideStatus && (
|
||
<div className="mt-4">
|
||
<PyodideLoading
|
||
stage={pyodideStatus.stage}
|
||
message={pyodideStatus.message}
|
||
progress={pyodideStatus.progress}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* 代码执行图片(从 props 传入) */}
|
||
{images && images.length > 0 && (
|
||
<CodeExecutionResult
|
||
images={images}
|
||
success={true}
|
||
/>
|
||
)}
|
||
|
||
{/* 流式状态指示器 */}
|
||
{isStreaming && message.content && (
|
||
<div className="flex items-center gap-2 mt-3 text-sm text-[var(--color-text-tertiary)]">
|
||
<Loader2 size={14} className="animate-spin" />
|
||
<span>生成中...</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作按钮(非流式状态下显示) */}
|
||
{!isStreaming && message.content && (
|
||
<>
|
||
<div className="flex items-center gap-1 mt-4 pt-3">
|
||
<button
|
||
onClick={handleCopy}
|
||
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
|
||
title={copied ? '已复制' : '复制'}
|
||
>
|
||
{copied ? <Check size={16} className="text-green-500" /> : <Copy size={16} />}
|
||
</button>
|
||
<ActionButton icon={ThumbsUp} title="好的回答" />
|
||
<ActionButton icon={ThumbsDown} title="不好的回答" />
|
||
<ActionButton icon={RefreshCw} title="重新生成" />
|
||
</div>
|
||
|
||
{/* 免责声明 */}
|
||
<div className="text-xs text-[var(--color-text-tertiary)] text-right mt-2">
|
||
LionCode can make mistakes
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface ActionButtonProps {
|
||
icon: React.ComponentType<{ size?: number }>;
|
||
title: string;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
function ActionButton({ icon: Icon, title, onClick }: ActionButtonProps) {
|
||
return (
|
||
<button
|
||
onClick={onClick}
|
||
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
|
||
title={title}
|
||
>
|
||
<Icon size={16} />
|
||
</button>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 工具调用结果显示组件
|
||
* 专门处理代码执行结果和图片显示
|
||
*/
|
||
interface ToolResultDisplayProps {
|
||
result: ToolResult;
|
||
}
|
||
|
||
function ToolResultDisplay({ result }: ToolResultDisplayProps) {
|
||
// 只有代码执行工具才显示图片
|
||
if (result.toolName !== 'code_execution') {
|
||
return null;
|
||
}
|
||
|
||
// 如果没有图片,不显示
|
||
if (!result.images || result.images.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<CodeExecutionResult
|
||
images={result.images}
|
||
engine={result.engine}
|
||
executionTime={result.executionTime}
|
||
success={!result.isError}
|
||
/>
|
||
);
|
||
}
|