update:1.优化 AI 流式生成和进度显示系统 2.新增写作风格系统提示词支持 3.灵感模式功能增强,支持灵感重写 4.设置页面功能扩展,新增Gemini适配器 5.提示词模板系统优化,调整灵感模式提示词
This commit is contained in:
@@ -190,7 +190,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(Math.floor(prog / 3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
@@ -236,7 +237,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(33 + Math.floor(prog / 3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
@@ -273,7 +275,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(66 + Math.floor(prog / 3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: () => {
|
||||
@@ -336,15 +339,13 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
// 世界观生成占0%-20%,职业生成占20%-30%
|
||||
const baseProgress = Math.floor(prog / 5);
|
||||
setProgress(baseProgress);
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
|
||||
// 检测职业体系生成阶段 - 必须包含"职业体系"才算职业阶段
|
||||
// 检测职业体系生成阶段
|
||||
if (msg.includes('职业体系')) {
|
||||
if (msg.includes('开始') || msg.includes('生成')) {
|
||||
// 职业开始时,世界观应该已完成
|
||||
setGenerationSteps(prev => ({
|
||||
...prev,
|
||||
worldBuilding: 'completed',
|
||||
@@ -403,8 +404,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
// 角色生成占40%-70%
|
||||
setProgress(40 + Math.floor(prog * 0.3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
@@ -437,8 +438,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
// 大纲生成占70%-100%
|
||||
setProgress(70 + Math.floor(prog * 0.3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: () => {
|
||||
@@ -533,8 +534,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
const baseProgress = Math.floor(prog / 5);
|
||||
setProgress(baseProgress);
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
|
||||
// 检测职业体系生成阶段
|
||||
@@ -604,7 +605,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(33 + Math.floor(prog / 3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
@@ -647,7 +649,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(66 + Math.floor(prog / 3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: () => {
|
||||
@@ -707,7 +710,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(33 + Math.floor(prog / 3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: (result) => {
|
||||
@@ -746,7 +750,8 @@ export const AIProjectGenerator: React.FC<AIProjectGeneratorProps> = ({
|
||||
},
|
||||
{
|
||||
onProgress: (msg, prog) => {
|
||||
setProgress(66 + Math.floor(prog / 3));
|
||||
// 直接使用后端返回的进度值
|
||||
setProgress(prog);
|
||||
setProgressMessage(msg);
|
||||
},
|
||||
onResult: () => {
|
||||
|
||||
@@ -16,6 +16,8 @@ interface Message {
|
||||
options?: string[];
|
||||
isMultiSelect?: boolean;
|
||||
optionsDisabled?: boolean; // 标记选项是否已禁用
|
||||
canRefine?: boolean; // 是否可以优化(用于支持多轮对话)
|
||||
step?: Step; // 当前步骤(用于反馈)
|
||||
}
|
||||
|
||||
interface WizardData {
|
||||
@@ -69,6 +71,11 @@ const Inspiration: React.FC = () => {
|
||||
const [wizardData, setWizardData] = useState<Partial<WizardData>>({});
|
||||
// 保存用户的原始想法,用于保持上下文一致性
|
||||
const [initialIdea, setInitialIdea] = useState<string>('');
|
||||
|
||||
// 反馈相关状态
|
||||
const [feedbackValue, setFeedbackValue] = useState('');
|
||||
const [showFeedbackInput, setShowFeedbackInput] = useState<number | null>(null); // 当前显示反馈输入的消息索引
|
||||
const [refining, setRefining] = useState(false); // 正在优化选项
|
||||
|
||||
// 生成配置
|
||||
const [generationConfig, setGenerationConfig] = useState<GenerationConfig | null>(null);
|
||||
@@ -248,6 +255,86 @@ const Inspiration: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理用户反馈,重新生成选项
|
||||
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';
|
||||
|
||||
// 构建上下文
|
||||
const context: any = {
|
||||
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('已根据您的反馈重新生成选项');
|
||||
} catch (error: any) {
|
||||
console.error('优化选项失败:', error);
|
||||
message.error(error.response?.data?.detail || '优化失败,请重试');
|
||||
} finally {
|
||||
setRefining(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 步骤顺序
|
||||
const stepOrder: Step[] = ['idea', 'title', 'description', 'theme', 'genre', 'perspective', 'outline_mode', 'confirm'];
|
||||
|
||||
@@ -297,7 +384,9 @@ const Inspiration: React.FC = () => {
|
||||
const aiMessage: Message = {
|
||||
type: 'ai',
|
||||
content: response.prompt || '请选择一个书名,或者输入你自己的:',
|
||||
options: response.options
|
||||
options: response.options,
|
||||
canRefine: true,
|
||||
step: 'title'
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
setCurrentStep('title');
|
||||
@@ -497,6 +586,24 @@ const Inspiration: React.FC = () => {
|
||||
updatedData.genre = [input];
|
||||
} else if (currentStep === 'perspective') {
|
||||
updatedData.narrative_perspective = input;
|
||||
setWizardData(updatedData);
|
||||
|
||||
// 直接进入大纲模式选择
|
||||
const aiMessage: Message = {
|
||||
type: 'ai',
|
||||
content: `很好!现在请选择你想要的大纲模式:
|
||||
|
||||
📋 一对一模式:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
|
||||
|
||||
📚 一对多模式:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
|
||||
|
||||
请选择:`,
|
||||
options: ['📋 一对一模式', '📚 一对多模式']
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
setCurrentStep('outline_mode');
|
||||
setLoading(false);
|
||||
return;
|
||||
} else if (currentStep === 'outline_mode') {
|
||||
// 大纲模式不支持自定义输入
|
||||
message.warning('请从选项中选择一个大纲模式');
|
||||
@@ -561,7 +668,16 @@ const Inspiration: React.FC = () => {
|
||||
const currentIndex = stepOrder.indexOf(currentStep);
|
||||
const nextStep = stepOrder[currentIndex + 1];
|
||||
|
||||
if (nextStep === 'description') {
|
||||
if (nextStep === 'perspective') {
|
||||
// genre 步骤完成后,进入 perspective
|
||||
const aiMessage: Message = {
|
||||
type: 'ai',
|
||||
content: '很好!接下来,请选择小说的叙事视角:',
|
||||
options: ['第一人称', '第三人称', '全知视角']
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
setCurrentStep('perspective');
|
||||
} else if (nextStep === 'description') {
|
||||
const requestData = {
|
||||
step: 'description' as const,
|
||||
context: {
|
||||
@@ -587,7 +703,9 @@ const Inspiration: React.FC = () => {
|
||||
const aiMessage: Message = {
|
||||
type: 'ai',
|
||||
content: response.prompt || '请选择一个简介,或者输入你自己的:',
|
||||
options: response.options
|
||||
options: response.options,
|
||||
canRefine: true,
|
||||
step: 'description'
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
setCurrentStep('description');
|
||||
@@ -620,7 +738,9 @@ const Inspiration: React.FC = () => {
|
||||
const aiMessage: Message = {
|
||||
type: 'ai',
|
||||
content: response.prompt || '请选择一个主题,或者输入你自己的:',
|
||||
options: response.options
|
||||
options: response.options,
|
||||
canRefine: true,
|
||||
step: 'theme'
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
setCurrentStep('theme');
|
||||
@@ -656,7 +776,9 @@ const Inspiration: React.FC = () => {
|
||||
type: 'ai',
|
||||
content: response.prompt || '请选择类型标签(可多选):',
|
||||
options: response.options,
|
||||
isMultiSelect: true
|
||||
isMultiSelect: true,
|
||||
canRefine: true,
|
||||
step: 'genre'
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
setCurrentStep('genre');
|
||||
@@ -767,7 +889,7 @@ const Inspiration: React.FC = () => {
|
||||
background: msg.optionsDisabled
|
||||
? 'var(--color-bg-layout)'
|
||||
: msg.isMultiSelect && selectedOptions.includes(option)
|
||||
? 'var(--color-bg-spotlight)' // Need to ensure this exists or use safe fallback
|
||||
? 'var(--color-bg-spotlight)'
|
||||
: 'var(--color-bg-container)',
|
||||
opacity: msg.optionsDisabled ? 0.6 : 1,
|
||||
animation: 'floatIn 0.6s ease-out',
|
||||
@@ -802,19 +924,72 @@ const Inspiration: React.FC = () => {
|
||||
确认选择 ({selectedOptions.length})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 反馈优化区域 - 新增 */}
|
||||
{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>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{loading && (
|
||||
{(loading || refining) && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: 20,
|
||||
animation: 'fadeIn 0.3s ease-in'
|
||||
}}>
|
||||
<Spin tip="AI思考中..." />
|
||||
<Spin tip={refining ? "正在根据您的反馈重新生成..." : "AI思考中..."} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -150,10 +150,9 @@ export default function SettingsPage() {
|
||||
};
|
||||
|
||||
const apiProviders = [
|
||||
{ value: 'openai', label: 'OpenAl Compatible', defaultUrl: 'https://api.openai.com/v1' },
|
||||
// { value: 'azure', label: 'Azure OpenAI', defaultUrl: 'https://YOUR-RESOURCE.openai.azure.com' },
|
||||
// { value: 'anthropic', label: 'Anthropic', defaultUrl: 'https://api.anthropic.com' },
|
||||
// { value: 'custom', label: '自定义', defaultUrl: '' },
|
||||
{ value: 'openai', label: 'OpenAI Compatible', defaultUrl: 'https://api.openai.com/v1' },
|
||||
// { value: 'anthropic', label: 'Anthropic (Claude)', defaultUrl: 'https://api.anthropic.com' },
|
||||
{ value: 'gemini', label: 'Google Gemini', defaultUrl: 'https://generativelanguage.googleapis.com/v1beta' },
|
||||
];
|
||||
|
||||
const handleProviderChange = (value: string) => {
|
||||
@@ -483,8 +482,8 @@ export default function SettingsPage() {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return 'blue';
|
||||
case 'anthropic':
|
||||
return 'purple';
|
||||
// case 'anthropic':
|
||||
// return 'purple';
|
||||
case 'gemini':
|
||||
return 'green';
|
||||
default:
|
||||
@@ -973,6 +972,26 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space size={4}>
|
||||
<span>系统提示词</span>
|
||||
<Tooltip title="设置全局系统提示词,每次AI调用时都会自动使用。可用于设定AI的角色、语言风格等">
|
||||
<InfoCircleOutlined style={{ color: 'var(--color-text-secondary)', fontSize: isMobile ? '12px' : '14px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
name="system_prompt"
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="例如:你是一个专业的小说创作助手,请用生动、细腻的文字进行创作..."
|
||||
maxLength={10000}
|
||||
showCount
|
||||
style={{ fontSize: isMobile ? '13px' : '14px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 测试结果展示 */}
|
||||
{showTestResult && testResult && (
|
||||
<Alert
|
||||
@@ -1247,7 +1266,7 @@ export default function SettingsPage() {
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="openai">OpenAI</Select.Option>
|
||||
<Select.Option value="anthropic">Anthropic (Claude)</Select.Option>
|
||||
{/* <Select.Option value="anthropic">Anthropic (Claude)</Select.Option> */}
|
||||
<Select.Option value="gemini">Google Gemini</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@@ -1298,6 +1317,18 @@ export default function SettingsPage() {
|
||||
placeholder="2000"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="system_prompt"
|
||||
label="系统提示词"
|
||||
>
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder="例如:你是一个专业的小说创作助手...(可选)"
|
||||
maxLength={10000}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -557,6 +557,24 @@ export const inspirationApi = {
|
||||
error?: string;
|
||||
}>('/inspiration/generate-options', data),
|
||||
|
||||
// 基于用户反馈重新生成选项(新增)
|
||||
refineOptions: (data: {
|
||||
step: 'title' | 'description' | 'theme' | 'genre';
|
||||
context: {
|
||||
initial_idea?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
theme?: string;
|
||||
};
|
||||
feedback: string;
|
||||
previous_options?: string[];
|
||||
}) =>
|
||||
api.post<unknown, {
|
||||
prompt?: string;
|
||||
options: string[];
|
||||
error?: string;
|
||||
}>('/inspiration/refine-options', data),
|
||||
|
||||
// 智能补全缺失信息
|
||||
quickGenerate: (data: {
|
||||
title?: string;
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Settings {
|
||||
llm_model: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
system_prompt?: string;
|
||||
preferences?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -33,6 +34,7 @@ export interface SettingsUpdate {
|
||||
llm_model?: string;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
system_prompt?: string;
|
||||
preferences?: string;
|
||||
}
|
||||
|
||||
@@ -44,6 +46,7 @@ export interface APIKeyPresetConfig {
|
||||
llm_model: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
system_prompt?: string;
|
||||
}
|
||||
|
||||
export interface APIKeyPreset {
|
||||
|
||||
Reference in New Issue
Block a user