220 lines
6.8 KiB
TypeScript
220 lines
6.8 KiB
TypeScript
import { CheckCircleOutlined, LoadingOutlined, ThunderboltOutlined, ToolOutlined } from '@ant-design/icons';
|
||
import { Button, Card, Collapse, Spin, Tag, Typography } from 'antd';
|
||
import { useState } from 'react';
|
||
import type { ThoughtItem } from '~/api/dify-chat';
|
||
import '../../styles/components/chat-with-llm/thought-process.css';
|
||
import Markdown from './markdown';
|
||
|
||
const { Panel } = Collapse;
|
||
const { Text, Paragraph } = Typography;
|
||
|
||
interface ThoughtProcessProps {
|
||
thought: ThoughtItem;
|
||
isFinished: boolean;
|
||
allToolIcons?: Record<string, string>;
|
||
}
|
||
|
||
/**
|
||
* 思考过程组件
|
||
* 展示AI的思考过程和工具调用
|
||
*/
|
||
export default function ThoughtProcess({
|
||
thought,
|
||
isFinished,
|
||
allToolIcons = {}
|
||
}: ThoughtProcessProps) {
|
||
const [expanded, setExpanded] = useState(false);
|
||
|
||
const { tool_name, tool_input, tool_output, thought: thoughtText, observation } = thought;
|
||
|
||
/**
|
||
* 获取工具图标
|
||
*/
|
||
const getToolIcon = (toolName?: string) => {
|
||
if (!toolName) return <ToolOutlined />;
|
||
|
||
// 如果有自定义图标映射
|
||
if (allToolIcons[toolName]) {
|
||
return <span>{allToolIcons[toolName]}</span>;
|
||
}
|
||
|
||
// 根据工具名称返回默认图标
|
||
switch (toolName.toLowerCase()) {
|
||
case 'search':
|
||
case 'web_search':
|
||
return '🔍';
|
||
case 'calculator':
|
||
case 'math':
|
||
return '🧮';
|
||
case 'code':
|
||
case 'python':
|
||
return '💻';
|
||
case 'image':
|
||
case 'vision':
|
||
return '👁️';
|
||
case 'file':
|
||
case 'document':
|
||
return '📄';
|
||
default:
|
||
return <ToolOutlined />;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取状态图标和颜色
|
||
*/
|
||
const getStatusInfo = () => {
|
||
if (isFinished && observation) {
|
||
return {
|
||
icon: <CheckCircleOutlined />,
|
||
color: 'success',
|
||
text: '已完成'
|
||
};
|
||
} else if (!isFinished) {
|
||
return {
|
||
icon: <LoadingOutlined spin />,
|
||
color: 'processing',
|
||
text: '执行中'
|
||
};
|
||
} else {
|
||
return {
|
||
icon: <ThunderboltOutlined />,
|
||
color: 'default',
|
||
text: '等待中'
|
||
};
|
||
}
|
||
};
|
||
|
||
const statusInfo = getStatusInfo();
|
||
|
||
/**
|
||
* 格式化工具输入
|
||
*/
|
||
const formatToolInput = (input?: string) => {
|
||
if (!input) return '无输入参数';
|
||
|
||
try {
|
||
const parsed = JSON.parse(input);
|
||
return (
|
||
<pre className="bg-gray-50 p-2 rounded text-sm overflow-x-auto">
|
||
{JSON.stringify(parsed, null, 2)}
|
||
</pre>
|
||
);
|
||
} catch {
|
||
return <Text code>{input}</Text>;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化工具输出
|
||
*/
|
||
const formatToolOutput = (output?: string) => {
|
||
if (!output) return '暂无输出';
|
||
|
||
try {
|
||
const parsed = JSON.parse(output);
|
||
return (
|
||
<pre className="bg-gray-50 p-2 rounded text-sm overflow-x-auto">
|
||
{JSON.stringify(parsed, null, 2)}
|
||
</pre>
|
||
);
|
||
} catch {
|
||
return <Markdown content={output} />;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Card
|
||
size="small"
|
||
className="my-2 border-l-4 border-l-blue-400 bg-blue-50"
|
||
bodyStyle={{ padding: '12px' }}
|
||
>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-lg">{getToolIcon(tool_name)}</span>
|
||
<Text strong className="text-blue-700">
|
||
{tool_name || '工具调用'}
|
||
</Text>
|
||
<Tag
|
||
color={statusInfo.color as any}
|
||
icon={statusInfo.icon}
|
||
className="ml-2"
|
||
>
|
||
{statusInfo.text}
|
||
</Tag>
|
||
</div>
|
||
|
||
{(tool_input || tool_output) && (
|
||
<Button
|
||
type="text"
|
||
size="small"
|
||
onClick={() => setExpanded(!expanded)}
|
||
className="text-blue-600"
|
||
>
|
||
{expanded ? '收起详情' : '查看详情'}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 思考内容 */}
|
||
{thoughtText && (
|
||
<div className="mb-2">
|
||
<Markdown content={thoughtText} />
|
||
</div>
|
||
)}
|
||
|
||
{/* 工具详情 */}
|
||
{expanded && (tool_input || tool_output) && (
|
||
<Collapse
|
||
ghost
|
||
size="small"
|
||
className="bg-white rounded"
|
||
>
|
||
{tool_input && (
|
||
<Panel
|
||
header={
|
||
<span className="text-sm font-medium text-gray-600">
|
||
📥 输入参数
|
||
</span>
|
||
}
|
||
key="input"
|
||
>
|
||
{formatToolInput(tool_input)}
|
||
</Panel>
|
||
)}
|
||
|
||
{tool_output && (
|
||
<Panel
|
||
header={
|
||
<span className="text-sm font-medium text-gray-600">
|
||
📤 执行结果
|
||
</span>
|
||
}
|
||
key="output"
|
||
>
|
||
{formatToolOutput(tool_output)}
|
||
</Panel>
|
||
)}
|
||
</Collapse>
|
||
)}
|
||
|
||
{/* 观察结果 */}
|
||
{observation && observation !== tool_output && (
|
||
<div className="mt-2 p-2 bg-green-50 rounded border border-green-200">
|
||
<Text className="text-green-700 text-sm font-medium">💡 观察结果:</Text>
|
||
<div className="mt-1">
|
||
<Markdown content={observation} />
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 加载状态 */}
|
||
{!isFinished && !observation && (
|
||
<div className="flex items-center justify-center py-2 text-blue-600">
|
||
<Spin size="small" className="mr-2" />
|
||
<Text className="text-sm">正在执行工具调用...</Text>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|