Files
AI-Expert-Sidebar/frontend/src/components/AIPanel.tsx
T
2026-04-01 14:09:33 +08:00

137 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
interface AIPanelProps {
text: string;
loading: boolean;
error: string | null;
isFallback: boolean;
question: string;
onCopy: (text: string) => void;
onStop: () => void;
onClose: () => void;
}
export default function AIPanel({
text, loading, error, isFallback, question, onCopy, onStop, onClose,
}: AIPanelProps) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
onCopy(text);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
return (
<div className="flex-shrink-0 px-4 pb-4 animate-fade-in">
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5">
<span className="text-sm"></span>
<span className="text-[12px] font-semibold text-accent-light">
DeepSeek RAG
</span>
{/* Animated loading dots */}
{loading && (
<div className="flex gap-1 ml-1">
{[0, 1, 2].map(i => (
<span
key={i}
className="w-1 h-1 rounded-full bg-accent-light"
style={{ animation: `pulseDot 1.4s ease-in-out ${i * 0.2}s infinite` }}
/>
))}
</div>
)}
{/* Fallback badge */}
{isFallback && !loading && (
<span className="text-[9px] px-1.5 py-0.5 rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30 ml-1">
</span>
)}
</div>
<div className="flex items-center gap-1">
{/* Stop button — only visible while streaming */}
{loading && (
<button
id="btn-ai-stop"
onClick={onStop}
title="停止生成"
className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-md
bg-red-500/15 text-red-400 border border-red-500/25
hover:bg-red-500/25 transition-all"
>
<span></span>
</button>
)}
<button
id="btn-ai-close"
onClick={onClose}
className="text-white/30 hover:text-white/70 text-xs transition-colors ml-1"
>
</button>
</div>
</div>
{/* Question context */}
{question && (
<p className="text-[10px] text-white/35 mb-2 line-clamp-1">
{question}
</p>
)}
{/* Fallback notice */}
{isFallback && (
<div className="mb-2 px-2 py-1.5 rounded-lg bg-amber-500/10 border border-amber-500/20
text-amber-400 text-[10px] flex items-center gap-1.5">
<span></span>
<span>DeepSeek </span>
</div>
)}
{/* Error state */}
{error && !isFallback ? (
<div className="ai-panel border-red-500/30 bg-red-500/5">
<p className="text-red-400 text-[12px] font-medium mb-1"> AI </p>
<p className="text-white/50 text-[11px]">{error}</p>
<p className="text-white/30 text-[11px] mt-2">使</p>
</div>
) : (
/* Content panel with typewriter effect */
<div className={`ai-panel ${isFallback ? 'border-amber-500/20 bg-amber-500/5' : ''}`}>
{loading && !text ? (
<p className="text-white/30 text-[12px] animate-pulse">
DeepSeek
</p>
) : (
<p className="text-[13px] leading-relaxed whitespace-pre-wrap">
{text}
{/* Blinking cursor while streaming */}
{loading && (
<span className="inline-block w-0.5 h-3.5 bg-accent ml-0.5 align-middle"
style={{ animation: 'pulseDot 0.8s ease-in-out infinite' }} />
)}
</p>
)}
</div>
)}
{/* Copy button */}
{text && !error && (
<button
id="btn-ai-copy"
onClick={handleCopy}
className="mt-2 w-full text-[11px] py-2 rounded-lg bg-accent/20 text-accent-light
border border-accent/30 hover:bg-accent/35 transition-all font-medium"
>
{copied ? '✓ 已复制' : '📋 复制此话术'}
</button>
)}
</div>
);
}