247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Bot, CheckCircle2, ChevronDown, Info, Sparkles, UserRound, Wrench } from 'lucide-react';
|
|
import MarkdownRenderer from '../MarkdownRenderer';
|
|
import { useTheme } from '../../contexts/ThemeContext';
|
|
|
|
interface ConversationStep {
|
|
text: string;
|
|
subText?: string;
|
|
status: 'done' | 'processing';
|
|
}
|
|
|
|
interface ToolCall {
|
|
id: string;
|
|
name: string;
|
|
input?: Record<string, unknown>;
|
|
output?: string;
|
|
}
|
|
|
|
interface ConversationMessage {
|
|
id: string;
|
|
role: 'user' | 'assistant';
|
|
content: string;
|
|
steps?: ConversationStep[];
|
|
thinkings?: Array<{ duration: number; content: string }>;
|
|
toolCalls?: ToolCall[];
|
|
}
|
|
|
|
interface SkillCreatorConversationItemProps {
|
|
message: ConversationMessage;
|
|
isConnecting?: boolean;
|
|
}
|
|
|
|
function MessageAvatar({ role, active = false }: { role: 'user' | 'assistant'; active?: boolean }) {
|
|
const { theme } = useTheme();
|
|
const isDarkMode = theme === 'dark';
|
|
const Icon = role === 'assistant' ? Bot : UserRound;
|
|
|
|
return (
|
|
<div
|
|
className={`relative w-9 h-9 rounded-2xl flex items-center justify-center flex-shrink-0 shadow-sm ${
|
|
role === 'assistant'
|
|
? isDarkMode
|
|
? 'bg-gradient-to-br from-slate-700 to-slate-900 text-sky-200 ring-1 ring-slate-600'
|
|
: 'bg-gradient-to-br from-red-50 to-rose-100 text-red-500 ring-1 ring-red-100'
|
|
: isDarkMode
|
|
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white'
|
|
: 'bg-gradient-to-br from-red-500 to-rose-500 text-white'
|
|
}`}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{role === 'assistant' && active && (
|
|
<Sparkles className={`absolute -right-0.5 -top-0.5 w-3.5 h-3.5 ${isDarkMode ? 'text-sky-200' : 'text-amber-400'}`} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StepsSummary({ steps }: { steps: ConversationStep[] }) {
|
|
const { theme } = useTheme();
|
|
const isDarkMode = theme === 'dark';
|
|
const [open, setOpen] = useState(true);
|
|
const doneCount = steps.filter((step) => step.status === 'done').length;
|
|
|
|
return (
|
|
<div
|
|
className={`mb-3 overflow-hidden rounded-2xl border shadow-sm ${
|
|
isDarkMode ? 'border-slate-700 bg-slate-900/60 text-slate-300' : 'border-red-100 bg-white text-gray-600'
|
|
}`}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((prev) => !prev)}
|
|
className={`w-full h-10 px-3.5 flex items-center gap-2 text-xs transition ${
|
|
isDarkMode ? 'bg-slate-800/70 hover:bg-slate-800' : 'bg-red-50/60 hover:bg-red-50'
|
|
}`}
|
|
>
|
|
<CheckCircle2 className={`w-3.5 h-3.5 ${isDarkMode ? 'text-emerald-400' : 'text-red-500'}`} />
|
|
<span className="font-medium">执行步骤</span>
|
|
<span className={isDarkMode ? 'text-slate-500' : 'text-gray-400'}>已完成 {doneCount} / {steps.length}</span>
|
|
<ChevronDown className={`w-3.5 h-3.5 ml-auto transition-transform ${open ? '' : '-rotate-90'}`} />
|
|
</button>
|
|
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${open ? 'max-h-[640px] opacity-100' : 'max-h-0 opacity-0'}`}>
|
|
<div className={`border-t px-4 py-3 space-y-3 ${isDarkMode ? 'border-slate-700' : 'border-red-50'}`}>
|
|
{steps.map((step, index) => {
|
|
const done = step.status === 'done';
|
|
|
|
return (
|
|
<div key={`${step.text}-${index}`} className="flex gap-3">
|
|
<div className="pt-0.5">
|
|
{done ? (
|
|
<CheckCircle2 className={`w-4 h-4 ${isDarkMode ? 'text-emerald-400' : 'text-red-500'}`} />
|
|
) : (
|
|
<span className={`block w-4 h-4 rounded-full border-2 ${isDarkMode ? 'border-sky-300 border-t-transparent' : 'border-red-400 border-t-transparent'} animate-spin`} />
|
|
)}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className={`text-xs font-medium ${isDarkMode ? 'text-slate-200' : 'text-gray-800'}`}>
|
|
第 {index + 1} 步:{step.text}
|
|
</div>
|
|
{step.subText && (
|
|
<div className={`mt-1 rounded-xl px-3 py-2 text-xs leading-5 ${isDarkMode ? 'bg-slate-950/50 text-slate-400' : 'bg-gray-50 text-gray-600'}`}>
|
|
{step.subText}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ThinkingPanel({ duration, content }: { duration: number; content: string }) {
|
|
const { theme } = useTheme();
|
|
const isDarkMode = theme === 'dark';
|
|
const [open, setOpen] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (duration > 0) setOpen(false);
|
|
}, [duration]);
|
|
|
|
return (
|
|
<div className={`mb-3 rounded-2xl border overflow-hidden shadow-sm ${isDarkMode ? 'border-slate-700 bg-slate-900/60' : 'border-red-100 bg-white'}`}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((prev) => !prev)}
|
|
className={`w-full h-10 px-3.5 flex items-center gap-2 text-xs transition ${
|
|
isDarkMode ? 'text-slate-300 bg-slate-800/70 hover:bg-slate-800' : 'text-gray-600 bg-red-50/60 hover:bg-red-50'
|
|
}`}
|
|
>
|
|
<Info className={`w-3.5 h-3.5 ${isDarkMode ? 'text-sky-300' : 'text-red-500'}`} />
|
|
<span className="font-medium">思考过程</span>
|
|
<ChevronDown className={`w-3.5 h-3.5 ml-auto transition-transform ${open ? '' : '-rotate-90'}`} />
|
|
</button>
|
|
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${open ? 'opacity-100' : 'max-h-0 opacity-0'}`}>
|
|
<div className={`px-4 py-3 text-xs leading-6 whitespace-pre-wrap border-t ${isDarkMode ? 'border-slate-700 text-slate-300' : 'border-red-50 text-gray-600'}`}>
|
|
{content}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ToolCallPanel({ toolCall }: { toolCall: ToolCall }) {
|
|
const { theme } = useTheme();
|
|
const isDarkMode = theme === 'dark';
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const details = {
|
|
input: toolCall.input && Object.keys(toolCall.input).length > 0 ? JSON.stringify(toolCall.input, null, 2) : '',
|
|
output: toolCall.output ?? '',
|
|
};
|
|
const hasDetails = Boolean(details.input || details.output);
|
|
|
|
return (
|
|
<div className={`mb-3 rounded-2xl border overflow-hidden shadow-sm ${isDarkMode ? 'border-slate-700 bg-slate-900/60' : 'border-red-100 bg-white'}`}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((prev) => !prev)}
|
|
className={`w-full h-10 px-3.5 flex items-center gap-2 text-xs transition ${
|
|
isDarkMode ? 'text-slate-300 bg-slate-800/70 hover:bg-slate-800' : 'text-gray-600 bg-red-50/60 hover:bg-red-50'
|
|
}`}
|
|
>
|
|
<Wrench className={`w-3.5 h-3.5 ${isDarkMode ? 'text-amber-300' : 'text-red-500'}`} />
|
|
<span className="font-medium">{toolCall.name}</span>
|
|
{hasDetails && <ChevronDown className={`w-3.5 h-3.5 ml-auto transition-transform ${open ? '' : '-rotate-90'}`} />}
|
|
</button>
|
|
{hasDetails && (
|
|
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${open ? 'max-h-[520px] opacity-100' : 'max-h-0 opacity-0'}`}>
|
|
<div className={`border-t px-4 py-3 space-y-3 ${isDarkMode ? 'border-slate-700' : 'border-red-50'}`}>
|
|
{details.input && (
|
|
<div>
|
|
<p className={`text-[11px] mb-1 ${isDarkMode ? 'text-slate-400' : 'text-gray-500'}`}>请求参数</p>
|
|
<pre className={`rounded-xl px-3 py-2 text-[11px] leading-5 overflow-x-auto ${isDarkMode ? 'bg-slate-950/60 text-slate-300' : 'bg-gray-50 text-gray-700'}`}>{details.input}</pre>
|
|
</div>
|
|
)}
|
|
{details.output && (
|
|
<div>
|
|
<p className={`text-[11px] mb-1 ${isDarkMode ? 'text-slate-400' : 'text-gray-500'}`}>返回结果</p>
|
|
<pre className={`rounded-xl px-3 py-2 text-[11px] leading-5 overflow-x-auto ${isDarkMode ? 'bg-slate-950/60 text-slate-300' : 'bg-gray-50 text-gray-700'}`}>{details.output}</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function SkillCreatorConversationItem({ message, isConnecting = false }: SkillCreatorConversationItemProps) {
|
|
const { theme } = useTheme();
|
|
const isDarkMode = theme === 'dark';
|
|
|
|
if (message.role === 'user') {
|
|
return (
|
|
<div className="flex justify-end gap-3">
|
|
<div className={`max-w-[72%] rounded-2xl rounded-tr-md px-4 py-3 text-sm leading-relaxed shadow-sm ${
|
|
isDarkMode ? 'bg-blue-600/80 text-white shadow-blue-950/20' : 'bg-gradient-to-r from-red-500 to-rose-500 text-white shadow-red-200/70'
|
|
}`}>
|
|
{message.content}
|
|
</div>
|
|
<MessageAvatar role="user" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex gap-3">
|
|
<MessageAvatar role="assistant" active={!message.content} />
|
|
<div
|
|
className={`max-w-[86%] flex-1 rounded-3xl border px-4 py-3.5 text-sm shadow-sm ${
|
|
isDarkMode ? 'border-slate-700 bg-slate-800/45 text-slate-200 shadow-black/10' : 'border-red-100 bg-white text-[#2a2f3f] shadow-red-100/60'
|
|
}`}
|
|
>
|
|
{message.steps && message.steps.length > 0 && <StepsSummary steps={message.steps} />}
|
|
{message.thinkings?.map((thinking, index) => (
|
|
<ThinkingPanel key={`${message.id}-thinking-${index}`} duration={thinking.duration} content={thinking.content} />
|
|
))}
|
|
{message.toolCalls?.map((toolCall) => (
|
|
<ToolCallPanel key={toolCall.id} toolCall={toolCall} />
|
|
))}
|
|
{isConnecting && (
|
|
<div className={`flex items-center gap-2 text-sm ${isDarkMode ? 'text-slate-400' : 'text-gray-500'}`}>
|
|
<span className="skill-creator-dot" />
|
|
<span className="skill-creator-dot" />
|
|
<span className="skill-creator-dot" />
|
|
<span
|
|
data-text="正在连接技能服务..."
|
|
className={`ml-1 skill-creator-shimmer-text ${isDarkMode ? 'skill-creator-shimmer-dark' : 'skill-creator-shimmer-light'}`}
|
|
>
|
|
正在连接技能服务...
|
|
</span>
|
|
</div>
|
|
)}
|
|
{message.content && (
|
|
<MarkdownRenderer
|
|
content={message.content}
|
|
className={isDarkMode ? '[&_p]:text-slate-200 [&_li]:text-slate-200' : '[&_p]:text-[#2a2f3f] [&_li]:text-[#2a2f3f]'}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|