mirror of
https://github.com/patdelphi/suanming.git
synced 2026-03-10 18:43:11 +08:00
ai enpowered
This commit is contained in:
@@ -4,6 +4,8 @@ import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, L
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||
import { BackToTop } from './ui/BackToTop';
|
||||
import DownloadButton from './ui/DownloadButton';
|
||||
import AIInterpretationButton from './ui/AIInterpretationButton';
|
||||
import AIConfigModal from './ui/AIConfigModal';
|
||||
import { localApi } from '../lib/localApi';
|
||||
|
||||
interface CompleteBaziAnalysisProps {
|
||||
@@ -20,6 +22,7 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
|
||||
const [isLoading, setIsLoading] = useState(!propAnalysisData);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [analysisData, setAnalysisData] = useState<any>(propAnalysisData || null);
|
||||
const [showAIConfig, setShowAIConfig] = useState(false);
|
||||
|
||||
// 五行颜色配置
|
||||
const elementColors: { [key: string]: string } = {
|
||||
@@ -278,15 +281,26 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-yellow-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8" id="bazi-analysis-content" data-export-content>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
<div className="flex justify-end no-export" data-no-export>
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="bazi"
|
||||
userName={birthDate.name}
|
||||
targetElementId="bazi-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
{/* 下载和AI解读按钮 */}
|
||||
<div className="flex justify-between items-start no-export" data-no-export>
|
||||
<div className="flex-1">
|
||||
<AIInterpretationButton
|
||||
analysisData={analysisData}
|
||||
analysisType="bazi"
|
||||
analysisId={`bazi-${birthDate.date}-${birthDate.time}`}
|
||||
onConfigClick={() => setShowAIConfig(true)}
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="bazi"
|
||||
userName={birthDate.name}
|
||||
targetElementId="bazi-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标题和基本信息 */}
|
||||
@@ -1047,6 +1061,16 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
|
||||
|
||||
{/* 回到顶部按钮 */}
|
||||
<BackToTop />
|
||||
|
||||
{/* AI配置模态框 */}
|
||||
<AIConfigModal
|
||||
isOpen={showAIConfig}
|
||||
onClose={() => setShowAIConfig(false)}
|
||||
onConfigSaved={() => {
|
||||
setShowAIConfig(false);
|
||||
// 可以在这里添加配置保存后的逻辑
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, L
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||
import { BackToTop } from './ui/BackToTop';
|
||||
import DownloadButton from './ui/DownloadButton';
|
||||
import AIInterpretationButton from './ui/AIInterpretationButton';
|
||||
import AIConfigModal from './ui/AIConfigModal';
|
||||
import { localApi } from '../lib/localApi';
|
||||
|
||||
interface CompleteYijingAnalysisProps {
|
||||
@@ -21,6 +23,7 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(!propAnalysisData);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [analysisData, setAnalysisData] = useState<any>(propAnalysisData || null);
|
||||
const [showAIConfig, setShowAIConfig] = useState(false);
|
||||
|
||||
// 卦象颜色配置
|
||||
const hexagramColors: { [key: string]: string } = {
|
||||
@@ -266,15 +269,26 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-yellow-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8" id="yijing-analysis-content" data-export-content>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
<div className="flex justify-end no-export" data-no-export>
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="yijing"
|
||||
userName={question ? `占卜_${question.substring(0, 10)}` : 'user'}
|
||||
targetElementId="yijing-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
{/* 下载和AI解读按钮 */}
|
||||
<div className="flex justify-between items-start no-export" data-no-export>
|
||||
<div className="flex-1">
|
||||
<AIInterpretationButton
|
||||
analysisData={analysisData}
|
||||
analysisType="yijing"
|
||||
analysisId={`yijing-${question || 'general'}-${Date.now()}`}
|
||||
onConfigClick={() => setShowAIConfig(true)}
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="yijing"
|
||||
userName={question ? `占卜_${question.substring(0, 10)}` : 'user'}
|
||||
targetElementId="yijing-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标题和基本信息 */}
|
||||
@@ -748,10 +762,22 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 返回顶部按钮 */}
|
||||
<BackToTop />
|
||||
|
||||
{/* AI配置模态框 */}
|
||||
<AIConfigModal
|
||||
isOpen={showAIConfig}
|
||||
onClose={() => setShowAIConfig(false)}
|
||||
onConfigSaved={() => {
|
||||
setShowAIConfig(false);
|
||||
// 可以在这里添加配置保存后的逻辑
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle }
|
||||
import { ChineseLoading } from './ui/ChineseLoading';
|
||||
import { BackToTop } from './ui/BackToTop';
|
||||
import DownloadButton from './ui/DownloadButton';
|
||||
import AIInterpretationButton from './ui/AIInterpretationButton';
|
||||
import AIConfigModal from './ui/AIConfigModal';
|
||||
import { localApi } from '../lib/localApi';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
@@ -23,6 +25,7 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
||||
const [isLoading, setIsLoading] = useState(!propAnalysisData);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [analysisData, setAnalysisData] = useState<any>(propAnalysisData || null);
|
||||
const [showAIConfig, setShowAIConfig] = useState(false);
|
||||
|
||||
// 四化飞星详细解释
|
||||
const sihuaExplanations = {
|
||||
@@ -581,15 +584,26 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 space-y-8" id="ziwei-analysis-content" data-export-content>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
<div className="flex justify-end no-export" data-no-export>
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="ziwei"
|
||||
userName={birthDate.name}
|
||||
targetElementId="ziwei-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
{/* 下载和AI解读按钮 */}
|
||||
<div className="flex justify-between items-start no-export" data-no-export>
|
||||
<div className="flex-1">
|
||||
<AIInterpretationButton
|
||||
analysisData={analysisData}
|
||||
analysisType="ziwei"
|
||||
analysisId={`ziwei-${birthDate.date}-${birthDate.time}`}
|
||||
onConfigClick={() => setShowAIConfig(true)}
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<DownloadButton
|
||||
analysisData={analysisData}
|
||||
analysisType="ziwei"
|
||||
userName={birthDate.name}
|
||||
targetElementId="ziwei-analysis-content"
|
||||
className="sticky top-4 z-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标题和基本信息 */}
|
||||
@@ -1562,10 +1576,22 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 回到顶部按钮 */}
|
||||
<BackToTop />
|
||||
|
||||
{/* AI配置模态框 */}
|
||||
<AIConfigModal
|
||||
isOpen={showAIConfig}
|
||||
onClose={() => setShowAIConfig(false)}
|
||||
onConfigSaved={() => {
|
||||
setShowAIConfig(false);
|
||||
// 可以在这里添加配置保存后的逻辑
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
353
src/components/ui/AIConfigModal.tsx
Normal file
353
src/components/ui/AIConfigModal.tsx
Normal 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;
|
||||
474
src/components/ui/AIInterpretationButton.tsx
Normal file
474
src/components/ui/AIInterpretationButton.tsx
Normal 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 Key、API地址等信息才能使用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;
|
||||
108
src/config/aiConfig.ts
Normal file
108
src/config/aiConfig.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// AI解读功能配置文件
|
||||
export interface AIConfig {
|
||||
apiKey: string;
|
||||
apiUrl: string;
|
||||
modelName: string;
|
||||
maxTokens: number;
|
||||
temperature: number;
|
||||
timeout: number;
|
||||
stream: boolean;
|
||||
}
|
||||
|
||||
// 默认AI配置
|
||||
export const defaultAIConfig: AIConfig = {
|
||||
apiKey: import.meta.env.VITE_AI_API_KEY || 'dee444451bdf4232920a88ef430ce753.Z4SAbECrSnf5JMq7',
|
||||
apiUrl: import.meta.env.VITE_AI_API_URL || 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
|
||||
modelName: import.meta.env.VITE_AI_MODEL_NAME || 'GLM-4.5',
|
||||
maxTokens: parseInt(import.meta.env.VITE_AI_MAX_TOKENS || '50000'),
|
||||
temperature: parseFloat(import.meta.env.VITE_AI_TEMPERATURE || '0.6'),
|
||||
timeout: parseInt(import.meta.env.VITE_AI_TIMEOUT || '120000'),
|
||||
stream: import.meta.env.VITE_AI_STREAM === 'false' ? false : true
|
||||
};
|
||||
|
||||
// AI解读提示词模板
|
||||
export const aiPromptTemplates = {
|
||||
bazi: `你是一位专业的八字命理大师,请对以下八字分析结果进行深度解读和补充说明。
|
||||
|
||||
请从以下几个方面进行解读:
|
||||
1. 命格特点的深层含义
|
||||
2. 五行平衡对人生的具体影响
|
||||
3. 大运流年的关键转折点
|
||||
4. 实用的人生建议和注意事项
|
||||
5. 现代生活中的应用指导
|
||||
|
||||
请用通俗易懂的语言,结合现代生活实际,给出具有指导意义的解读。
|
||||
|
||||
八字分析结果:
|
||||
{analysisContent}
|
||||
|
||||
请提供详细的AI解读:`,
|
||||
|
||||
ziwei: `你是一位资深的紫微斗数专家,请对以下紫微斗数分析结果进行专业解读。
|
||||
|
||||
请重点分析:
|
||||
1. 命宫主星的性格特质解析
|
||||
2. 十二宫位的相互影响
|
||||
3. 大限小限的运势变化
|
||||
4. 桃花、财帛、事业等重点宫位分析
|
||||
5. 现实生活中的应用建议
|
||||
|
||||
请结合现代社会背景,提供实用的人生指导。
|
||||
|
||||
紫微斗数分析结果:
|
||||
{analysisContent}
|
||||
|
||||
请提供专业的AI解读:`,
|
||||
|
||||
yijing: `你是一位精通易经的占卜大师,请对以下易经占卜结果进行深入解读。
|
||||
|
||||
请从以下角度分析:
|
||||
1. 卦象的深层寓意
|
||||
2. 爻辞的具体指导意义
|
||||
3. 变卦的发展趋势
|
||||
4. 针对问题的具体建议
|
||||
5. 行动时机和注意事项
|
||||
|
||||
请用现代语言解释古典智慧,提供切实可行的指导。
|
||||
|
||||
易经占卜结果:
|
||||
{analysisContent}
|
||||
|
||||
请提供智慧的AI解读:`
|
||||
};
|
||||
|
||||
// 获取AI配置
|
||||
export const getAIConfig = (): AIConfig => {
|
||||
// 可以从localStorage或其他存储中读取用户自定义配置
|
||||
const savedConfig = localStorage.getItem('ai-config');
|
||||
if (savedConfig) {
|
||||
try {
|
||||
const parsedConfig = JSON.parse(savedConfig);
|
||||
return { ...defaultAIConfig, ...parsedConfig };
|
||||
} catch (error) {
|
||||
console.warn('解析AI配置失败,使用默认配置:', error);
|
||||
}
|
||||
}
|
||||
return defaultAIConfig;
|
||||
};
|
||||
|
||||
// 保存AI配置
|
||||
export const saveAIConfig = (config: Partial<AIConfig>): void => {
|
||||
try {
|
||||
const currentConfig = getAIConfig();
|
||||
const newConfig = { ...currentConfig, ...config };
|
||||
localStorage.setItem('ai-config', JSON.stringify(newConfig));
|
||||
} catch (error) {
|
||||
console.error('保存AI配置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证AI配置
|
||||
export const validateAIConfig = (config: AIConfig): boolean => {
|
||||
return !!(config.apiKey && config.apiUrl && config.modelName);
|
||||
};
|
||||
|
||||
// 获取提示词模板
|
||||
export const getPromptTemplate = (analysisType: 'bazi' | 'ziwei' | 'yijing'): string => {
|
||||
return aiPromptTemplates[analysisType] || aiPromptTemplates.bazi;
|
||||
};
|
||||
@@ -170,6 +170,7 @@ const HistoryPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<AnalysisResultDisplay
|
||||
analysisResult={selectedReading.analysis}
|
||||
analysisType={selectedReading.reading_type as 'bazi' | 'ziwei' | 'yijing'}
|
||||
@@ -186,6 +187,7 @@ const HistoryPage: React.FC = () => {
|
||||
getInputDataValue(selectedReading.input_data, 'divination_method', 'time') : undefined}
|
||||
preAnalysisData={selectedReading.analysis}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -272,6 +274,7 @@ const HistoryPage: React.FC = () => {
|
||||
<Eye className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span className="hidden sm:inline">查看</span>
|
||||
</ChineseButton>
|
||||
|
||||
<DownloadButton
|
||||
analysisData={{
|
||||
...(reading.analysis || reading.results),
|
||||
@@ -299,6 +302,8 @@ const HistoryPage: React.FC = () => {
|
||||
)}
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
536
src/services/aiInterpretationService.ts
Normal file
536
src/services/aiInterpretationService.ts
Normal file
@@ -0,0 +1,536 @@
|
||||
import { getAIConfig, validateAIConfig, getPromptTemplate } from '../config/aiConfig';
|
||||
|
||||
// AI解读结果接口
|
||||
export interface AIInterpretationResult {
|
||||
success: boolean;
|
||||
content?: string;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
model?: string;
|
||||
tokensUsed?: number;
|
||||
}
|
||||
|
||||
// AI解读请求参数
|
||||
export interface AIInterpretationRequest {
|
||||
analysisType: 'bazi' | 'ziwei' | 'yijing';
|
||||
analysisContent: string;
|
||||
customPrompt?: string;
|
||||
onStreamUpdate?: (content: string) => void; // 流式更新回调
|
||||
}
|
||||
|
||||
// 将分析结果转换为Markdown格式
|
||||
const convertAnalysisToMarkdown = (analysisData: any, analysisType: string): string => {
|
||||
try {
|
||||
let markdown = `# ${getAnalysisTitle(analysisType)}分析结果\n\n`;
|
||||
|
||||
// 根据不同分析类型生成不同的Markdown内容
|
||||
switch (analysisType) {
|
||||
case 'bazi':
|
||||
markdown += generateBaziMarkdown(analysisData);
|
||||
break;
|
||||
case 'ziwei':
|
||||
markdown += generateZiweiMarkdown(analysisData);
|
||||
break;
|
||||
case 'yijing':
|
||||
markdown += generateYijingMarkdown(analysisData);
|
||||
break;
|
||||
default:
|
||||
markdown += JSON.stringify(analysisData, null, 2);
|
||||
}
|
||||
|
||||
return markdown;
|
||||
} catch (error) {
|
||||
console.error('转换分析结果为Markdown失败:', error);
|
||||
return JSON.stringify(analysisData, null, 2);
|
||||
}
|
||||
};
|
||||
|
||||
// 生成八字分析的Markdown
|
||||
const generateBaziMarkdown = (data: any): string => {
|
||||
let markdown = '';
|
||||
|
||||
if (data.basic_info) {
|
||||
markdown += '## 基本信息\n\n';
|
||||
if (data.basic_info.bazi_chart) {
|
||||
markdown += '### 八字排盘\n';
|
||||
const chart = data.basic_info.bazi_chart;
|
||||
markdown += `- 年柱: ${chart.year_pillar?.stem}${chart.year_pillar?.branch}\n`;
|
||||
markdown += `- 月柱: ${chart.month_pillar?.stem}${chart.month_pillar?.branch}\n`;
|
||||
markdown += `- 日柱: ${chart.day_pillar?.stem}${chart.day_pillar?.branch}\n`;
|
||||
markdown += `- 时柱: ${chart.hour_pillar?.stem}${chart.hour_pillar?.branch}\n\n`;
|
||||
}
|
||||
|
||||
if (data.basic_info.pillar_interpretations) {
|
||||
markdown += '### 四柱解释\n';
|
||||
const interpretations = data.basic_info.pillar_interpretations;
|
||||
markdown += `**年柱**: ${interpretations.year_pillar}\n\n`;
|
||||
markdown += `**月柱**: ${interpretations.month_pillar}\n\n`;
|
||||
markdown += `**日柱**: ${interpretations.day_pillar}\n\n`;
|
||||
markdown += `**时柱**: ${interpretations.hour_pillar}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.geju_analysis) {
|
||||
markdown += '## 格局分析\n\n';
|
||||
markdown += `${data.geju_analysis.pattern_analysis || ''}\n\n`;
|
||||
}
|
||||
|
||||
if (data.dayun_analysis) {
|
||||
markdown += '## 大运分析\n\n';
|
||||
if (data.dayun_analysis.current_dayun) {
|
||||
markdown += `**当前大运**: ${data.dayun_analysis.current_dayun.period} (${data.dayun_analysis.current_dayun.age_range})\n`;
|
||||
markdown += `${data.dayun_analysis.current_dayun.analysis}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.life_guidance) {
|
||||
markdown += '## 人生指导\n\n';
|
||||
markdown += `${data.life_guidance.overall_summary || ''}\n\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
// 生成紫微斗数分析的Markdown
|
||||
const generateZiweiMarkdown = (data: any): string => {
|
||||
let markdown = '';
|
||||
|
||||
if (data.basic_chart) {
|
||||
markdown += '## 基本命盘\n\n';
|
||||
markdown += `${JSON.stringify(data.basic_chart, null, 2)}\n\n`;
|
||||
}
|
||||
|
||||
if (data.palace_analysis) {
|
||||
markdown += '## 宫位分析\n\n';
|
||||
markdown += `${data.palace_analysis}\n\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
// 生成易经分析的Markdown
|
||||
const generateYijingMarkdown = (data: any): string => {
|
||||
let markdown = '';
|
||||
|
||||
// 基本信息
|
||||
if (data.basic_info) {
|
||||
markdown += '## 占卜基本信息\n\n';
|
||||
if (data.basic_info.divination_data) {
|
||||
markdown += `**问题**: ${data.basic_info.divination_data.question}\n`;
|
||||
markdown += `**占卜方法**: ${data.basic_info.divination_data.method}\n`;
|
||||
markdown += `**占卜时间**: ${data.basic_info.divination_data.divination_time}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// 卦象信息
|
||||
if (data.basic_info?.hexagram_info) {
|
||||
const hexInfo = data.basic_info.hexagram_info;
|
||||
markdown += '## 卦象信息\n\n';
|
||||
|
||||
// 本卦信息
|
||||
markdown += `**本卦**: ${hexInfo.main_hexagram} (第${hexInfo.main_hexagram_number}卦)\n`;
|
||||
markdown += `**卦象符号**: ${hexInfo.main_hexagram_symbol}\n`;
|
||||
markdown += `**卦辞**: ${hexInfo.hexagram_description}\n`;
|
||||
|
||||
// 卦象结构
|
||||
if (hexInfo.hexagram_structure) {
|
||||
markdown += `**上卦**: ${hexInfo.hexagram_structure.upper_trigram}\n`;
|
||||
markdown += `**下卦**: ${hexInfo.hexagram_structure.lower_trigram}\n`;
|
||||
}
|
||||
|
||||
// 变卦信息
|
||||
if (hexInfo.changing_hexagram && hexInfo.changing_hexagram !== '无') {
|
||||
markdown += `**变卦**: ${hexInfo.changing_hexagram}\n`;
|
||||
markdown += `**变卦符号**: ${hexInfo.changing_hexagram_symbol}\n`;
|
||||
} else {
|
||||
markdown += `**变卦**: 无变卦\n`;
|
||||
}
|
||||
|
||||
markdown += '\n';
|
||||
}
|
||||
|
||||
// 详细分析
|
||||
if (data.detailed_analysis) {
|
||||
const analysis = data.detailed_analysis;
|
||||
|
||||
// 卦象分析
|
||||
if (analysis.hexagram_analysis) {
|
||||
markdown += '## 卦象分析\n\n';
|
||||
markdown += `**主要含义**: ${analysis.hexagram_analysis.primary_meaning}\n`;
|
||||
markdown += `**卦辞解释**: ${analysis.hexagram_analysis.judgment}\n`;
|
||||
markdown += `**象传**: ${analysis.hexagram_analysis.image}\n`;
|
||||
if (analysis.hexagram_analysis.trigram_analysis) {
|
||||
markdown += `**卦象分析**: ${analysis.hexagram_analysis.trigram_analysis}\n`;
|
||||
}
|
||||
markdown += '\n';
|
||||
}
|
||||
|
||||
// 动爻分析
|
||||
if (analysis.changing_lines_analysis) {
|
||||
markdown += '## 动爻分析\n\n';
|
||||
markdown += `**动爻数量**: ${analysis.changing_lines_analysis.changing_lines_count}爻\n`;
|
||||
if (analysis.changing_lines_analysis.changing_line_position) {
|
||||
markdown += `**动爻位置**: ${analysis.changing_lines_analysis.changing_line_position}\n`;
|
||||
}
|
||||
if (analysis.changing_lines_analysis.line_meanings) {
|
||||
markdown += `**爻辞含义**: ${analysis.changing_lines_analysis.line_meanings}\n`;
|
||||
}
|
||||
markdown += '\n';
|
||||
}
|
||||
|
||||
// 变卦分析
|
||||
if (analysis.changing_hexagram_analysis) {
|
||||
markdown += '## 变卦分析\n\n';
|
||||
markdown += `**变化含义**: ${analysis.changing_hexagram_analysis.meaning}\n`;
|
||||
markdown += `**转化洞察**: ${analysis.changing_hexagram_analysis.transformation_insight}\n`;
|
||||
markdown += `**指导建议**: ${analysis.changing_hexagram_analysis.guidance}\n`;
|
||||
markdown += `**时机把握**: ${analysis.changing_hexagram_analysis.timing}\n`;
|
||||
markdown += '\n';
|
||||
}
|
||||
|
||||
// 高级分析(互卦、错卦、综卦)
|
||||
if (analysis.advanced_analysis) {
|
||||
markdown += '## 高级卦象分析\n\n';
|
||||
|
||||
if (analysis.advanced_analysis.inter_hexagram) {
|
||||
markdown += `**互卦**: ${analysis.advanced_analysis.inter_hexagram.name}\n`;
|
||||
markdown += `互卦分析: ${analysis.advanced_analysis.inter_hexagram.analysis}\n\n`;
|
||||
}
|
||||
|
||||
if (analysis.advanced_analysis.opposite_hexagram) {
|
||||
markdown += `**错卦**: ${analysis.advanced_analysis.opposite_hexagram.name}\n`;
|
||||
markdown += `错卦分析: ${analysis.advanced_analysis.opposite_hexagram.analysis}\n\n`;
|
||||
}
|
||||
|
||||
if (analysis.advanced_analysis.reverse_hexagram) {
|
||||
markdown += `**综卦**: ${analysis.advanced_analysis.reverse_hexagram.name}\n`;
|
||||
markdown += `综卦分析: ${analysis.advanced_analysis.reverse_hexagram.analysis}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// 五行分析
|
||||
if (analysis.hexagram_analysis?.five_elements) {
|
||||
const elements = analysis.hexagram_analysis.five_elements;
|
||||
markdown += '## 五行分析\n\n';
|
||||
markdown += `**上卦五行**: ${elements.upper_element}\n`;
|
||||
markdown += `**下卦五行**: ${elements.lower_element}\n`;
|
||||
markdown += `**五行关系**: ${elements.relationship}\n`;
|
||||
markdown += `**五行平衡**: ${elements.balance}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// 综合解读
|
||||
if (data.comprehensive_interpretation) {
|
||||
markdown += '## 综合解读\n\n';
|
||||
markdown += `${data.comprehensive_interpretation}\n\n`;
|
||||
}
|
||||
|
||||
// 实用建议
|
||||
if (data.practical_guidance) {
|
||||
markdown += '## 实用建议\n\n';
|
||||
if (data.practical_guidance.immediate_actions) {
|
||||
markdown += `**近期行动**: ${data.practical_guidance.immediate_actions}\n`;
|
||||
}
|
||||
if (data.practical_guidance.long_term_strategy) {
|
||||
markdown += `**长期策略**: ${data.practical_guidance.long_term_strategy}\n`;
|
||||
}
|
||||
if (data.practical_guidance.timing_advice) {
|
||||
markdown += `**时机建议**: ${data.practical_guidance.timing_advice}\n`;
|
||||
}
|
||||
markdown += '\n';
|
||||
}
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
// 获取分析类型标题
|
||||
const getAnalysisTitle = (analysisType: string): string => {
|
||||
const titles = {
|
||||
'bazi': '八字命理',
|
||||
'ziwei': '紫微斗数',
|
||||
'yijing': '易经占卜'
|
||||
};
|
||||
return titles[analysisType as keyof typeof titles] || '命理';
|
||||
};
|
||||
|
||||
// 调用AI API进行解读
|
||||
export const requestAIInterpretation = async (request: AIInterpretationRequest): Promise<AIInterpretationResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 获取AI配置
|
||||
const config = getAIConfig();
|
||||
|
||||
// 验证配置
|
||||
if (!validateAIConfig(config)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'AI配置不完整,请检查API Key、API地址和模型名称设置',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// 转换分析内容为Markdown
|
||||
const analysisMarkdown = typeof request.analysisContent === 'string'
|
||||
? request.analysisContent
|
||||
: convertAnalysisToMarkdown(request.analysisContent, request.analysisType);
|
||||
|
||||
console.log('🔄 分析内容转换为Markdown:', {
|
||||
originalType: typeof request.analysisContent,
|
||||
markdownLength: analysisMarkdown.length,
|
||||
preview: analysisMarkdown.substring(0, 200) + '...'
|
||||
});
|
||||
|
||||
// 获取提示词模板
|
||||
const promptTemplate = request.customPrompt || getPromptTemplate(request.analysisType);
|
||||
const prompt = promptTemplate.replace('{analysisContent}', analysisMarkdown);
|
||||
|
||||
console.log('📝 构建AI提示词:', {
|
||||
templateLength: promptTemplate.length,
|
||||
finalPromptLength: prompt.length,
|
||||
analysisType: request.analysisType
|
||||
});
|
||||
|
||||
// 构建请求体
|
||||
const requestBody = {
|
||||
model: config.modelName,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
max_tokens: config.maxTokens,
|
||||
temperature: config.temperature,
|
||||
stream: config.stream
|
||||
};
|
||||
|
||||
console.log('🚀 准备发送API请求:', {
|
||||
url: config.apiUrl,
|
||||
model: config.modelName,
|
||||
maxTokens: config.maxTokens,
|
||||
temperature: config.temperature,
|
||||
timeout: config.timeout,
|
||||
messageLength: prompt.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log('⏰ 请求超时,正在中止请求...');
|
||||
controller.abort();
|
||||
}, config.timeout);
|
||||
|
||||
const requestStartTime = Date.now();
|
||||
console.log('📡 开始发送HTTP请求...', {
|
||||
method: 'POST',
|
||||
url: config.apiUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey.substring(0, 10)}...`
|
||||
},
|
||||
bodySize: JSON.stringify(requestBody).length
|
||||
});
|
||||
|
||||
const response = await fetch(config.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
const requestDuration = Date.now() - requestStartTime;
|
||||
|
||||
console.log('📨 收到HTTP响应:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
duration: `${requestDuration}ms`,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
console.log('❌ API错误响应:', errorData);
|
||||
} catch (parseError) {
|
||||
console.log('❌ 无法解析错误响应:', parseError);
|
||||
errorData = {};
|
||||
}
|
||||
|
||||
const errorMessage = `API请求失败: ${response.status} ${response.statusText}. ${errorData.error?.message || ''}`;
|
||||
console.log('❌ 请求失败:', errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
let content = '';
|
||||
let tokensUsed = 0;
|
||||
let model = config.modelName;
|
||||
|
||||
if (config.stream) {
|
||||
// 处理流式响应
|
||||
console.log('📡 开始处理流式响应...');
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error('无法获取响应流');
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
console.log('📡 流式响应完成');
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 保留不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue;
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
if (data === '[DONE]') {
|
||||
console.log('📡 收到流式结束标记');
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.choices && parsed.choices[0] && parsed.choices[0].delta) {
|
||||
const delta = parsed.choices[0].delta;
|
||||
if (delta.content) {
|
||||
content += delta.content;
|
||||
// 调用流式更新回调
|
||||
if (request.onStreamUpdate) {
|
||||
request.onStreamUpdate(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取使用情况和模型信息
|
||||
if (parsed.usage) {
|
||||
tokensUsed = parsed.usage.total_tokens;
|
||||
}
|
||||
if (parsed.model) {
|
||||
model = parsed.model;
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('解析流式数据失败:', parseError, 'data:', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
console.log('📄 流式AI解读完成:', {
|
||||
contentLength: content.length,
|
||||
tokensUsed,
|
||||
model,
|
||||
totalDuration: `${Date.now() - startTime}ms`
|
||||
});
|
||||
|
||||
} else {
|
||||
// 处理非流式响应
|
||||
const data = await response.json();
|
||||
|
||||
console.log('✅ AI API成功响应:', {
|
||||
id: data.id,
|
||||
object: data.object,
|
||||
created: data.created,
|
||||
model: data.model,
|
||||
usage: data.usage,
|
||||
choicesCount: data.choices?.length || 0,
|
||||
totalDuration: `${Date.now() - startTime}ms`
|
||||
});
|
||||
|
||||
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
|
||||
console.log('❌ AI响应格式异常:', data);
|
||||
throw new Error('AI响应格式异常');
|
||||
}
|
||||
|
||||
content = data.choices[0].message.content;
|
||||
tokensUsed = data.usage?.total_tokens;
|
||||
model = data.model || config.modelName;
|
||||
|
||||
console.log('📄 AI解读内容:', {
|
||||
contentLength: content?.length || 0,
|
||||
tokensUsed,
|
||||
finishReason: data.choices[0].finish_reason,
|
||||
contentPreview: content?.substring(0, 100) + '...'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
model,
|
||||
tokensUsed
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('AI解读请求失败:', error);
|
||||
|
||||
let errorMessage = '未知错误';
|
||||
if (error.name === 'AbortError') {
|
||||
errorMessage = '请求超时,请稍后重试';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 保存AI解读结果到本地存储
|
||||
export const saveAIInterpretation = (analysisId: string, result: AIInterpretationResult): void => {
|
||||
try {
|
||||
const key = `ai-interpretation-${analysisId}`;
|
||||
localStorage.setItem(key, JSON.stringify(result));
|
||||
} catch (error) {
|
||||
console.error('保存AI解读结果失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从本地存储获取AI解读结果
|
||||
export const getAIInterpretation = (analysisId: string): AIInterpretationResult | null => {
|
||||
try {
|
||||
const key = `ai-interpretation-${analysisId}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
return JSON.parse(saved);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取AI解读结果失败:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 清除AI解读结果
|
||||
export const clearAIInterpretation = (analysisId: string): void => {
|
||||
try {
|
||||
const key = `ai-interpretation-${analysisId}`;
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error('清除AI解读结果失败:', error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user