import React, { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, Input, Button, Space, Typography, message, Spin, Progress } from 'antd'; import { SendOutlined, ArrowLeftOutlined, CheckCircleOutlined, LoadingOutlined, RocketOutlined } from '@ant-design/icons'; import { inspirationApi, wizardStreamApi } from '../services/api'; import type { ApiError } from '../types'; const { Title, Text, Paragraph } = Typography; const { TextArea } = Input; type Step = 'idea' | 'title' | 'description' | 'theme' | 'genre' | 'perspective' | 'confirm' | 'generating' | 'complete'; interface Message { type: 'ai' | 'user'; content: string; options?: string[]; isMultiSelect?: boolean; } interface WizardData { title: string; description: string; theme: string; genre: string[]; narrative_perspective: string; } const Inspiration: React.FC = () => { const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState('idea'); const [messages, setMessages] = useState([ { type: 'ai', content: '你好!我是你的AI创作助手。让我们一起创作一部精彩的小说吧!\n\n请告诉我,你想写一本什么样的小说?', } ]); const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(false); const [selectedOptions, setSelectedOptions] = useState([]); // 收集的数据 const [wizardData, setWizardData] = useState>({}); // 保存用户的原始想法,用于保持上下文一致性 const [initialIdea, setInitialIdea] = useState(''); // 项目生成状态 const [projectId, setProjectId] = useState(''); const [projectTitle, setProjectTitle] = useState(''); const [progress, setProgress] = useState(0); const [progressMessage, setProgressMessage] = useState(''); const [errorDetails, setErrorDetails] = useState(''); // 新增:错误详情 const [generationSteps, setGenerationSteps] = useState<{ worldBuilding: 'pending' | 'processing' | 'completed' | 'error'; characters: 'pending' | 'processing' | 'completed' | 'error'; outline: 'pending' | 'processing' | 'completed' | 'error'; }>({ worldBuilding: 'pending', characters: 'pending', outline: 'pending' }); // 新增:保存生成数据,用于重试 const [generationData, setGenerationData] = useState(null); // 保存世界观生成结果,用于后续步骤 const [worldBuildingResult, setWorldBuildingResult] = useState(null); // 滚动容器引用 const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); // 记录上次失败的请求参数,用于重试 const [lastFailedRequest, setLastFailedRequest] = useState<{ step: 'title' | 'description' | 'theme' | 'genre'; context: Partial; } | null>(null); // 自动滚动到底部 - 使用更丝滑的方式 const scrollToBottom = () => { // 使用 setTimeout 确保 DOM 已更新 setTimeout(() => { if (chatContainerRef.current) { chatContainerRef.current.scrollTo({ top: chatContainerRef.current.scrollHeight, behavior: 'smooth' }); } }, 100); }; // 当消息更新时自动滚动 useEffect(() => { scrollToBottom(); }, [messages]); // 重试生成 const handleRetry = async () => { if (!lastFailedRequest) return; setLoading(true); try { const response = await inspirationApi.generateOptions({ step: lastFailedRequest.step, context: lastFailedRequest.context }); if (response.error) { message.error(response.error); return; } // 移除失败消息,添加成功的AI消息 setMessages(prev => { const newMessages = [...prev]; if (newMessages[newMessages.length - 1].type === 'ai' && (newMessages[newMessages.length - 1].content.includes('生成失败') || newMessages[newMessages.length - 1].content.includes('出错了'))) { newMessages.pop(); } return newMessages; }); const aiMessage: Message = { type: 'ai', content: response.prompt || '请选择一个选项,或者输入你自己的:', options: response.options || [], isMultiSelect: lastFailedRequest.step === 'genre' }; setMessages(prev => [...prev, aiMessage]); setLastFailedRequest(null); } catch (error: any) { console.error('重试失败:', error); message.error('重试失败,请稍后再试'); } finally { setLoading(false); } }; // 步骤顺序 const stepOrder: Step[] = ['idea', 'title', 'description', 'theme', 'genre', 'perspective', 'confirm']; const handleSendMessage = async () => { if (!inputValue.trim()) { message.warning('请输入内容'); return; } const userMessage: Message = { type: 'user', content: inputValue, }; setMessages(prev => [...prev, userMessage]); const userInput = inputValue; setInputValue(''); setLoading(true); try { if (currentStep === 'idea') { // 保存用户的原始想法 setInitialIdea(userInput); const requestData = { step: 'title' as const, context: { initial_idea: userInput, description: userInput } }; const response = await inspirationApi.generateOptions(requestData); // 前端格式校验:检查是否有错误或选项数量不足 if (response.error || !response.options || response.options.length < 3) { const errorMessage: Message = { type: 'ai', content: response.error ? `生成书名时出错:${response.error}\n\n你可以选择:` : `生成的选项格式不正确(至少需要3个有效选项)\n\n你可以选择:`, options: response.options && response.options.length > 0 ? response.options : ['重新生成', '我自己输入书名'] }; setMessages(prev => [...prev, errorMessage]); setLastFailedRequest(requestData); return; } const aiMessage: Message = { type: 'ai', content: response.prompt || '请选择一个书名,或者输入你自己的:', options: response.options }; setMessages(prev => [...prev, aiMessage]); setCurrentStep('title'); setLastFailedRequest(null); } else { await handleCustomInput(userInput); } } catch (error: any) { console.error('发送消息失败:', error); message.error(error.response?.data?.detail || '生成失败,请重试'); } finally { setLoading(false); } }; const handleSelectOption = async (option: string) => { if (option === '重新生成' && lastFailedRequest) { await handleRetry(); return; } if (option === '我自己输入书名' || option === '我自己输入') { message.info('请在下方输入框中输入您的内容'); return; } if (currentStep === 'genre') { const newSelected = selectedOptions.includes(option) ? selectedOptions.filter(o => o !== option) : [...selectedOptions, option]; setSelectedOptions(newSelected); return; } if (currentStep === 'perspective') { // 叙事视角是单选 const userMessage: Message = { type: 'user', content: option, }; setMessages(prev => [...prev, userMessage]); const updatedData = { ...wizardData, narrative_perspective: option, genre: wizardData.genre || [] } as WizardData; setWizardData(updatedData); // 显示预览和确认选项 const summary = ` 太棒了!你的小说设定已完成,请确认: 📖 书名:${updatedData.title} 📝 简介:${updatedData.description} 🎯 主题:${updatedData.theme} 🏷️ 类型:${updatedData.genre.join('、')} 👁️ 视角:${updatedData.narrative_perspective} 请选择下一步操作: `.trim(); const aiMessage: Message = { type: 'ai', content: summary, options: ['✅ 确认创建', '🔄 重新开始'] }; setMessages(prev => [...prev, aiMessage]); setCurrentStep('confirm'); return; } if (currentStep === 'confirm') { if (option === '✅ 确认创建') { const userMessage: Message = { type: 'user', content: '确认创建', }; setMessages(prev => [...prev, userMessage]); const aiMessage: Message = { type: 'ai', content: '好的!正在为你创建项目,这可能需要几分钟时间...' }; setMessages(prev => [...prev, aiMessage]); // 开始生成项目 await handleAutoGenerate(wizardData as WizardData); return; } else if (option === '🔄 重新开始') { handleRestart(); return; } } const userMessage: Message = { type: 'user', content: option, }; setMessages(prev => [...prev, userMessage]); setLoading(true); try { const updatedData = { ...wizardData }; if (currentStep === 'title') { updatedData.title = option; } else if (currentStep === 'description') { updatedData.description = option; } else if (currentStep === 'theme') { updatedData.theme = option; } setWizardData(updatedData); await generateNextStep(updatedData); } catch (error: any) { console.error('选择选项失败:', error); message.error(error.response?.data?.detail || '生成失败,请重试'); } finally { setLoading(false); } }; const handleCustomInput = async (input: string) => { setLoading(true); try { const updatedData = { ...wizardData }; if (currentStep === 'title') { updatedData.title = input; } else if (currentStep === 'description') { updatedData.description = input; } else if (currentStep === 'theme') { updatedData.theme = input; } else if (currentStep === 'genre') { updatedData.genre = [input]; } else if (currentStep === 'perspective') { updatedData.narrative_perspective = input; } setWizardData(updatedData); await generateNextStep(updatedData); } catch (error: any) { console.error('处理自定义输入失败:', error); message.error(error.response?.data?.detail || '处理失败,请重试'); } finally { setLoading(false); } }; // 自动化生成项目流程 const handleAutoGenerate = async (data: WizardData) => { try { setLoading(true); setCurrentStep('generating'); setProjectTitle(data.title); setProgress(0); setProgressMessage('开始创建项目...'); setErrorDetails(''); // 清空错误详情 setGenerationData(data); // 保存数据用于重试 // 步骤1: 生成世界观并创建项目 setGenerationSteps(prev => ({ ...prev, worldBuilding: 'processing' })); setProgressMessage('正在生成世界观...'); const worldResult = await wizardStreamApi.generateWorldBuildingStream( { title: data.title, description: data.description, theme: data.theme, genre: data.genre.join('、'), narrative_perspective: data.narrative_perspective, target_words: 100000, chapter_count: 5, character_count: 5, }, { onProgress: (msg, prog) => { setProgress(Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: (result) => { setProjectId(result.project_id); setWorldBuildingResult(result); // 保存世界观结果 setGenerationSteps(prev => ({ ...prev, worldBuilding: 'completed' })); }, onError: (error) => { console.error('世界观生成失败:', error); setErrorDetails(`世界观生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, worldBuilding: 'error' })); setLoading(false); // 确保错误时解除加载状态 throw new Error(error); }, onComplete: () => { console.log('世界观生成完成'); } } ); if (!worldResult?.project_id) { throw new Error('项目创建失败:未获取到项目ID'); } const createdProjectId = worldResult.project_id; setProjectId(createdProjectId); setWorldBuildingResult(worldResult); // 保存世界观结果 // 步骤2: 生成角色 setGenerationSteps(prev => ({ ...prev, characters: 'processing' })); setProgressMessage('正在生成角色...'); await wizardStreamApi.generateCharactersStream( { project_id: createdProjectId, count: 5, world_context: { time_period: worldResult.time_period || '', location: worldResult.location || '', atmosphere: worldResult.atmosphere || '', rules: worldResult.rules || '', }, theme: data.theme, genre: data.genre.join('、'), }, { onProgress: (msg, prog) => { setProgress(33 + Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: (result) => { console.log(`成功生成${result.characters?.length || 0}个角色`); setGenerationSteps(prev => ({ ...prev, characters: 'completed' })); }, onError: (error) => { console.error('角色生成失败:', error); setErrorDetails(`角色生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, characters: 'error' })); setLoading(false); // 确保错误时解除加载状态 throw new Error(error); }, onComplete: () => { console.log('角色生成完成'); } } ); // 步骤3: 生成大纲 setGenerationSteps(prev => ({ ...prev, outline: 'processing' })); setProgressMessage('正在生成大纲...'); await wizardStreamApi.generateCompleteOutlineStream( { project_id: createdProjectId, chapter_count: 5, narrative_perspective: data.narrative_perspective, target_words: 100000, }, { onProgress: (msg, prog) => { setProgress(66 + Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: () => { console.log('大纲生成完成'); setGenerationSteps(prev => ({ ...prev, outline: 'completed' })); }, onError: (error) => { console.error('大纲生成失败:', error); setErrorDetails(`大纲生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, outline: 'error' })); setLoading(false); // 确保错误时解除加载状态 throw new Error(error); }, onComplete: () => { console.log('大纲生成完成'); } } ); // 全部完成 setProgress(100); setProgressMessage('项目创建完成!'); setCurrentStep('complete'); message.success('项目创建成功!'); } catch (error) { const apiError = error as ApiError; const errorMsg = apiError.response?.data?.detail || apiError.message || '未知错误'; console.error('创建项目失败:', errorMsg); setErrorDetails(errorMsg); message.error('创建项目失败:' + errorMsg); // 不重置步骤,保持在generating状态以显示重试按钮 setLoading(false); // 确保在错误时也设置loading为false } }; // 智能重试:从失败的步骤继续生成 const handleSmartRetry = async () => { if (!generationData) { message.warning('缺少生成数据'); return; } setLoading(true); setErrorDetails(''); try { // 判断从哪个步骤开始重试 if (generationSteps.worldBuilding === 'error') { // 世界观失败,从世界观开始重新生成 message.info('从世界观步骤开始重新生成...'); await retryFromWorldBuilding(); } else if (generationSteps.characters === 'error') { // 角色失败,从角色开始生成 message.info('从角色步骤继续生成...'); await retryFromCharacters(); } else if (generationSteps.outline === 'error') { // 大纲失败,从大纲开始生成 message.info('从大纲步骤继续生成...'); await retryFromOutline(); } } catch (error: any) { console.error('智能重试失败:', error); message.error('重试失败:' + (error.message || '未知错误')); setLoading(false); } }; // 从世界观步骤重新开始 const retryFromWorldBuilding = async () => { if (!generationData) return; setGenerationSteps(prev => ({ ...prev, worldBuilding: 'processing' })); setProgressMessage('重新生成世界观...'); try { const worldResult = await wizardStreamApi.generateWorldBuildingStream( { title: generationData.title, description: generationData.description, theme: generationData.theme, genre: generationData.genre.join('、'), narrative_perspective: generationData.narrative_perspective, target_words: 100000, chapter_count: 5, character_count: 5, }, { onProgress: (msg, prog) => { setProgress(Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: (result) => { setProjectId(result.project_id); setWorldBuildingResult(result); setGenerationSteps(prev => ({ ...prev, worldBuilding: 'completed' })); }, onError: (error) => { console.error('世界观生成失败:', error); setErrorDetails(`世界观生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, worldBuilding: 'error' })); setLoading(false); throw new Error(error); }, onComplete: () => { console.log('世界观重新生成完成'); } } ); if (!worldResult?.project_id) { throw new Error('项目创建失败:未获取到项目ID'); } // 继续生成角色和大纲 await continueFromCharacters(worldResult); } catch (error: any) { throw error; } }; // 从角色步骤继续 const retryFromCharacters = async () => { if (!generationData || !projectId || !worldBuildingResult) { message.warning('缺少必要数据,无法从角色步骤继续'); setLoading(false); return; } setGenerationSteps(prev => ({ ...prev, characters: 'processing' })); setProgressMessage('重新生成角色...'); try { await wizardStreamApi.generateCharactersStream( { project_id: projectId, count: 5, world_context: { time_period: worldBuildingResult.time_period || '', location: worldBuildingResult.location || '', atmosphere: worldBuildingResult.atmosphere || '', rules: worldBuildingResult.rules || '', }, theme: generationData.theme, genre: generationData.genre.join('、'), }, { onProgress: (msg, prog) => { setProgress(33 + Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: (result) => { console.log(`成功生成${result.characters?.length || 0}个角色`); setGenerationSteps(prev => ({ ...prev, characters: 'completed' })); }, onError: (error) => { console.error('角色生成失败:', error); setErrorDetails(`角色生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, characters: 'error' })); setLoading(false); throw new Error(error); }, onComplete: () => { console.log('角色重新生成完成'); } } ); // 继续生成大纲 await continueFromOutline(); } catch (error: any) { throw error; } }; // 从大纲步骤继续 const retryFromOutline = async () => { if (!generationData || !projectId) { message.warning('缺少必要数据,无法从大纲步骤继续'); setLoading(false); return; } setGenerationSteps(prev => ({ ...prev, outline: 'processing' })); setProgressMessage('重新生成大纲...'); try { await wizardStreamApi.generateCompleteOutlineStream( { project_id: projectId, chapter_count: 5, narrative_perspective: generationData.narrative_perspective, target_words: 100000, }, { onProgress: (msg, prog) => { setProgress(66 + Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: () => { console.log('大纲生成完成'); setGenerationSteps(prev => ({ ...prev, outline: 'completed' })); }, onError: (error) => { console.error('大纲生成失败:', error); setErrorDetails(`大纲生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, outline: 'error' })); setLoading(false); throw new Error(error); }, onComplete: () => { console.log('大纲重新生成完成'); } } ); // 全部完成 setProgress(100); setProgressMessage('项目创建完成!'); setCurrentStep('complete'); message.success('项目创建成功!'); setLoading(false); } catch (error: any) { throw error; } }; // 从角色步骤开始的完整流程(世界观成功后调用) const continueFromCharacters = async (worldResult: any) => { if (!generationData || !worldResult?.project_id) return; try { // 生成角色 setGenerationSteps(prev => ({ ...prev, characters: 'processing' })); setProgressMessage('正在生成角色...'); await wizardStreamApi.generateCharactersStream( { project_id: worldResult.project_id, count: 5, world_context: { time_period: worldResult.time_period || '', location: worldResult.location || '', atmosphere: worldResult.atmosphere || '', rules: worldResult.rules || '', }, theme: generationData.theme, genre: generationData.genre.join('、'), }, { onProgress: (msg, prog) => { setProgress(33 + Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: (result) => { console.log(`成功生成${result.characters?.length || 0}个角色`); setGenerationSteps(prev => ({ ...prev, characters: 'completed' })); }, onError: (error) => { console.error('角色生成失败:', error); setErrorDetails(`角色生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, characters: 'error' })); setLoading(false); throw new Error(error); }, onComplete: () => { console.log('角色生成完成'); } } ); // 生成大纲 await continueFromOutline(); } catch (error: any) { console.error('继续生成失败:', error); throw error; } }; // 从大纲步骤开始的完整流程(角色成功后调用) const continueFromOutline = async () => { if (!generationData || !projectId) return; setGenerationSteps(prev => ({ ...prev, outline: 'processing' })); setProgressMessage('正在生成大纲...'); await wizardStreamApi.generateCompleteOutlineStream( { project_id: projectId, chapter_count: 5, narrative_perspective: generationData.narrative_perspective, target_words: 100000, }, { onProgress: (msg, prog) => { setProgress(66 + Math.floor(prog / 3)); setProgressMessage(msg); }, onResult: () => { console.log('大纲生成完成'); setGenerationSteps(prev => ({ ...prev, outline: 'completed' })); }, onError: (error) => { console.error('大纲生成失败:', error); setErrorDetails(`大纲生成失败: ${error}`); setGenerationSteps(prev => ({ ...prev, outline: 'error' })); setLoading(false); throw new Error(error); }, onComplete: () => { console.log('大纲生成完成'); } } ); // 全部完成 setProgress(100); setProgressMessage('项目创建完成!'); setCurrentStep('complete'); message.success('项目创建成功!'); setLoading(false); }; const handleConfirmGenres = async () => { if (selectedOptions.length === 0) { message.warning('请至少选择一个类型'); return; } const userMessage: Message = { type: 'user', content: selectedOptions.join('、'), }; setMessages(prev => [...prev, userMessage]); const updatedData = { ...wizardData, genre: selectedOptions }; setWizardData(updatedData); setSelectedOptions([]); // 进入叙事视角选择 setLoading(true); try { const aiMessage: Message = { type: 'ai', content: '很好!最后一步,请选择小说的叙事视角:', options: ['第一人称', '第三人称', '全知视角'] }; setMessages(prev => [...prev, aiMessage]); setCurrentStep('perspective'); } finally { setLoading(false); } }; const generateNextStep = async (data: Partial) => { const currentIndex = stepOrder.indexOf(currentStep); const nextStep = stepOrder[currentIndex + 1]; if (nextStep === 'description') { const requestData = { step: 'description' as const, context: { initial_idea: initialIdea, title: data.title } }; const response = await inspirationApi.generateOptions(requestData); // 前端格式校验 if (response.error || !response.options || response.options.length < 3) { const errorMessage: Message = { type: 'ai', content: response.error ? `生成简介时出错:${response.error}\n\n你可以选择:` : `生成的选项格式不正确(至少需要3个有效选项)\n\n你可以选择:`, options: response.options && response.options.length > 0 ? response.options : ['重新生成', '我自己输入'] }; setMessages(prev => [...prev, errorMessage]); setLastFailedRequest(requestData); return; } const aiMessage: Message = { type: 'ai', content: response.prompt || '请选择一个简介,或者输入你自己的:', options: response.options }; setMessages(prev => [...prev, aiMessage]); setCurrentStep('description'); setLastFailedRequest(null); } else if (nextStep === 'theme') { const requestData = { step: 'theme' as const, context: { initial_idea: initialIdea, title: data.title, description: data.description } }; const response = await inspirationApi.generateOptions(requestData); // 前端格式校验 if (response.error || !response.options || response.options.length < 3) { const errorMessage: Message = { type: 'ai', content: response.error ? `生成主题时出错:${response.error}\n\n你可以选择:` : `生成的选项格式不正确(至少需要3个有效选项)\n\n你可以选择:`, options: response.options && response.options.length > 0 ? response.options : ['重新生成', '我自己输入'] }; setMessages(prev => [...prev, errorMessage]); setLastFailedRequest(requestData); return; } const aiMessage: Message = { type: 'ai', content: response.prompt || '请选择一个主题,或者输入你自己的:', options: response.options }; setMessages(prev => [...prev, aiMessage]); setCurrentStep('theme'); setLastFailedRequest(null); } else if (nextStep === 'genre') { const requestData = { step: 'genre' as const, context: { initial_idea: initialIdea, title: data.title, description: data.description, theme: data.theme } }; const response = await inspirationApi.generateOptions(requestData); // 前端格式校验 if (response.error || !response.options || response.options.length < 3) { const errorMessage: Message = { type: 'ai', content: response.error ? `生成类型时出错:${response.error}\n\n你可以选择:` : `生成的选项格式不正确(至少需要3个有效选项)\n\n你可以选择:`, options: response.options && response.options.length > 0 ? response.options : ['重新生成', '我自己输入'], isMultiSelect: false }; setMessages(prev => [...prev, errorMessage]); setLastFailedRequest(requestData); return; } const aiMessage: Message = { type: 'ai', content: response.prompt || '请选择类型标签(可多选):', options: response.options, isMultiSelect: true }; setMessages(prev => [...prev, aiMessage]); setCurrentStep('genre'); setLastFailedRequest(null); } }; const handleRestart = () => { setCurrentStep('idea'); setMessages([ { type: 'ai', content: '好的,让我们重新开始!\n\n请告诉我,你想写一本什么样的小说?', } ]); setWizardData({}); setInitialIdea(''); // 重置原始想法 setSelectedOptions([]); setLoading(false); }; const handleBack = () => { navigate('/projects'); }; // 渲染生成进度页面 const renderGenerating = () => { const getStepStatus = (step: 'pending' | 'processing' | 'completed' | 'error') => { if (step === 'completed') return { icon: , color: '#52c41a' }; if (step === 'processing') return { icon: , color: '#1890ff' }; if (step === 'error') return { icon: '✗', color: '#ff4d4f' }; return { icon: '○', color: '#d9d9d9' }; }; const hasError = generationSteps.worldBuilding === 'error' || generationSteps.characters === 'error' || generationSteps.outline === 'error'; return (
正在为《{projectTitle}》生成内容 {progressMessage} {/* 错误详情显示 */} {errorDetails && ( 错误详情:
{errorDetails}
)} {[ { key: 'worldBuilding', label: '生成世界观', step: generationSteps.worldBuilding }, { key: 'characters', label: '生成角色', step: generationSteps.characters }, { key: 'outline', label: '生成大纲', step: generationSteps.outline }, ].map(({ key, label, step }) => { const status = getStepStatus(step); return (
{label} {status.icon}
); })}
{hasError ? '生成过程中出现错误,请点击重试按钮重新生成' : '请耐心等待,AI正在为您精心创作...'} {hasError && ( )}
); }; // 渲染完成页面 const renderComplete = () => (
项目创建完成! 《{projectTitle}》已成功创建,包含完整的世界观、角色和开局大纲
); // 渲染对话界面 const renderChat = () => ( <> {/* 对话区域 */} {messages.map((msg, index) => (
{msg.content} {/* 选项卡片 */} {msg.options && msg.options.length > 0 && ( {msg.options.map((option, optIndex) => ( handleSelectOption(option)} style={{ cursor: 'pointer', border: msg.isMultiSelect && selectedOptions.includes(option) ? '2px solid #1890ff' : '1px solid #d9d9d9', background: msg.isMultiSelect && selectedOptions.includes(option) ? '#e6f7ff' : '#fff', animation: 'floatIn 0.6s ease-out', animationDelay: `${optIndex * 0.1}s`, animationFillMode: 'both', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-2px) scale(1.02)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(24,144,255,0.2)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0) scale(1)'; e.currentTarget.style.boxShadow = 'none'; }} > {option} ))} {/* 多选确认按钮 */} {msg.isMultiSelect && ( )} )}
))} {loading && (
)} {/* 滚动锚点 */}
{/* 输入区域 */}