2026-01-14 14:33:43 +08:00
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
2025-11-14 17:16:24 +08:00
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-12-31 11:57:21 +08:00
|
|
|
import { Card, Input, Button, Space, Typography, message, Spin, Modal } from 'antd';
|
|
|
|
|
import { SendOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
|
2025-11-26 14:56:13 +08:00
|
|
|
import { inspirationApi } from '../services/api';
|
|
|
|
|
import { AIProjectGenerator, type GenerationConfig } from '../components/AIProjectGenerator';
|
2025-11-14 17:16:24 +08:00
|
|
|
|
|
|
|
|
const { Title, Text, Paragraph } = Typography;
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
type Step = 'idea' | 'title' | 'description' | 'theme' | 'genre' | 'perspective' | 'outline_mode' | 'confirm' | 'generating' | 'complete';
|
2025-11-14 17:16:24 +08:00
|
|
|
|
|
|
|
|
interface Message {
|
|
|
|
|
type: 'ai' | 'user';
|
|
|
|
|
content: string;
|
|
|
|
|
options?: string[];
|
|
|
|
|
isMultiSelect?: boolean;
|
2025-11-27 17:29:23 +08:00
|
|
|
optionsDisabled?: boolean; // 标记选项是否已禁用
|
2025-12-28 19:35:23 +08:00
|
|
|
canRefine?: boolean; // 是否可以优化(用于支持多轮对话)
|
|
|
|
|
step?: Step; // 当前步骤(用于反馈)
|
2025-11-14 17:16:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface WizardData {
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
theme: string;
|
|
|
|
|
genre: string[];
|
|
|
|
|
narrative_perspective: string;
|
2025-11-27 17:29:23 +08:00
|
|
|
outline_mode: 'one-to-one' | 'one-to-many';
|
2025-11-14 17:16:24 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 缓存数据接口
|
|
|
|
|
interface CacheData {
|
|
|
|
|
messages: Message[];
|
|
|
|
|
currentStep: Step;
|
|
|
|
|
wizardData: Partial<WizardData>;
|
|
|
|
|
initialIdea: string;
|
|
|
|
|
selectedOptions: string[];
|
2025-12-31 11:57:21 +08:00
|
|
|
lastFailedRequest: {
|
|
|
|
|
step: 'title' | 'description' | 'theme' | 'genre';
|
|
|
|
|
context: Partial<WizardData>;
|
|
|
|
|
} | null;
|
2025-11-27 17:29:23 +08:00
|
|
|
timestamp: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 缓存键
|
|
|
|
|
const CACHE_KEY = 'inspiration_conversation_cache';
|
|
|
|
|
// 缓存有效期:24小时
|
|
|
|
|
const CACHE_EXPIRY = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
const Inspiration: React.FC = () => {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const [currentStep, setCurrentStep] = useState<Step>('idea');
|
2025-12-11 17:01:25 +08:00
|
|
|
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
setIsMobile(window.innerWidth <= 768);
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
const [messages, setMessages] = useState<Message[]>([
|
|
|
|
|
{
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: '你好!我是你的AI创作助手。让我们一起创作一部精彩的小说吧!\n\n请告诉我,你想写一本什么样的小说?',
|
|
|
|
|
}
|
|
|
|
|
]);
|
|
|
|
|
const [inputValue, setInputValue] = useState('');
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
// 收集的数据
|
|
|
|
|
const [wizardData, setWizardData] = useState<Partial<WizardData>>({});
|
2025-11-17 16:00:07 +08:00
|
|
|
// 保存用户的原始想法,用于保持上下文一致性
|
|
|
|
|
const [initialIdea, setInitialIdea] = useState<string>('');
|
2025-12-28 19:35:23 +08:00
|
|
|
|
|
|
|
|
// 反馈相关状态
|
|
|
|
|
const [feedbackValue, setFeedbackValue] = useState('');
|
|
|
|
|
const [showFeedbackInput, setShowFeedbackInput] = useState<number | null>(null); // 当前显示反馈输入的消息索引
|
|
|
|
|
const [refining, setRefining] = useState(false); // 正在优化选项
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-26 14:56:13 +08:00
|
|
|
// 生成配置
|
|
|
|
|
const [generationConfig, setGenerationConfig] = useState<GenerationConfig | null>(null);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-12-31 11:57:21 +08:00
|
|
|
// Modal hook
|
|
|
|
|
const [modal, contextHolder] = Modal.useModal();
|
|
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
// 滚动容器引用
|
|
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
// 记录上次失败的请求参数,用于重试
|
|
|
|
|
const [lastFailedRequest, setLastFailedRequest] = useState<{
|
|
|
|
|
step: 'title' | 'description' | 'theme' | 'genre';
|
|
|
|
|
context: Partial<WizardData>;
|
|
|
|
|
} | null>(null);
|
|
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 标记是否已经加载缓存
|
|
|
|
|
const [cacheLoaded, setCacheLoaded] = useState(false);
|
|
|
|
|
|
|
|
|
|
// ==================== 缓存管理函数 ====================
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2026-01-14 14:33:43 +08:00
|
|
|
// 清除缓存
|
|
|
|
|
const clearCache = useCallback(() => {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.removeItem(CACHE_KEY);
|
|
|
|
|
console.log('🗑️ 缓存已清除');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('清除缓存失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 保存到缓存
|
2026-01-14 14:33:43 +08:00
|
|
|
const saveToCache = useCallback(() => {
|
2025-11-27 17:29:23 +08:00
|
|
|
try {
|
|
|
|
|
// 只在对话阶段保存,生成阶段不保存
|
|
|
|
|
if (currentStep === 'generating' || currentStep === 'complete') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 只有用户有输入时才保存(至少两条消息:AI问候+用户回复)
|
|
|
|
|
if (messages.length <= 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cacheData: CacheData = {
|
|
|
|
|
messages,
|
|
|
|
|
currentStep,
|
|
|
|
|
wizardData,
|
|
|
|
|
initialIdea,
|
|
|
|
|
selectedOptions,
|
2025-12-31 11:57:21 +08:00
|
|
|
lastFailedRequest,
|
2025-11-27 17:29:23 +08:00
|
|
|
timestamp: Date.now()
|
|
|
|
|
};
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
|
|
|
|
|
console.log('💾 对话已自动保存');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存缓存失败:', error);
|
|
|
|
|
}
|
2026-01-14 14:33:43 +08:00
|
|
|
}, [currentStep, messages, wizardData, initialIdea, selectedOptions, lastFailedRequest]);
|
2025-11-27 17:29:23 +08:00
|
|
|
|
|
|
|
|
// 从缓存恢复
|
2026-01-14 14:33:43 +08:00
|
|
|
const restoreFromCache = useCallback((): boolean => {
|
2025-11-27 17:29:23 +08:00
|
|
|
try {
|
|
|
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
|
|
|
if (!cached) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cacheData: CacheData = JSON.parse(cached);
|
|
|
|
|
const age = Date.now() - cacheData.timestamp;
|
|
|
|
|
|
|
|
|
|
// 检查缓存是否过期
|
|
|
|
|
if (age > CACHE_EXPIRY) {
|
|
|
|
|
console.log('⏰ 缓存已过期,清除');
|
|
|
|
|
clearCache();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 必须有有效的对话数据
|
|
|
|
|
if (!cacheData.messages || cacheData.messages.length <= 1) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 恢复所有状态
|
|
|
|
|
setMessages(cacheData.messages);
|
|
|
|
|
setCurrentStep(cacheData.currentStep);
|
|
|
|
|
setWizardData(cacheData.wizardData);
|
|
|
|
|
setInitialIdea(cacheData.initialIdea);
|
|
|
|
|
setSelectedOptions(cacheData.selectedOptions);
|
2025-12-31 11:57:21 +08:00
|
|
|
// 恢复失败请求信息,确保"重新生成"按钮可用
|
|
|
|
|
if (cacheData.lastFailedRequest) {
|
|
|
|
|
setLastFailedRequest(cacheData.lastFailedRequest);
|
|
|
|
|
}
|
2025-11-27 17:29:23 +08:00
|
|
|
|
|
|
|
|
console.log('✅ 已恢复上次的对话进度');
|
|
|
|
|
message.success('已恢复上次的对话进度', 2);
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('恢复缓存失败:', error);
|
|
|
|
|
clearCache();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-14 14:33:43 +08:00
|
|
|
}, [clearCache]);
|
2025-11-27 17:29:23 +08:00
|
|
|
|
|
|
|
|
// ==================== 组件挂载时恢复缓存 ====================
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!cacheLoaded) {
|
|
|
|
|
restoreFromCache();
|
|
|
|
|
setCacheLoaded(true);
|
|
|
|
|
}
|
2026-01-14 14:33:43 +08:00
|
|
|
}, [cacheLoaded, restoreFromCache]);
|
2025-11-27 17:29:23 +08:00
|
|
|
|
|
|
|
|
// ==================== 自动保存:状态变化时保存 ====================
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
// 防抖保存
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
if (cacheLoaded) {
|
|
|
|
|
saveToCache();
|
|
|
|
|
}
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
2026-01-14 14:33:43 +08:00
|
|
|
}, [messages, currentStep, wizardData, initialIdea, selectedOptions, lastFailedRequest, cacheLoaded, saveToCache]);
|
2025-11-27 17:29:23 +08:00
|
|
|
|
2025-11-26 14:56:13 +08:00
|
|
|
// 自动滚动到底部
|
2025-11-14 17:16:24 +08:00
|
|
|
const scrollToBottom = () => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (chatContainerRef.current) {
|
|
|
|
|
chatContainerRef.current.scrollTo({
|
|
|
|
|
top: chatContainerRef.current.scrollHeight,
|
|
|
|
|
behavior: 'smooth'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 当消息更新时自动滚动
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}, [messages]);
|
|
|
|
|
|
|
|
|
|
// 重试生成
|
|
|
|
|
const handleRetry = async () => {
|
|
|
|
|
if (!lastFailedRequest) return;
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await inspirationApi.generateOptions({
|
|
|
|
|
step: lastFailedRequest.step,
|
|
|
|
|
context: lastFailedRequest.context
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.error) {
|
|
|
|
|
message.error(response.error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setMessages(prev => {
|
|
|
|
|
const newMessages = [...prev];
|
|
|
|
|
if (newMessages[newMessages.length - 1].type === 'ai' &&
|
2025-12-11 17:01:25 +08:00
|
|
|
(newMessages[newMessages.length - 1].content.includes('生成失败') ||
|
|
|
|
|
newMessages[newMessages.length - 1].content.includes('出错了'))) {
|
2025-11-14 17:16:24 +08:00
|
|
|
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);
|
2026-01-14 14:33:43 +08:00
|
|
|
} catch (error: unknown) {
|
2025-11-14 17:16:24 +08:00
|
|
|
console.error('重试失败:', error);
|
|
|
|
|
message.error('重试失败,请稍后再试');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-28 19:35:23 +08:00
|
|
|
// 处理用户反馈,重新生成选项
|
|
|
|
|
const handleRefineOptions = async (messageIndex: number, feedback: string) => {
|
|
|
|
|
if (!feedback.trim()) {
|
|
|
|
|
message.warning('请输入您的反馈意见');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetMessage = messages[messageIndex];
|
|
|
|
|
if (!targetMessage.options || !targetMessage.step) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setRefining(true);
|
|
|
|
|
setShowFeedbackInput(null);
|
|
|
|
|
setFeedbackValue('');
|
|
|
|
|
|
|
|
|
|
// 先禁用旧的选项
|
|
|
|
|
setMessages(prev => {
|
|
|
|
|
const newMessages = [...prev];
|
|
|
|
|
if (newMessages[messageIndex]) {
|
|
|
|
|
newMessages[messageIndex] = {
|
|
|
|
|
...newMessages[messageIndex],
|
|
|
|
|
optionsDisabled: true,
|
|
|
|
|
canRefine: false, // 同时禁用反馈功能
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return newMessages;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 添加用户反馈消息
|
|
|
|
|
const feedbackMessage: Message = {
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: `💭 ${feedback}`,
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, feedbackMessage]);
|
|
|
|
|
|
|
|
|
|
const step = targetMessage.step as 'title' | 'description' | 'theme' | 'genre';
|
|
|
|
|
|
|
|
|
|
// 构建上下文
|
2026-01-14 14:33:43 +08:00
|
|
|
const context: Partial<WizardData> & { initial_idea?: string } = {
|
2025-12-28 19:35:23 +08:00
|
|
|
initial_idea: initialIdea,
|
|
|
|
|
title: wizardData.title,
|
|
|
|
|
description: wizardData.description,
|
|
|
|
|
theme: wizardData.theme,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 调用refine接口
|
|
|
|
|
const response = await inspirationApi.refineOptions({
|
|
|
|
|
step,
|
|
|
|
|
context,
|
|
|
|
|
feedback,
|
|
|
|
|
previous_options: targetMessage.options,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.error) {
|
|
|
|
|
message.error(response.error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加新的AI消息
|
|
|
|
|
const aiMessage: Message = {
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: response.prompt || `根据您的反馈,我重新生成了一些${step === 'title' ? '书名' : step === 'description' ? '简介' : step === 'theme' ? '主题' : '类型'}选项:`,
|
|
|
|
|
options: response.options || [],
|
|
|
|
|
isMultiSelect: step === 'genre',
|
|
|
|
|
canRefine: true,
|
|
|
|
|
step: step,
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
|
|
|
|
|
message.success('已根据您的反馈重新生成选项');
|
2026-01-14 14:33:43 +08:00
|
|
|
} catch (error: unknown) {
|
2025-12-28 19:35:23 +08:00
|
|
|
console.error('优化选项失败:', error);
|
2026-01-14 14:33:43 +08:00
|
|
|
const errMsg = error instanceof Error ? error.message : '优化失败,请重试';
|
|
|
|
|
const axiosError = error as { response?: { data?: { detail?: string } } };
|
|
|
|
|
message.error(axiosError.response?.data?.detail || errMsg);
|
2025-12-28 19:35:23 +08:00
|
|
|
} finally {
|
|
|
|
|
setRefining(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
// 步骤顺序
|
2025-11-27 17:29:23 +08:00
|
|
|
const stepOrder: Step[] = ['idea', 'title', 'description', 'theme', 'genre', 'perspective', 'outline_mode', 'confirm'];
|
2025-11-14 17:16:24 +08:00
|
|
|
|
|
|
|
|
const handleSendMessage = async () => {
|
|
|
|
|
if (!inputValue.trim()) {
|
|
|
|
|
message.warning('请输入内容');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userMessage: Message = {
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: inputValue,
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
const userInput = inputValue;
|
|
|
|
|
setInputValue('');
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (currentStep === 'idea') {
|
2025-11-17 16:00:07 +08:00
|
|
|
setInitialIdea(userInput);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
const requestData = {
|
|
|
|
|
step: 'title' as const,
|
2025-11-17 16:00:07 +08:00
|
|
|
context: {
|
|
|
|
|
initial_idea: userInput,
|
|
|
|
|
description: userInput
|
|
|
|
|
}
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
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 || '请选择一个书名,或者输入你自己的:',
|
2025-12-28 19:35:23 +08:00
|
|
|
options: response.options,
|
|
|
|
|
canRefine: true,
|
|
|
|
|
step: 'title'
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('title');
|
|
|
|
|
setLastFailedRequest(null);
|
|
|
|
|
} else {
|
2025-11-14 19:54:04 +08:00
|
|
|
await handleCustomInput(userInput);
|
2025-11-14 17:16:24 +08:00
|
|
|
}
|
2026-01-14 14:33:43 +08:00
|
|
|
} catch (error: unknown) {
|
2025-11-14 17:16:24 +08:00
|
|
|
console.error('发送消息失败:', error);
|
2026-01-14 14:33:43 +08:00
|
|
|
const errMsg = error instanceof Error ? error.message : '生成失败,请重试';
|
|
|
|
|
const axiosError = error as { response?: { data?: { detail?: string } } };
|
|
|
|
|
message.error(axiosError.response?.data?.detail || errMsg);
|
2025-11-14 17:16:24 +08:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSelectOption = async (option: string) => {
|
|
|
|
|
if (option === '重新生成' && lastFailedRequest) {
|
|
|
|
|
await handleRetry();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
if (option === '我自己输入书名' || option === '我自己输入') {
|
|
|
|
|
message.info('请在下方输入框中输入您的内容');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 对于多选类型,不立即禁用选项
|
2025-11-14 17:16:24 +08:00
|
|
|
if (currentStep === 'genre') {
|
|
|
|
|
const newSelected = selectedOptions.includes(option)
|
|
|
|
|
? selectedOptions.filter(o => o !== option)
|
|
|
|
|
: [...selectedOptions, option];
|
|
|
|
|
setSelectedOptions(newSelected);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 立即禁用当前消息的选项(单选场景)
|
|
|
|
|
setMessages(prev => {
|
|
|
|
|
const newMessages = [...prev];
|
|
|
|
|
const lastAiMessageIndex = newMessages.map((m, i) => m.type === 'ai' && m.options ? i : -1).filter(i => i >= 0).pop();
|
|
|
|
|
if (lastAiMessageIndex !== undefined && lastAiMessageIndex >= 0) {
|
|
|
|
|
newMessages[lastAiMessageIndex] = {
|
|
|
|
|
...newMessages[lastAiMessageIndex],
|
|
|
|
|
optionsDisabled: true
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return newMessages;
|
|
|
|
|
});
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
if (currentStep === 'perspective') {
|
|
|
|
|
const userMessage: Message = {
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: option,
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
const updatedData = { ...wizardData, narrative_perspective: option };
|
2025-11-14 17:16:24 +08:00
|
|
|
setWizardData(updatedData);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 询问大纲模式
|
|
|
|
|
const aiMessage: Message = {
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: `很好!现在请选择你想要的大纲模式:
|
|
|
|
|
|
2025-11-29 22:01:02 +08:00
|
|
|
📋 一对一模式:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
|
2025-11-27 17:29:23 +08:00
|
|
|
|
2025-11-29 22:01:02 +08:00
|
|
|
📚 一对多模式:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
|
2025-11-27 17:29:23 +08:00
|
|
|
|
|
|
|
|
请选择:`,
|
|
|
|
|
options: ['📋 一对一模式', '📚 一对多模式']
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('outline_mode');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
if (currentStep === 'outline_mode') {
|
|
|
|
|
const userMessage: Message = {
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: option,
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 将选项转换为实际的模式值
|
|
|
|
|
const modeValue: 'one-to-one' | 'one-to-many' =
|
|
|
|
|
option === '📋 一对一模式' ? 'one-to-one' : 'one-to-many';
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
const updatedData = {
|
|
|
|
|
...wizardData,
|
|
|
|
|
outline_mode: modeValue,
|
|
|
|
|
genre: wizardData.genre || []
|
|
|
|
|
} as WizardData;
|
|
|
|
|
setWizardData(updatedData);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 显示摘要
|
|
|
|
|
const modeText = modeValue === 'one-to-one' ? '一对一模式' : '一对多模式';
|
2025-11-14 17:16:24 +08:00
|
|
|
const summary = `
|
|
|
|
|
太棒了!你的小说设定已完成,请确认:
|
|
|
|
|
|
|
|
|
|
📖 书名:${updatedData.title}
|
|
|
|
|
📝 简介:${updatedData.description}
|
|
|
|
|
🎯 主题:${updatedData.theme}
|
|
|
|
|
🏷️ 类型:${updatedData.genre.join('、')}
|
|
|
|
|
👁️ 视角:${updatedData.narrative_perspective}
|
2025-11-27 17:29:23 +08:00
|
|
|
📋 大纲模式:${modeText}
|
2025-11-14 17:16:24 +08:00
|
|
|
|
|
|
|
|
请选择下一步操作:
|
|
|
|
|
`.trim();
|
|
|
|
|
|
|
|
|
|
const aiMessage: Message = {
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: summary,
|
|
|
|
|
options: ['✅ 确认创建', '🔄 重新开始']
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('confirm');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
if (currentStep === 'confirm') {
|
|
|
|
|
if (option === '✅ 确认创建') {
|
|
|
|
|
const userMessage: Message = {
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: '确认创建',
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
const aiMessage: Message = {
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: '好的!正在为你创建项目,这可能需要几分钟时间...'
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 清除缓存(对话完成,进入生成阶段)
|
|
|
|
|
clearCache();
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
// 开始生成项目
|
2025-11-26 14:56:13 +08:00
|
|
|
const data = wizardData as WizardData;
|
|
|
|
|
const config: GenerationConfig = {
|
|
|
|
|
title: data.title,
|
|
|
|
|
description: data.description,
|
|
|
|
|
theme: data.theme,
|
|
|
|
|
genre: data.genre,
|
|
|
|
|
narrative_perspective: data.narrative_perspective,
|
|
|
|
|
target_words: 100000,
|
|
|
|
|
chapter_count: 3,
|
|
|
|
|
character_count: 5,
|
2025-11-27 17:29:23 +08:00
|
|
|
outline_mode: data.outline_mode,
|
2025-11-26 14:56:13 +08:00
|
|
|
};
|
|
|
|
|
setGenerationConfig(config);
|
|
|
|
|
setCurrentStep('generating');
|
2025-11-14 17:16:24 +08:00
|
|
|
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);
|
2026-01-14 14:33:43 +08:00
|
|
|
} catch (error: unknown) {
|
2025-11-14 17:16:24 +08:00
|
|
|
console.error('选择选项失败:', error);
|
2026-01-14 14:33:43 +08:00
|
|
|
const errMsg = error instanceof Error ? error.message : '生成失败,请重试';
|
|
|
|
|
const axiosError = error as { response?: { data?: { detail?: string } } };
|
|
|
|
|
message.error(axiosError.response?.data?.detail || errMsg);
|
2025-11-14 17:16:24 +08:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCustomInput = async (input: string) => {
|
2025-11-14 19:54:04 +08:00
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const updatedData = { ...wizardData };
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 19:54:04 +08:00
|
|
|
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;
|
2025-12-28 19:35:23 +08:00
|
|
|
setWizardData(updatedData);
|
|
|
|
|
|
|
|
|
|
// 直接进入大纲模式选择
|
|
|
|
|
const aiMessage: Message = {
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: `很好!现在请选择你想要的大纲模式:
|
|
|
|
|
|
|
|
|
|
📋 一对一模式:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
|
|
|
|
|
|
|
|
|
|
📚 一对多模式:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
|
|
|
|
|
|
|
|
|
|
请选择:`,
|
|
|
|
|
options: ['📋 一对一模式', '📚 一对多模式']
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('outline_mode');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
2025-11-27 17:29:23 +08:00
|
|
|
} else if (currentStep === 'outline_mode') {
|
|
|
|
|
// 大纲模式不支持自定义输入
|
|
|
|
|
message.warning('请从选项中选择一个大纲模式');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
2025-11-14 19:54:04 +08:00
|
|
|
}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 19:54:04 +08:00
|
|
|
setWizardData(updatedData);
|
|
|
|
|
await generateNextStep(updatedData);
|
2026-01-14 14:33:43 +08:00
|
|
|
} catch (error: unknown) {
|
2025-11-14 19:54:04 +08:00
|
|
|
console.error('处理自定义输入失败:', error);
|
2026-01-14 14:33:43 +08:00
|
|
|
const errMsg = error instanceof Error ? error.message : '处理失败,请重试';
|
|
|
|
|
const axiosError = error as { response?: { data?: { detail?: string } } };
|
|
|
|
|
message.error(axiosError.response?.data?.detail || errMsg);
|
2025-11-14 19:54:04 +08:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
2025-11-14 17:16:24 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleConfirmGenres = async () => {
|
|
|
|
|
if (selectedOptions.length === 0) {
|
|
|
|
|
message.warning('请至少选择一个类型');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 17:29:23 +08:00
|
|
|
// 禁用类型选择的选项
|
|
|
|
|
setMessages(prev => {
|
|
|
|
|
const newMessages = [...prev];
|
|
|
|
|
const lastAiMessageIndex = newMessages.map((m, i) => m.type === 'ai' && m.options ? i : -1).filter(i => i >= 0).pop();
|
|
|
|
|
if (lastAiMessageIndex !== undefined && lastAiMessageIndex >= 0) {
|
|
|
|
|
newMessages[lastAiMessageIndex] = {
|
|
|
|
|
...newMessages[lastAiMessageIndex],
|
|
|
|
|
optionsDisabled: true
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return newMessages;
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
const userMessage: Message = {
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: selectedOptions.join('、'),
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
|
|
|
|
|
|
|
|
|
const updatedData = { ...wizardData, genre: selectedOptions };
|
|
|
|
|
setWizardData(updatedData);
|
|
|
|
|
setSelectedOptions([]);
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const aiMessage: Message = {
|
|
|
|
|
type: 'ai',
|
2025-11-27 17:29:23 +08:00
|
|
|
content: '很好!接下来,请选择小说的叙事视角:',
|
2025-11-14 17:16:24 +08:00
|
|
|
options: ['第一人称', '第三人称', '全知视角']
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('perspective');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const generateNextStep = async (data: Partial<WizardData>) => {
|
|
|
|
|
const currentIndex = stepOrder.indexOf(currentStep);
|
|
|
|
|
const nextStep = stepOrder[currentIndex + 1];
|
|
|
|
|
|
2025-12-28 19:35:23 +08:00
|
|
|
if (nextStep === 'perspective') {
|
|
|
|
|
// genre 步骤完成后,进入 perspective
|
|
|
|
|
const aiMessage: Message = {
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: '很好!接下来,请选择小说的叙事视角:',
|
|
|
|
|
options: ['第一人称', '第三人称', '全知视角']
|
|
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('perspective');
|
|
|
|
|
} else if (nextStep === 'description') {
|
2025-11-14 17:16:24 +08:00
|
|
|
const requestData = {
|
|
|
|
|
step: 'description' as const,
|
2025-11-17 16:00:07 +08:00
|
|
|
context: {
|
|
|
|
|
initial_idea: initialIdea,
|
|
|
|
|
title: data.title
|
|
|
|
|
}
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
|
|
|
|
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 || '请选择一个简介,或者输入你自己的:',
|
2025-12-28 19:35:23 +08:00
|
|
|
options: response.options,
|
|
|
|
|
canRefine: true,
|
|
|
|
|
step: 'description'
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('description');
|
|
|
|
|
setLastFailedRequest(null);
|
|
|
|
|
|
|
|
|
|
} else if (nextStep === 'theme') {
|
|
|
|
|
const requestData = {
|
|
|
|
|
step: 'theme' as const,
|
2025-11-17 16:00:07 +08:00
|
|
|
context: {
|
|
|
|
|
initial_idea: initialIdea,
|
|
|
|
|
title: data.title,
|
|
|
|
|
description: data.description
|
|
|
|
|
}
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
|
|
|
|
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 || '请选择一个主题,或者输入你自己的:',
|
2025-12-28 19:35:23 +08:00
|
|
|
options: response.options,
|
|
|
|
|
canRefine: true,
|
|
|
|
|
step: 'theme'
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('theme');
|
|
|
|
|
setLastFailedRequest(null);
|
|
|
|
|
|
|
|
|
|
} else if (nextStep === 'genre') {
|
|
|
|
|
const requestData = {
|
|
|
|
|
step: 'genre' as const,
|
|
|
|
|
context: {
|
2025-11-17 16:00:07 +08:00
|
|
|
initial_idea: initialIdea,
|
2025-11-14 17:16:24 +08:00
|
|
|
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,
|
2025-12-28 19:35:23 +08:00
|
|
|
isMultiSelect: true,
|
|
|
|
|
canRefine: true,
|
|
|
|
|
step: 'genre'
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
|
|
|
|
setMessages(prev => [...prev, aiMessage]);
|
|
|
|
|
setCurrentStep('genre');
|
|
|
|
|
setLastFailedRequest(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRestart = () => {
|
2025-11-27 17:29:23 +08:00
|
|
|
// 清除缓存
|
|
|
|
|
clearCache();
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
setCurrentStep('idea');
|
|
|
|
|
setMessages([
|
|
|
|
|
{
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: '好的,让我们重新开始!\n\n请告诉我,你想写一本什么样的小说?',
|
|
|
|
|
}
|
|
|
|
|
]);
|
|
|
|
|
setWizardData({});
|
2025-11-26 14:56:13 +08:00
|
|
|
setInitialIdea('');
|
2025-11-14 17:16:24 +08:00
|
|
|
setSelectedOptions([]);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
navigate('/projects');
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 14:56:13 +08:00
|
|
|
// 生成完成回调
|
|
|
|
|
const handleComplete = (projectId: string) => {
|
|
|
|
|
console.log('灵感模式项目创建完成:', projectId);
|
2025-11-27 17:29:23 +08:00
|
|
|
// 确保清除缓存
|
|
|
|
|
clearCache();
|
2025-11-26 14:56:13 +08:00
|
|
|
setCurrentStep('complete');
|
2025-11-14 17:16:24 +08:00
|
|
|
};
|
|
|
|
|
|
2025-11-26 14:56:13 +08:00
|
|
|
// 返回对话界面
|
|
|
|
|
const handleBackToChat = () => {
|
2025-11-27 17:29:23 +08:00
|
|
|
clearCache();
|
2025-11-26 14:56:13 +08:00
|
|
|
setCurrentStep('idea');
|
|
|
|
|
setGenerationConfig(null);
|
|
|
|
|
handleRestart();
|
|
|
|
|
};
|
2025-11-14 17:16:24 +08:00
|
|
|
|
|
|
|
|
// 渲染对话界面
|
|
|
|
|
const renderChat = () => (
|
|
|
|
|
<>
|
|
|
|
|
<Card
|
|
|
|
|
ref={chatContainerRef}
|
|
|
|
|
style={{
|
2025-12-11 17:01:25 +08:00
|
|
|
height: isMobile ? 'calc(100vh - 280px)' : 600,
|
2025-11-14 17:16:24 +08:00
|
|
|
overflowY: 'auto',
|
|
|
|
|
marginBottom: 16,
|
|
|
|
|
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
|
|
|
|
scrollBehavior: 'smooth'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
|
|
|
|
{messages.map((msg, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: msg.type === 'ai' ? 'flex-start' : 'flex-end',
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
animation: 'fadeInUp 0.5s ease-out',
|
|
|
|
|
animationFillMode: 'both',
|
|
|
|
|
animationDelay: `${index * 0.1}s`
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{
|
|
|
|
|
maxWidth: '80%',
|
|
|
|
|
padding: '12px 16px',
|
|
|
|
|
borderRadius: 12,
|
2025-12-11 17:01:25 +08:00
|
|
|
background: msg.type === 'ai' ? 'var(--color-bg-container)' : 'var(--color-primary)',
|
|
|
|
|
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
|
2025-11-14 17:16:24 +08:00
|
|
|
boxShadow: msg.type === 'ai'
|
2025-12-11 17:01:25 +08:00
|
|
|
? 'var(--shadow-card)'
|
|
|
|
|
: 'var(--shadow-primary)',
|
2025-11-14 17:16:24 +08:00
|
|
|
}}>
|
2025-12-11 17:01:25 +08:00
|
|
|
<Paragraph
|
|
|
|
|
style={{
|
|
|
|
|
margin: 0,
|
|
|
|
|
color: msg.type === 'ai' ? 'var(--color-text-primary)' : '#fff',
|
2025-11-14 17:16:24 +08:00
|
|
|
whiteSpace: 'pre-wrap'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{msg.content}
|
|
|
|
|
</Paragraph>
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
{msg.options && msg.options.length > 0 && (
|
|
|
|
|
<Space
|
|
|
|
|
direction="vertical"
|
|
|
|
|
style={{ width: '100%', marginTop: 12 }}
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
{msg.options.map((option, optIndex) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={optIndex}
|
2025-11-27 17:29:23 +08:00
|
|
|
hoverable={!msg.optionsDisabled}
|
2025-11-14 17:16:24 +08:00
|
|
|
size="small"
|
2025-11-27 17:29:23 +08:00
|
|
|
onClick={() => !msg.optionsDisabled && handleSelectOption(option)}
|
2025-11-14 17:16:24 +08:00
|
|
|
style={{
|
2025-11-27 17:29:23 +08:00
|
|
|
cursor: msg.optionsDisabled ? 'not-allowed' : 'pointer',
|
2025-11-14 17:16:24 +08:00
|
|
|
border: msg.isMultiSelect && selectedOptions.includes(option)
|
2025-12-11 17:01:25 +08:00
|
|
|
? '2px solid var(--color-primary)'
|
|
|
|
|
: '1px solid var(--color-border)',
|
2025-11-27 17:29:23 +08:00
|
|
|
background: msg.optionsDisabled
|
2025-12-11 17:01:25 +08:00
|
|
|
? 'var(--color-bg-layout)'
|
2025-11-27 17:29:23 +08:00
|
|
|
: msg.isMultiSelect && selectedOptions.includes(option)
|
2025-12-28 19:35:23 +08:00
|
|
|
? 'var(--color-bg-spotlight)'
|
2025-12-11 17:01:25 +08:00
|
|
|
: 'var(--color-bg-container)',
|
2025-11-27 17:29:23 +08:00
|
|
|
opacity: msg.optionsDisabled ? 0.6 : 1,
|
2025-11-14 17:16:24 +08:00
|
|
|
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) => {
|
2025-11-27 17:29:23 +08:00
|
|
|
if (!msg.optionsDisabled) {
|
|
|
|
|
e.currentTarget.style.transform = 'translateY(-2px) scale(1.02)';
|
2025-12-11 17:01:25 +08:00
|
|
|
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)';
|
2025-11-27 17:29:23 +08:00
|
|
|
}
|
2025-11-14 17:16:24 +08:00
|
|
|
}}
|
|
|
|
|
onMouseLeave={(e) => {
|
2025-11-27 17:29:23 +08:00
|
|
|
if (!msg.optionsDisabled) {
|
|
|
|
|
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
|
|
|
|
e.currentTarget.style.boxShadow = 'none';
|
|
|
|
|
}
|
2025-11-14 17:16:24 +08:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{option}
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
{msg.isMultiSelect && (
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
block
|
|
|
|
|
onClick={handleConfirmGenres}
|
|
|
|
|
disabled={selectedOptions.length === 0}
|
|
|
|
|
>
|
|
|
|
|
确认选择 ({selectedOptions.length})
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-12-28 19:35:23 +08:00
|
|
|
|
|
|
|
|
{/* 反馈优化区域 - 新增 */}
|
|
|
|
|
{msg.canRefine && !msg.optionsDisabled && !msg.isMultiSelect && (
|
|
|
|
|
<div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px dashed var(--color-border)' }}>
|
|
|
|
|
{showFeedbackInput === index ? (
|
|
|
|
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
|
|
|
|
<TextArea
|
|
|
|
|
value={feedbackValue}
|
|
|
|
|
onChange={(e) => setFeedbackValue(e.target.value)}
|
|
|
|
|
placeholder="例如:我想要更悲剧的主题、能不能更简短一些、偏向古风..."
|
|
|
|
|
autoSize={{ minRows: 2, maxRows: 3 }}
|
|
|
|
|
disabled={refining}
|
|
|
|
|
onPressEnter={(e) => {
|
|
|
|
|
if (!e.shiftKey && feedbackValue.trim()) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleRefineOptions(index, feedbackValue);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowFeedbackInput(null);
|
|
|
|
|
setFeedbackValue('');
|
|
|
|
|
}}
|
|
|
|
|
disabled={refining}
|
|
|
|
|
>
|
|
|
|
|
取消
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => handleRefineOptions(index, feedbackValue)}
|
|
|
|
|
loading={refining}
|
|
|
|
|
disabled={!feedbackValue.trim()}
|
|
|
|
|
>
|
|
|
|
|
重新生成
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</Space>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
type="link"
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => setShowFeedbackInput(index)}
|
|
|
|
|
style={{ padding: 0, height: 'auto' }}
|
|
|
|
|
>
|
|
|
|
|
💡 不太满意?告诉我你的想法
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-14 17:16:24 +08:00
|
|
|
</Space>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-12-28 19:35:23 +08:00
|
|
|
{(loading || refining) && (
|
2025-11-14 17:16:24 +08:00
|
|
|
<div style={{
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
padding: 20,
|
|
|
|
|
animation: 'fadeIn 0.3s ease-in'
|
|
|
|
|
}}>
|
2025-12-28 19:35:23 +08:00
|
|
|
<Spin tip={refining ? "正在根据您的反馈重新生成..." : "AI思考中..."} />
|
2025-11-14 17:16:24 +08:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-11 17:01:25 +08:00
|
|
|
|
2025-11-14 17:16:24 +08:00
|
|
|
<div ref={messagesEndRef} />
|
|
|
|
|
</Space>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2025-11-14 19:54:04 +08:00
|
|
|
<Card
|
|
|
|
|
style={{ boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
|
|
|
|
styles={{ body: { padding: 12 } }}
|
|
|
|
|
>
|
2025-11-14 17:16:24 +08:00
|
|
|
<Space.Compact style={{ width: '100%' }}>
|
|
|
|
|
<TextArea
|
|
|
|
|
value={inputValue}
|
|
|
|
|
onChange={(e) => setInputValue(e.target.value)}
|
|
|
|
|
placeholder={
|
2025-12-11 17:01:25 +08:00
|
|
|
currentStep === 'idea'
|
2025-11-14 17:16:24 +08:00
|
|
|
? '例如:我想写一本关于时间旅行的科幻小说...'
|
|
|
|
|
: '输入自定义内容,或点击上方选项卡片...'
|
|
|
|
|
}
|
|
|
|
|
autoSize={{ minRows: 2, maxRows: 4 }}
|
|
|
|
|
onPressEnter={(e) => {
|
|
|
|
|
if (!e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleSendMessage();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
icon={<SendOutlined />}
|
|
|
|
|
onClick={handleSendMessage}
|
|
|
|
|
loading={loading}
|
|
|
|
|
style={{ height: 'auto' }}
|
|
|
|
|
>
|
|
|
|
|
发送
|
|
|
|
|
</Button>
|
|
|
|
|
</Space.Compact>
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12, marginTop: 8, display: 'block' }}>
|
|
|
|
|
💡 提示:按 Enter 发送,Shift+Enter 换行
|
|
|
|
|
</Text>
|
|
|
|
|
</Card>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-14 19:54:04 +08:00
|
|
|
<div style={{
|
2026-01-14 14:33:43 +08:00
|
|
|
minHeight: '100dvh',
|
2025-12-11 17:01:25 +08:00
|
|
|
background: 'var(--color-bg-base)',
|
2025-11-14 17:16:24 +08:00
|
|
|
}}>
|
2025-12-31 11:57:21 +08:00
|
|
|
{contextHolder}
|
2025-11-14 17:16:24 +08:00
|
|
|
<style>
|
|
|
|
|
{`
|
|
|
|
|
@keyframes fadeInUp {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(20px);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes floatIn {
|
|
|
|
|
0% {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(10px) scale(0.95);
|
|
|
|
|
}
|
|
|
|
|
60% {
|
|
|
|
|
transform: translateY(-5px) scale(1.02);
|
|
|
|
|
}
|
|
|
|
|
100% {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateY(0) scale(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`}
|
|
|
|
|
</style>
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
|
|
|
{/* 顶部标题栏 - 固定不滚动 */}
|
|
|
|
|
<div style={{
|
|
|
|
|
position: 'sticky',
|
|
|
|
|
top: 0,
|
|
|
|
|
zIndex: 100,
|
|
|
|
|
background: 'var(--color-primary)',
|
|
|
|
|
boxShadow: 'var(--shadow-header)',
|
|
|
|
|
}}>
|
2025-11-14 19:54:04 +08:00
|
|
|
<div style={{
|
2025-12-11 17:01:25 +08:00
|
|
|
maxWidth: 1200,
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
padding: isMobile ? '12px 16px' : '16px 24px',
|
2025-11-14 19:54:04 +08:00
|
|
|
}}>
|
|
|
|
|
<Button
|
|
|
|
|
icon={<ArrowLeftOutlined />}
|
2025-11-14 17:16:24 +08:00
|
|
|
onClick={handleBack}
|
2025-12-11 17:01:25 +08:00
|
|
|
size={isMobile ? 'middle' : 'large'}
|
2025-11-14 19:54:04 +08:00
|
|
|
style={{
|
2025-12-11 17:01:25 +08:00
|
|
|
background: 'rgba(255,255,255,0.2)',
|
|
|
|
|
borderColor: 'rgba(255,255,255,0.3)',
|
2025-11-14 19:54:04 +08:00
|
|
|
color: '#fff',
|
|
|
|
|
}}
|
2025-11-14 17:16:24 +08:00
|
|
|
>
|
2026-01-14 14:33:43 +08:00
|
|
|
{isMobile ? '返回' : '返回首页'}
|
2025-11-14 17:16:24 +08:00
|
|
|
</Button>
|
2025-12-11 17:01:25 +08:00
|
|
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
2025-11-14 19:54:04 +08:00
|
|
|
<Title
|
2025-12-11 17:01:25 +08:00
|
|
|
level={isMobile ? 4 : 2}
|
2025-11-14 19:54:04 +08:00
|
|
|
style={{
|
|
|
|
|
margin: 0,
|
2025-12-11 17:01:25 +08:00
|
|
|
color: '#fff',
|
|
|
|
|
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
|
|
|
lineHeight: 1.2
|
2025-11-14 19:54:04 +08:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
✨ 灵感模式
|
|
|
|
|
</Title>
|
|
|
|
|
</div>
|
2025-11-14 17:16:24 +08:00
|
|
|
|
2025-12-31 11:57:21 +08:00
|
|
|
{/* 重新开始按钮 - 只在对话进行中显示 */}
|
|
|
|
|
{currentStep !== 'idea' && currentStep !== 'generating' && currentStep !== 'complete' ? (
|
|
|
|
|
<Button
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
modal.confirm({
|
|
|
|
|
title: '确认重新开始',
|
|
|
|
|
content: '确定要重新开始吗?当前的对话进度将会丢失。',
|
|
|
|
|
okText: '确认',
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
centered: true,
|
|
|
|
|
okButtonProps: { danger: true },
|
|
|
|
|
onOk: () => {
|
|
|
|
|
handleRestart();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
size={isMobile ? 'middle' : 'large'}
|
|
|
|
|
style={{
|
|
|
|
|
background: 'rgba(255,255,255,0.2)',
|
|
|
|
|
borderColor: 'rgba(255,255,255,0.3)',
|
|
|
|
|
color: '#fff',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isMobile ? '重新' : '重新开始'}
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<div style={{ width: isMobile ? 60 : 120 }}></div>
|
|
|
|
|
)}
|
2025-12-11 17:01:25 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
maxWidth: 800,
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
padding: isMobile ? '16px 12px' : '24px 24px',
|
|
|
|
|
}}>
|
2025-11-14 17:16:24 +08:00
|
|
|
{(currentStep === 'idea' || currentStep === 'title' || currentStep === 'description' ||
|
|
|
|
|
currentStep === 'theme' || currentStep === 'genre' || currentStep === 'perspective' ||
|
2025-11-27 17:29:23 +08:00
|
|
|
currentStep === 'outline_mode' || currentStep === 'confirm') && renderChat()}
|
2025-11-26 14:56:13 +08:00
|
|
|
{(currentStep === 'generating' || currentStep === 'complete') && generationConfig && (
|
|
|
|
|
<AIProjectGenerator
|
|
|
|
|
config={generationConfig}
|
|
|
|
|
storagePrefix="inspiration"
|
|
|
|
|
onComplete={handleComplete}
|
|
|
|
|
onBack={handleBackToChat}
|
2025-12-11 17:01:25 +08:00
|
|
|
isMobile={isMobile}
|
2025-11-26 14:56:13 +08:00
|
|
|
/>
|
|
|
|
|
)}
|
2025-11-14 17:16:24 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default Inspiration;
|