ai enpowered

This commit is contained in:
patdelphi
2025-08-21 22:59:35 +08:00
parent c7534db874
commit 7910dd4bbf
10 changed files with 1806 additions and 28 deletions

View File

@@ -0,0 +1,353 @@
import React, { useState, useEffect } from 'react';
import { X, Settings, Eye, EyeOff, Save, TestTube, CheckCircle, AlertCircle } from 'lucide-react';
import { ChineseButton } from './ChineseButton';
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from './ChineseCard';
import { getAIConfig, saveAIConfig, validateAIConfig, AIConfig, defaultAIConfig } from '../../config/aiConfig';
import { toast } from 'sonner';
import { cn } from '../../lib/utils';
interface AIConfigModalProps {
isOpen: boolean;
onClose: () => void;
onConfigSaved?: () => void;
}
const AIConfigModal: React.FC<AIConfigModalProps> = ({
isOpen,
onClose,
onConfigSaved
}) => {
const [config, setConfig] = useState<AIConfig>({
apiKey: '',
apiUrl: '',
modelName: '',
maxTokens: 2000,
temperature: 0.7,
timeout: 30000
});
const [showApiKey, setShowApiKey] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
// 加载当前配置
useEffect(() => {
if (isOpen) {
const currentConfig = getAIConfig();
setConfig(currentConfig);
setTestResult(null);
}
}, [isOpen]);
// 处理输入变化
const handleInputChange = (field: keyof AIConfig, value: string | number) => {
setConfig(prev => ({
...prev,
[field]: value
}));
setTestResult(null); // 清除测试结果
};
// 保存配置
const handleSave = async () => {
if (!validateAIConfig(config)) {
toast.error('请填写完整的配置信息');
return;
}
setIsSaving(true);
try {
saveAIConfig(config);
toast.success('AI配置保存成功');
if (onConfigSaved) {
onConfigSaved();
}
onClose();
} catch (error: any) {
toast.error(`保存配置失败: ${error.message}`);
} finally {
setIsSaving(false);
}
};
// 测试配置
const handleTest = async () => {
if (!validateAIConfig(config)) {
toast.error('请先填写完整的配置信息');
return;
}
setIsTesting(true);
setTestResult(null);
try {
const testRequest = {
model: config.modelName,
messages: [
{
role: 'user',
content: '请回复"配置测试成功"来确认连接正常。'
}
],
max_tokens: 50,
temperature: 0.1
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
const response = await fetch(config.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
},
body: JSON.stringify(testRequest),
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
const data = await response.json();
if (data.choices && data.choices[0] && data.choices[0].message) {
setTestResult({
success: true,
message: '连接测试成功AI API配置正确。'
});
toast.success('配置测试成功');
} else {
throw new Error('API响应格式异常');
}
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(`API请求失败: ${response.status} ${errorData.error?.message || response.statusText}`);
}
} catch (error: any) {
let errorMessage = '连接测试失败';
if (error.name === 'AbortError') {
errorMessage = '请求超时请检查网络连接和API地址';
} else if (error.message) {
errorMessage = error.message;
}
setTestResult({
success: false,
message: errorMessage
});
toast.error('配置测试失败');
} finally {
setIsTesting(false);
}
};
// 重置为默认配置
const handleReset = () => {
setConfig({
...defaultAIConfig // 使用完整的默认配置包括API Key
});
setTestResult(null);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<ChineseCard className="border-0 shadow-none">
<ChineseCardHeader className="border-b">
<div className="flex items-center justify-between">
<ChineseCardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5 text-blue-600" />
<span>AI解读配置</span>
</ChineseCardTitle>
<ChineseButton
variant="ghost"
size="sm"
onClick={onClose}
className="p-1"
>
<X className="h-4 w-4" />
</ChineseButton>
</div>
</ChineseCardHeader>
<ChineseCardContent className="space-y-6 p-6">
{/* API Key */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
API Key <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type={showApiKey ? 'text' : 'password'}
value={config.apiKey}
onChange={(e) => handleInputChange('apiKey', e.target.value)}
placeholder="请输入您的API Key"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
{/* API URL */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
API地址 <span className="text-red-500">*</span>
</label>
<input
type="url"
value={config.apiUrl}
onChange={(e) => handleInputChange('apiUrl', e.target.value)}
placeholder="https://api.openai.com/v1/chat/completions"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Model Name */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
<span className="text-red-500">*</span>
</label>
<input
type="text"
value={config.modelName}
onChange={(e) => handleInputChange('modelName', e.target.value)}
placeholder="gpt-3.5-turbo"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Advanced Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">Token数</label>
<input
type="number"
value={config.maxTokens}
onChange={(e) => handleInputChange('maxTokens', parseInt(e.target.value) || 2000)}
min="100"
max="4000"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<input
type="number"
value={config.temperature}
onChange={(e) => handleInputChange('temperature', parseFloat(e.target.value) || 0.7)}
min="0"
max="2"
step="0.1"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">(ms)</label>
<input
type="number"
value={config.timeout}
onChange={(e) => handleInputChange('timeout', parseInt(e.target.value) || 30000)}
min="5000"
max="120000"
step="1000"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{/* Stream Setting */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<div className="flex items-center space-x-3">
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={config.stream}
onChange={(e) => handleInputChange('stream', e.target.checked)}
className="sr-only"
/>
<div className={cn(
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
config.stream ? 'bg-blue-600' : 'bg-gray-200'
)}>
<span className={cn(
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
config.stream ? 'translate-x-6' : 'translate-x-1'
)} />
</div>
<span className="ml-2 text-sm text-gray-600">
{config.stream ? '启用' : '禁用'}
</span>
</label>
<span className="text-xs text-gray-500">
AI生成的内容
</span>
</div>
</div>
{/* Test Result */}
{testResult && (
<div className={cn(
'flex items-center space-x-2 p-3 rounded-lg border',
testResult.success
? 'bg-green-50 border-green-200 text-green-800'
: 'bg-red-50 border-red-200 text-red-800'
)}>
{testResult.success ? (
<CheckCircle className="h-4 w-4 flex-shrink-0" />
) : (
<AlertCircle className="h-4 w-4 flex-shrink-0" />
)}
<span className="text-sm">{testResult.message}</span>
</div>
)}
{/* Action Buttons */}
<div className="flex items-center justify-between pt-4 border-t">
<ChineseButton
variant="outline"
onClick={handleReset}
className="text-gray-600"
>
</ChineseButton>
<div className="flex items-center space-x-3">
<ChineseButton
variant="outline"
onClick={handleTest}
disabled={isTesting || !validateAIConfig(config)}
className="flex items-center space-x-2"
>
<TestTube className={cn('h-4 w-4', isTesting && 'animate-pulse')} />
<span>{isTesting ? '测试中...' : '测试连接'}</span>
</ChineseButton>
<ChineseButton
onClick={handleSave}
disabled={isSaving || !validateAIConfig(config)}
className="flex items-center space-x-2"
>
<Save className={cn('h-4 w-4', isSaving && 'animate-pulse')} />
<span>{isSaving ? '保存中...' : '保存配置'}</span>
</ChineseButton>
</div>
</div>
</ChineseCardContent>
</ChineseCard>
</div>
</div>
);
};
export default AIConfigModal;

View File

@@ -0,0 +1,474 @@
import React, { useState, useEffect } from 'react';
import { Brain, Loader2, Sparkles, AlertCircle, CheckCircle, Settings, RefreshCw, Eye, X } from 'lucide-react';
import { ChineseButton } from './ChineseButton';
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from './ChineseCard';
import { cn } from '../../lib/utils';
import {
requestAIInterpretation,
saveAIInterpretation,
getAIInterpretation,
AIInterpretationResult,
AIInterpretationRequest
} from '../../services/aiInterpretationService';
import { getAIConfig, validateAIConfig, getPromptTemplate } from '../../config/aiConfig';
import { toast } from 'sonner';
interface AIInterpretationButtonProps {
analysisData: any;
analysisType: 'bazi' | 'ziwei' | 'yijing';
analysisId?: string; // 用于缓存解读结果
className?: string;
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
showConfigButton?: boolean; // 是否显示配置按钮
onConfigClick?: () => void; // 配置按钮点击回调
}
const AIInterpretationButton: React.FC<AIInterpretationButtonProps> = ({
analysisData,
analysisType,
analysisId,
className,
variant = 'default',
size = 'md',
showConfigButton = true,
onConfigClick
}) => {
const [isLoading, setIsLoading] = useState(false);
const [interpretation, setInterpretation] = useState<AIInterpretationResult | null>(null);
const [showResult, setShowResult] = useState(false);
const [isConfigValid, setIsConfigValid] = useState(false);
const [debugInfo, setDebugInfo] = useState<any>(null);
const [requestStartTime, setRequestStartTime] = useState<number | null>(null);
const [streamingContent, setStreamingContent] = useState<string>(''); // 流式内容
// 检查AI配置是否有效
useEffect(() => {
const config = getAIConfig();
setIsConfigValid(validateAIConfig(config));
}, []);
// 加载已保存的解读结果
useEffect(() => {
if (analysisId) {
const savedInterpretation = getAIInterpretation(analysisId);
if (savedInterpretation) {
setInterpretation(savedInterpretation);
}
}
}, [analysisId]);
// 处理AI解读请求
const handleAIInterpretation = async () => {
if (!isConfigValid) {
toast.error('AI配置不完整请先配置API设置');
if (onConfigClick) {
onConfigClick();
}
return;
}
if (!analysisData) {
toast.error('没有可解读的分析数据');
return;
}
setIsLoading(true);
setRequestStartTime(Date.now());
// 获取用户配置的AI设置
const currentConfig = getAIConfig();
setDebugInfo({
status: '开始请求',
startTime: new Date().toLocaleString(),
config: {
apiUrl: currentConfig.apiUrl,
modelName: currentConfig.modelName,
maxTokens: currentConfig.maxTokens,
temperature: currentConfig.temperature,
timeout: currentConfig.timeout,
apiKeyLength: currentConfig.apiKey?.length || 0
},
analysisType,
analysisDataSize: JSON.stringify(analysisData).length
});
try {
const request: AIInterpretationRequest = {
analysisType,
analysisContent: analysisData,
onStreamUpdate: currentConfig.stream ? (content: string) => {
setStreamingContent(content);
setShowResult(true); // 开始流式输出时就显示结果区域
} : undefined
};
// 获取提示词用于调试显示
const analysisMarkdown = typeof request.analysisContent === 'string'
? request.analysisContent
: JSON.stringify(request.analysisContent, null, 2);
const promptTemplate = getPromptTemplate(request.analysisType);
const fullPrompt = promptTemplate.replace('{analysisContent}', analysisMarkdown);
// 生成curl命令用于调试
const requestBody = {
model: currentConfig.modelName,
messages: [{ role: 'user', content: fullPrompt }],
max_tokens: currentConfig.maxTokens,
temperature: currentConfig.temperature
};
const curlCommand = `curl -X POST "${currentConfig.apiUrl}" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${currentConfig.apiKey.substring(0, 10)}..." \\
-d '${JSON.stringify(requestBody, null, 2).replace(/'/g, "'\"'\"'")}'`;
setDebugInfo(prev => ({
...prev,
status: '发送请求中',
requestTime: new Date().toLocaleString(),
apiParams: {
model: currentConfig.modelName,
maxTokens: currentConfig.maxTokens,
temperature: currentConfig.temperature,
promptLength: fullPrompt.length,
promptPreview: fullPrompt.substring(0, 300) + '...',
fullPrompt: fullPrompt, // 完整的prompt用于调试
requestBody: JSON.stringify(requestBody, null, 2),
curlCommand: curlCommand
}
}));
const result = await requestAIInterpretation(request);
const endTime = Date.now();
const duration = requestStartTime ? endTime - requestStartTime : 0;
console.log('🐛 调试时间计算 (成功):', {
requestStartTime,
endTime,
duration,
durationSeconds: duration / 1000
});
setDebugInfo(prev => ({
...prev,
status: result.success ? '请求成功' : '请求失败',
endTime: new Date().toLocaleString(),
duration: `${duration}ms (${(duration / 1000).toFixed(1)}秒)`,
result: {
success: result.success,
contentLength: result.content?.length || 0,
error: result.error,
model: result.model,
tokensUsed: result.tokensUsed,
actualDuration: duration,
startTime: requestStartTime,
endTime: endTime
}
}));
if (result.success) {
console.log('AI解读成功结果:', result);
setInterpretation(result);
setShowResult(true);
setStreamingContent(''); // 清空流式内容,使用最终结果
// 保存解读结果
if (analysisId) {
saveAIInterpretation(analysisId, result);
}
toast.success(`AI解读完成耗时${duration}ms`);
} else {
console.error('AI解读失败:', result.error);
toast.error(`AI解读失败: ${result.error}`);
setStreamingContent(''); // 清空流式内容
}
} catch (error: any) {
const endTime = Date.now();
const duration = requestStartTime ? endTime - requestStartTime : 0;
console.log('🐛 调试时间计算:', {
requestStartTime,
endTime,
duration,
durationSeconds: duration / 1000
});
setDebugInfo(prev => ({
...prev,
status: '请求异常',
endTime: new Date().toLocaleString(),
duration: `${duration}ms (${(duration / 1000).toFixed(1)}秒)`,
error: {
name: error.name,
message: error.message,
stack: error.stack?.substring(0, 500),
actualDuration: duration,
startTime: requestStartTime,
endTime: endTime
}
}));
console.error('AI解读出错:', error);
toast.error(`解读过程出错: ${error.message || '未知错误'}`);
setStreamingContent(''); // 清空流式内容
} finally {
setIsLoading(false);
// 不要立即清除requestStartTime保留用于调试
// setRequestStartTime(null);
}
};
// 重新解读
const handleReinterpret = () => {
setInterpretation(null);
setShowResult(false);
handleAIInterpretation();
};
// 获取分析类型显示名称
const getAnalysisTypeName = (type: string) => {
const names = {
'bazi': '八字',
'ziwei': '紫微斗数',
'yijing': '易经'
};
return names[type as keyof typeof names] || '命理';
};
return (
<div className={cn('space-y-4', className)}>
{/* AI解读按钮区域 */}
<div className="flex items-center space-x-2">
<ChineseButton
variant="outline"
size="md"
onClick={interpretation ? () => setShowResult(!showResult) : handleAIInterpretation}
disabled={isLoading || (!isConfigValid && !interpretation)}
className={cn(
'px-3 sm:px-6 text-xs sm:text-sm',
!isConfigValid && !interpretation && 'opacity-50 cursor-not-allowed'
)}
>
{isLoading ? (
<Loader2 className="mr-1 h-3 w-3 sm:h-4 sm:w-4 animate-spin" />
) : (
<Eye className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
)}
<span className="hidden sm:inline">
{isLoading
? 'AI解读中...'
: interpretation
? (showResult ? '隐藏解读' : 'AI解读')
: 'AI解读'
}
</span>
</ChineseButton>
{/* 重新解读按钮 */}
{interpretation && (
<ChineseButton
variant="outline"
size={size}
onClick={handleReinterpret}
disabled={isLoading}
className="flex items-center space-x-1"
>
<RefreshCw className={cn('h-3 w-3', isLoading && 'animate-spin')} />
<span className="text-xs"></span>
</ChineseButton>
)}
{/* 配置按钮 */}
{showConfigButton && onConfigClick && (
<ChineseButton
variant="ghost"
size={size}
onClick={onConfigClick}
className="flex items-center space-x-1 text-gray-500 hover:text-gray-700"
>
<Settings className="h-3 w-3" />
<span className="text-xs"></span>
</ChineseButton>
)}
</div>
{/* 配置提示 */}
{!isConfigValid && !interpretation && (
<div className="flex items-center space-x-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-yellow-600 flex-shrink-0" />
<div className="text-sm text-yellow-800">
<p className="font-medium">AI设置</p>
<p className="text-xs mt-1">API KeyAPI地址等信息才能使用AI解读功能</p>
</div>
</div>
)}
{/* 调试信息 */}
{debugInfo && (
<div className="text-xs text-gray-500 p-3 bg-gray-100 rounded border">
<div className="flex justify-between items-center mb-2">
<div className="font-bold">🔍 AI解读调试信息</div>
<button
onClick={() => setDebugInfo(null)}
className="text-gray-400 hover:text-gray-600 p-1"
title="清除调试信息"
>
<X className="h-3 w-3" />
</button>
</div>
<div className="space-y-1">
<div><strong>:</strong> {debugInfo.status}</div>
<div><strong>:</strong> {debugInfo.startTime}</div>
{debugInfo.endTime && <div><strong>:</strong> {debugInfo.endTime}</div>}
{debugInfo.duration && <div><strong>:</strong> {debugInfo.duration}</div>}
<div><strong>:</strong> {debugInfo.analysisType}</div>
<div><strong>:</strong> {debugInfo.analysisDataSize} </div>
{debugInfo.config && (
<details className="mt-2">
<summary className="cursor-pointer font-medium"></summary>
<div className="ml-2 mt-1 space-y-1">
<div><strong>API地址:</strong> {debugInfo.config.apiUrl}</div>
<div><strong>:</strong> {debugInfo.config.modelName}</div>
<div><strong>Token:</strong> {debugInfo.config.maxTokens}</div>
<div><strong>:</strong> {debugInfo.config.temperature}</div>
<div><strong>:</strong> {debugInfo.config.timeout}ms</div>
<div><strong>API Key长度:</strong> {debugInfo.config.apiKeyLength}</div>
</div>
</details>
)}
{debugInfo.apiParams && (
<details className="mt-2">
<summary className="cursor-pointer font-medium">API调用参数</summary>
<div className="ml-2 mt-1 space-y-1">
<div><strong>:</strong> {debugInfo.apiParams.model}</div>
<div><strong>Token:</strong> {debugInfo.apiParams.maxTokens}</div>
<div><strong>:</strong> {debugInfo.apiParams.temperature}</div>
<div><strong>Prompt长度:</strong> {debugInfo.apiParams.promptLength} </div>
<div><strong>Prompt预览:</strong></div>
<pre className="text-xs mt-1 p-2 bg-white rounded border whitespace-pre-wrap max-h-32 overflow-y-auto">{debugInfo.apiParams.promptPreview}</pre>
<details className="mt-2">
<summary className="cursor-pointer text-xs">Prompt</summary>
<pre className="text-xs mt-1 p-2 bg-white rounded border whitespace-pre-wrap max-h-64 overflow-y-auto">{debugInfo.apiParams.fullPrompt}</pre>
</details>
<details className="mt-2">
<summary className="cursor-pointer text-xs">JSON</summary>
<pre className="text-xs mt-1 p-2 bg-white rounded border whitespace-pre-wrap max-h-64 overflow-y-auto">{debugInfo.apiParams.requestBody}</pre>
</details>
<details className="mt-2">
<summary className="cursor-pointer text-xs font-medium text-blue-600">🔧 API调用指令 (curl)</summary>
<div className="mt-1">
<div className="text-xs text-gray-600 mb-1">API:</div>
<pre className="text-xs p-2 bg-black text-green-400 rounded border whitespace-pre-wrap max-h-64 overflow-y-auto font-mono">{debugInfo.apiParams.curlCommand}</pre>
<button
onClick={() => navigator.clipboard.writeText(debugInfo.apiParams.curlCommand)}
className="mt-1 px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600"
>
curl命令
</button>
</div>
</details>
</div>
</details>
)}
{debugInfo.result && (
<details className="mt-2">
<summary className="cursor-pointer font-medium"></summary>
<div className="ml-2 mt-1 space-y-1">
<div><strong>:</strong> {debugInfo.result.success ? '是' : '否'}</div>
<div><strong>:</strong> {debugInfo.result.contentLength}</div>
<div><strong>使:</strong> {debugInfo.result.model || 'N/A'}</div>
<div><strong>Token:</strong> {debugInfo.result.tokensUsed || 'N/A'}</div>
{debugInfo.result.error && <div><strong>:</strong> {debugInfo.result.error}</div>}
<div className="mt-2 p-2 bg-yellow-50 rounded text-xs">
<div><strong>:</strong></div>
<div>: {debugInfo.result.startTime}</div>
<div>: {debugInfo.result.endTime}</div>
<div>: {debugInfo.result.actualDuration}ms</div>
</div>
</div>
</details>
)}
{debugInfo.error && (
<details className="mt-2">
<summary className="cursor-pointer font-medium text-red-600"></summary>
<div className="ml-2 mt-1 space-y-1 text-red-600">
<div><strong>:</strong> {debugInfo.error.name}</div>
<div><strong>:</strong> {debugInfo.error.message}</div>
{debugInfo.error.stack && (
<div><strong>:</strong> <pre className="text-xs mt-1 whitespace-pre-wrap">{debugInfo.error.stack}</pre></div>
)}
<div className="mt-2 p-2 bg-yellow-50 rounded text-xs text-black">
<div><strong>:</strong></div>
<div>: {debugInfo.error.startTime}</div>
<div>: {debugInfo.error.endTime}</div>
<div>: {debugInfo.error.actualDuration}ms</div>
</div>
</div>
</details>
)}
</div>
</div>
)}
{/* AI解读结果显示 */}
{(interpretation || streamingContent) && showResult && (
<ChineseCard className="border-2 border-purple-200 bg-gradient-to-br from-purple-50 to-blue-50">
<ChineseCardHeader>
<ChineseCardTitle className="flex items-center space-x-2 text-purple-800">
{isLoading ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Sparkles className="h-5 w-5" />
)}
<span>AI智能解读 - {getAnalysisTypeName(analysisType)}</span>
{isLoading && streamingContent && (
<span className="ml-2 text-sm font-normal text-purple-600">...</span>
)}
</ChineseCardTitle>
{interpretation && (
<div className="flex items-center space-x-4 text-xs text-gray-500 mt-2">
<span>: {new Date(interpretation.timestamp).toLocaleString('zh-CN')}</span>
{interpretation.model && <span>: {interpretation.model}</span>}
{interpretation.tokensUsed && <span>Token: {interpretation.tokensUsed}</span>}
</div>
)}
</ChineseCardHeader>
<ChineseCardContent>
{interpretation && !interpretation.success ? (
<div className="flex items-center space-x-2 p-4 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0" />
<div className="text-sm text-red-800">
<p className="font-medium"></p>
<p className="text-xs mt-1">{interpretation.error}</p>
</div>
</div>
) : (
<div className="prose prose-sm max-w-none">
<div className="whitespace-pre-wrap text-gray-800 leading-relaxed">
{streamingContent || interpretation?.content}
{isLoading && streamingContent && (
<span className="inline-block w-2 h-5 bg-purple-600 animate-pulse ml-1"></span>
)}
</div>
</div>
)}
</ChineseCardContent>
</ChineseCard>
)}
</div>
);
};
export default AIInterpretationButton;