fix:初始化
This commit is contained in:
845
src/pages/ConversationsPage.tsx
Normal file
845
src/pages/ConversationsPage.tsx
Normal file
@@ -0,0 +1,845 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Layout,
|
||||
List,
|
||||
Input,
|
||||
Button,
|
||||
Avatar,
|
||||
Typography,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Select,
|
||||
Tag,
|
||||
message,
|
||||
Dropdown,
|
||||
Popconfirm,
|
||||
Alert
|
||||
} from 'antd';
|
||||
import {
|
||||
SendOutlined,
|
||||
PlusOutlined,
|
||||
RobotOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
MessageOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusSquareOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
content: string;
|
||||
role: 'user' | 'assistant';
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
botName: string;
|
||||
botType: 'feishu' | 'other';
|
||||
lastMessage: string;
|
||||
timestamp: string;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
interface BotConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'feishu' | 'other';
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ConversationsPage: React.FC = () => {
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation | null>(null);
|
||||
const [messageInput, setMessageInput] = useState('');
|
||||
const [botConfigs, setBotConfigs] = useState<BotConfig[]>([]);
|
||||
const [showBotModal, setShowBotModal] = useState(false);
|
||||
const [showConversationModal, setShowConversationModal] = useState(false);
|
||||
const [editingBot, setEditingBot] = useState<BotConfig | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [conversationForm] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = () => {
|
||||
// 加载飞书机器人配置
|
||||
const savedBots = localStorage.getItem('botConfigs');
|
||||
if (savedBots) {
|
||||
setBotConfigs(JSON.parse(savedBots));
|
||||
} else {
|
||||
// 默认配置示例
|
||||
const defaultBots: BotConfig[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '助手机器人',
|
||||
type: 'feishu',
|
||||
appId: 'cli_xxxxxxxxxxxxx',
|
||||
appSecret: 'xxxxxxxxxxxxx',
|
||||
description: '用于日常事务处理和提醒'
|
||||
}
|
||||
];
|
||||
setBotConfigs(defaultBots);
|
||||
localStorage.setItem('botConfigs', JSON.stringify(defaultBots));
|
||||
}
|
||||
|
||||
// 加载会话列表
|
||||
const savedConversations = localStorage.getItem('conversations');
|
||||
if (savedConversations) {
|
||||
setConversations(JSON.parse(savedConversations));
|
||||
} else {
|
||||
// 默认会话示例
|
||||
const defaultConversations: Conversation[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: '欢迎使用飞书机器人',
|
||||
botName: '助手机器人',
|
||||
botType: 'feishu',
|
||||
lastMessage: '你好!我是飞书助手机器人,有什么可以帮助您的吗?',
|
||||
timestamp: new Date().toISOString(),
|
||||
messages: [
|
||||
{
|
||||
id: '1',
|
||||
content: '你好!我是飞书助手机器人,有什么可以帮助您的吗?',
|
||||
role: 'assistant',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
setConversations(defaultConversations);
|
||||
localStorage.setItem('conversations', JSON.stringify(defaultConversations));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!messageInput.trim() || !selectedConversation) return;
|
||||
|
||||
const newMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
content: messageInput,
|
||||
role: 'user',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedConversation = {
|
||||
...selectedConversation,
|
||||
messages: [...selectedConversation.messages, newMessage],
|
||||
lastMessage: messageInput,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
setSelectedConversation(updatedConversation);
|
||||
setConversations(conversations.map(conv =>
|
||||
conv.id === selectedConversation.id ? updatedConversation : conv
|
||||
));
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('conversations', JSON.stringify(
|
||||
conversations.map(conv => conv.id === selectedConversation.id ? updatedConversation : conv)
|
||||
));
|
||||
|
||||
setMessageInput('');
|
||||
|
||||
// 模拟飞书应用回复
|
||||
setTimeout(() => {
|
||||
const botReply: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
content: `收到您的消息:"${messageInput}"。这是一个模拟的飞书应用回复。在实际使用中,这里会调用飞书自建应用API来发送消息。`,
|
||||
role: 'assistant',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedWithReply = {
|
||||
...updatedConversation,
|
||||
messages: [...updatedConversation.messages, botReply],
|
||||
lastMessage: botReply.content,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
setSelectedConversation(updatedWithReply);
|
||||
setConversations(conversations.map(conv =>
|
||||
conv.id === selectedConversation.id ? updatedWithReply : conv
|
||||
));
|
||||
|
||||
localStorage.setItem('conversations', JSON.stringify(
|
||||
conversations.map(conv => conv.id === selectedConversation.id ? updatedWithReply : conv)
|
||||
));
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
const handleSubmitConversation = () => {
|
||||
conversationForm.validateFields().then(values => {
|
||||
const selectedBot = botConfigs.find(bot => bot.id === values.botId);
|
||||
if (!selectedBot) return;
|
||||
|
||||
const newConversation: Conversation = {
|
||||
id: Date.now().toString(),
|
||||
title: values.title,
|
||||
botName: selectedBot.name,
|
||||
botType: selectedBot.type,
|
||||
lastMessage: '新会话已创建',
|
||||
timestamp: new Date().toISOString(),
|
||||
messages: []
|
||||
};
|
||||
|
||||
setConversations([newConversation, ...conversations]);
|
||||
localStorage.setItem('conversations', JSON.stringify([newConversation, ...conversations]));
|
||||
setShowConversationModal(false);
|
||||
conversationForm.resetFields();
|
||||
setSelectedConversation(newConversation);
|
||||
message.success('会话创建成功');
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddBot = () => {
|
||||
setEditingBot(null);
|
||||
form.resetFields();
|
||||
setShowBotModal(true);
|
||||
};
|
||||
|
||||
const handleEditBot = (bot: BotConfig) => {
|
||||
setEditingBot(bot);
|
||||
form.setFieldsValue(bot);
|
||||
setShowBotModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteBot = (botId: string) => {
|
||||
const updatedBots = botConfigs.filter(bot => bot.id !== botId);
|
||||
setBotConfigs(updatedBots);
|
||||
localStorage.setItem('botConfigs', JSON.stringify(updatedBots));
|
||||
message.success('机器人配置已删除');
|
||||
};
|
||||
|
||||
const handleSubmitBot = () => {
|
||||
form.validateFields().then(values => {
|
||||
if (editingBot) {
|
||||
// 编辑模式
|
||||
const updatedBot: BotConfig = {
|
||||
...editingBot,
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
appId: values.appId,
|
||||
appSecret: values.appSecret,
|
||||
description: values.description
|
||||
};
|
||||
const updatedBots = botConfigs.map(bot =>
|
||||
bot.id === editingBot.id ? updatedBot : bot
|
||||
);
|
||||
setBotConfigs(updatedBots);
|
||||
localStorage.setItem('botConfigs', JSON.stringify(updatedBots));
|
||||
message.success('飞书应用配置已更新');
|
||||
} else {
|
||||
// 新增模式
|
||||
const newBot: BotConfig = {
|
||||
id: Date.now().toString(),
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
appId: values.appId,
|
||||
appSecret: values.appSecret,
|
||||
description: values.description
|
||||
};
|
||||
setBotConfigs([...botConfigs, newBot]);
|
||||
localStorage.setItem('botConfigs', JSON.stringify([...botConfigs, newBot]));
|
||||
message.success('飞书应用配置已添加');
|
||||
}
|
||||
setShowBotModal(false);
|
||||
form.resetFields();
|
||||
setEditingBot(null);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteConversation = (conversationId: string) => {
|
||||
const updatedConversations = conversations.filter(conv => conv.id !== conversationId);
|
||||
setConversations(updatedConversations);
|
||||
localStorage.setItem('conversations', JSON.stringify(updatedConversations));
|
||||
if (selectedConversation?.id === conversationId) {
|
||||
setSelectedConversation(null);
|
||||
}
|
||||
message.success('会话已删除');
|
||||
};
|
||||
|
||||
const getBotItems = () => {
|
||||
return [
|
||||
{
|
||||
key: 'add',
|
||||
label: (
|
||||
<div onClick={() => handleAddBot()}>
|
||||
<PlusSquareOutlined style={{ marginRight: '8px' }} />
|
||||
添加飞书应用
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
...botConfigs.map(bot => ({
|
||||
key: bot.id,
|
||||
label: (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span>
|
||||
<RobotOutlined style={{ marginRight: '8px', color: '#1890ff' }} />
|
||||
{bot.name}
|
||||
<Tag color={bot.type === 'feishu' ? 'blue' : 'green'} style={{ marginLeft: '8px' }}>
|
||||
{bot.type === 'feishu' ? '飞书' : '其他'}
|
||||
</Tag>
|
||||
</span>
|
||||
<Space size={4}>
|
||||
<EditOutlined
|
||||
style={{ color: '#52c41a', cursor: 'pointer' }}
|
||||
onClick={(e) => { e.stopPropagation(); handleEditBot(bot); }}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确认删除"
|
||||
description="确定要删除这个飞书应用配置吗?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDeleteBot(bot.id);
|
||||
}}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<DeleteOutlined
|
||||
style={{ color: '#ff4d4f', cursor: 'pointer' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: 'calc(100vh - 184px)' }}>
|
||||
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Title level={2} style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<MessageOutlined style={{ color: '#1890ff' }} />
|
||||
飞书机器人会话
|
||||
</Title>
|
||||
<Text type="secondary" style={{ marginTop: '8px', display: 'block' }}>
|
||||
与飞书自建应用进行对话,指挥AI助手完成各种任务
|
||||
</Text>
|
||||
</div>
|
||||
<Dropdown
|
||||
menu={{ items: getBotItems() }}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<SettingOutlined />}>
|
||||
飞书应用配置 ({botConfigs.length})
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<Layout style={{
|
||||
background: 'white',
|
||||
borderRadius: '12px',
|
||||
height: 'calc(100% - 80px)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 左侧会话列表 */}
|
||||
<Sider
|
||||
width={320}
|
||||
style={{
|
||||
background: '#fafafa',
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Input.Search
|
||||
placeholder="搜索会话..."
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 飞书应用列表 */}
|
||||
<div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
飞书应用
|
||||
</div>
|
||||
{botConfigs.length > 0 ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
||||
{botConfigs.map(bot => {
|
||||
const hasConversation = conversations.some(conv => conv.botName === bot.name);
|
||||
return (
|
||||
<div
|
||||
key={bot.id}
|
||||
onClick={() => {
|
||||
if (hasConversation) {
|
||||
const existingConv = conversations.find(conv => conv.botName === bot.name);
|
||||
if (existingConv) {
|
||||
setSelectedConversation(existingConv);
|
||||
}
|
||||
} else {
|
||||
// 创建新会话
|
||||
const newConversation: Conversation = {
|
||||
id: Date.now().toString(),
|
||||
title: bot.name,
|
||||
botName: bot.name,
|
||||
botType: bot.type,
|
||||
lastMessage: '新会话已创建',
|
||||
timestamp: new Date().toISOString(),
|
||||
messages: []
|
||||
};
|
||||
setConversations([newConversation, ...conversations]);
|
||||
localStorage.setItem('conversations', JSON.stringify([newConversation, ...conversations]));
|
||||
setSelectedConversation(newConversation);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '10px 12px',
|
||||
background: selectedConversation?.botName === bot.name ? '#e6f7ff' : 'white',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #f0f0f0',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedConversation?.botName !== bot.name) {
|
||||
e.currentTarget.style.background = '#f5f5f5';
|
||||
e.currentTarget.style.borderColor = '#d9d9d9';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedConversation?.botName !== bot.name) {
|
||||
e.currentTarget.style.background = 'white';
|
||||
e.currentTarget.style.borderColor = '#f0f0f0';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
icon={<RobotOutlined />}
|
||||
style={{ backgroundColor: '#1890ff' }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
{bot.name}
|
||||
</Text>
|
||||
<Tag color={bot.type === 'feishu' ? 'blue' : 'green'}>
|
||||
{bot.type === 'feishu' ? '飞书' : '其他'}
|
||||
</Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
{hasConversation ? '点击继续对话' : '点击开始对话'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px 0', color: '#999' }}>
|
||||
<Text type="secondary">暂无飞书应用</Text>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Button type="link" size="small" onClick={handleAddBot}>
|
||||
添加飞书应用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 会话列表 */}
|
||||
<div style={{ padding: '12px 16px 0', flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
会话列表
|
||||
</div>
|
||||
<List
|
||||
dataSource={conversations}
|
||||
renderItem={(conversation) => (
|
||||
<List.Item
|
||||
key={conversation.id}
|
||||
onClick={() => setSelectedConversation(conversation)}
|
||||
style={{
|
||||
padding: '12px',
|
||||
cursor: 'pointer',
|
||||
background: selectedConversation?.id === conversation.id ? '#e6f7ff' : 'transparent',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedConversation?.id !== conversation.id) {
|
||||
e.currentTarget.style.background = '#f5f5f5';
|
||||
e.currentTarget.style.borderColor = '#d9d9d9';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedConversation?.id !== conversation.id) {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#f0f0f0';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Avatar
|
||||
icon={<RobotOutlined />}
|
||||
style={{ backgroundColor: '#1890ff' }}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text strong ellipsis style={{ maxWidth: '140px' }}>
|
||||
{conversation.title}
|
||||
</Text>
|
||||
<Tag color={conversation.botType === 'feishu' ? 'blue' : 'green'}>
|
||||
{conversation.botType === 'feishu' ? '飞书' : '其他'}
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<Text ellipsis style={{ fontSize: '11px', color: '#666' }}>
|
||||
{conversation.lastMessage}
|
||||
</Text>
|
||||
<Text style={{ fontSize: '10px', color: '#999', display: 'block', marginTop: '2px' }}>
|
||||
{new Date(conversation.timestamp).toLocaleString()}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Sider>
|
||||
|
||||
{/* 右侧会话内容 */}
|
||||
<Content style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
{selectedConversation ? (
|
||||
<>
|
||||
{/* 会话头部 */}
|
||||
<div style={{
|
||||
padding: '16px 24px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
background: '#fafafa'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space>
|
||||
<Avatar icon={<RobotOutlined />} style={{ backgroundColor: '#1890ff' }} />
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
{selectedConversation.title}
|
||||
</Title>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
飞书机器人:{selectedConversation.botName}
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Popconfirm
|
||||
title="确认删除"
|
||||
description="确定要删除这个会话吗?"
|
||||
onConfirm={() => handleDeleteConversation(selectedConversation.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />}>
|
||||
删除会话
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
padding: '24px',
|
||||
overflowY: 'auto',
|
||||
background: 'white'
|
||||
}}>
|
||||
{selectedConversation.messages.length === 0 ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '100px 0',
|
||||
color: '#999'
|
||||
}}>
|
||||
<MessageOutlined style={{ fontSize: '64px', color: '#d9d9d9', marginBottom: '16px' }} />
|
||||
<div>暂无消息,开始与机器人对话吧!</div>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
{selectedConversation.messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start',
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
maxWidth: '70%',
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
alignItems: 'flex-start'
|
||||
}}>
|
||||
{message.role === 'assistant' && (
|
||||
<Avatar icon={<RobotOutlined />} style={{ backgroundColor: '#1890ff' }} />
|
||||
)}
|
||||
<div>
|
||||
{message.role === 'assistant' && (
|
||||
<Text type="secondary" style={{ fontSize: '12px', marginLeft: '8px' }}>
|
||||
{selectedConversation.botName}
|
||||
</Text>
|
||||
)}
|
||||
<div style={{
|
||||
background: message.role === 'user' ? '#1890ff' : '#f5f5f5',
|
||||
color: message.role === 'user' ? 'white' : 'black',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
marginTop: '4px',
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
{message.content}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '11px', marginTop: '4px', display: 'block' }}>
|
||||
{new Date(message.timestamp).toLocaleString()}
|
||||
</Text>
|
||||
</div>
|
||||
{message.role === 'user' && (
|
||||
<Avatar icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 消息输入区 */}
|
||||
<div style={{
|
||||
padding: '16px 24px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
background: '#fafafa'
|
||||
}}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<TextArea
|
||||
value={messageInput}
|
||||
onChange={(e) => setMessageInput(e.target.value)}
|
||||
placeholder="输入消息,按Enter发送,Shift+Enter换行"
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSendMessage}
|
||||
style={{ height: 'auto' }}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'white'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center', color: '#999' }}>
|
||||
<MessageOutlined style={{ fontSize: '64px', color: '#d9d9d9', marginBottom: '16px' }} />
|
||||
<div style={{ fontSize: '16px', marginBottom: '16px' }}>
|
||||
选择左侧的飞书应用开始对话
|
||||
</div>
|
||||
{botConfigs.length === 0 && (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>或先添加飞书应用配置</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddBot}
|
||||
>
|
||||
添加飞书应用
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
{/* 飞书应用配置弹窗 */}
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<RobotOutlined style={{ color: '#1890ff' }} />
|
||||
{editingBot ? '编辑飞书应用配置' : '添加飞书自建应用'}
|
||||
</Space>
|
||||
}
|
||||
open={showBotModal}
|
||||
onOk={handleSubmitBot}
|
||||
onCancel={() => {
|
||||
setShowBotModal(false);
|
||||
form.resetFields();
|
||||
setEditingBot(null);
|
||||
}}
|
||||
width={700}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Alert
|
||||
message="飞书自建应用配置说明"
|
||||
description={
|
||||
<div>
|
||||
<p>1. 在飞书开放平台创建自建应用</p>
|
||||
<p>2. 在「凭证与基础信息」页面复制 App ID 和 App Secret</p>
|
||||
<p>3. 在「权限管理」配置必要权限(如 im:message、im:resource 等)</p>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ marginTop: '24px' }}
|
||||
initialValues={{
|
||||
type: 'feishu'
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="应用名称"
|
||||
rules={[{ required: true, message: '请输入应用名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:AI助手" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="应用类型"
|
||||
rules={[{ required: true, message: '请选择应用类型' }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="feishu">飞书自建应用</Select.Option>
|
||||
<Select.Option value="other">其他应用</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="appId"
|
||||
label="App ID"
|
||||
rules={[{ required: true, message: '请输入 App ID' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="cli_xxxxxxxxxxxxx"
|
||||
prefix={<span style={{ color: '#999' }}>App ID:</span>}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="appSecret"
|
||||
label="App Secret"
|
||||
rules={[{ required: true, message: '请输入 App Secret' }]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="xxxxxxxxxxxxx"
|
||||
prefix={<span style={{ color: '#999' }}>Secret:</span>}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="应用描述"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="描述应用的用途和功能"
|
||||
rows={3}
|
||||
showCount
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 新建会话弹窗 */}
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<MessageOutlined style={{ color: '#1890ff' }} />
|
||||
新建会话
|
||||
</Space>
|
||||
}
|
||||
open={showConversationModal}
|
||||
onOk={handleSubmitConversation}
|
||||
onCancel={() => {
|
||||
setShowConversationModal(false);
|
||||
conversationForm.resetFields();
|
||||
}}
|
||||
okText="创建"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={conversationForm}
|
||||
layout="vertical"
|
||||
style={{ marginTop: '24px' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="会话标题"
|
||||
rules={[{ required: true, message: '请输入会话标题' }]}
|
||||
>
|
||||
<Input placeholder="例如:工作安排助手" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="botId"
|
||||
label="选择飞书应用"
|
||||
rules={[{ required: true, message: '请选择飞书应用' }]}
|
||||
>
|
||||
<Select placeholder="选择要使用的飞书应用">
|
||||
{botConfigs.map(bot => (
|
||||
<Select.Option key={bot.id} value={bot.id}>
|
||||
<Space>
|
||||
<RobotOutlined />
|
||||
{bot.name}
|
||||
<Tag color={bot.type === 'feishu' ? 'blue' : 'green'}>
|
||||
{bot.type === 'feishu' ? '飞书' : '其他'}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationsPage;
|
||||
816
src/pages/InitPage.tsx
Normal file
816
src/pages/InitPage.tsx
Normal file
@@ -0,0 +1,816 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Typography,
|
||||
Alert,
|
||||
Space,
|
||||
Modal,
|
||||
message,
|
||||
Divider,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Progress,
|
||||
Steps,
|
||||
Descriptions,
|
||||
Input
|
||||
} from 'antd';
|
||||
import {
|
||||
ClearOutlined,
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
DatabaseOutlined,
|
||||
SettingOutlined,
|
||||
DeleteOutlined,
|
||||
SafetyOutlined,
|
||||
RocketOutlined,
|
||||
CloudUploadOutlined,
|
||||
FundOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const InitPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [initStatus, setInitStatus] = useState<{
|
||||
step: number;
|
||||
message: string;
|
||||
status: 'process' | 'finish' | 'error' | 'wait';
|
||||
}>({ step: 0, message: '等待初始化...', status: 'wait' });
|
||||
const [dbStats, setDbStats] = useState({
|
||||
novels: 0,
|
||||
chapters: 0,
|
||||
settings: 0
|
||||
});
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [isFirstRun, setIsFirstRun] = useState(false);
|
||||
const [initMode, setInitMode] = useState<'reset' | 'setup'>('reset');
|
||||
|
||||
const getDbStats = async () => {
|
||||
try {
|
||||
const { storage } = await import('../utils/indexedDB');
|
||||
const novels = await storage.getNovels();
|
||||
const settings = localStorage.getItem('ollamaSettings');
|
||||
|
||||
let totalChapters = 0;
|
||||
for (const novel of novels) {
|
||||
const chapters = await storage.getChapters(novel.id);
|
||||
totalChapters += chapters.length;
|
||||
}
|
||||
|
||||
setDbStats({
|
||||
novels: novels.length,
|
||||
chapters: totalChapters,
|
||||
settings: settings ? 1 : 0
|
||||
});
|
||||
|
||||
// 检查是否是首次运行
|
||||
const hasRunBefore = localStorage.getItem('aiNovelInitialized');
|
||||
const isFirstRunDetected = !hasRunBefore && novels.length === 0 && !settings;
|
||||
setIsFirstRun(isFirstRunDetected);
|
||||
|
||||
if (isFirstRunDetected) {
|
||||
setInitMode('setup');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据库统计失败:', error);
|
||||
// 如果获取失败,可能是首次运行
|
||||
const hasRunBefore = localStorage.getItem('aiNovelInitialized');
|
||||
if (!hasRunBefore) {
|
||||
setIsFirstRun(true);
|
||||
setInitMode('setup');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDbStats();
|
||||
}, []);
|
||||
|
||||
const handleInit = async () => {
|
||||
setShowConfirmModal(true);
|
||||
};
|
||||
|
||||
const confirmInit = async () => {
|
||||
setShowConfirmModal(false);
|
||||
setLoading(true);
|
||||
setCurrentStep(0);
|
||||
|
||||
try {
|
||||
if (initMode === 'setup') {
|
||||
// 首次安装模式
|
||||
await firstTimeSetup();
|
||||
} else {
|
||||
// 重置模式
|
||||
await systemReset();
|
||||
}
|
||||
|
||||
// 标记系统已初始化
|
||||
localStorage.setItem('aiNovelInitialized', new Date().toISOString());
|
||||
|
||||
setCurrentStep(currentStep + 1);
|
||||
message.success(initMode === 'setup' ? '系统配置完成!' : '系统重置完成!');
|
||||
|
||||
// 重新获取统计数据
|
||||
setTimeout(() => {
|
||||
getDbStats();
|
||||
}, 1000);
|
||||
|
||||
} catch (error: any) {
|
||||
setInitStatus({
|
||||
step: currentStep + 1,
|
||||
message: `初始化失败: ${error.message}`,
|
||||
status: 'error'
|
||||
});
|
||||
message.error('系统初始化失败,请查看错误信息');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const firstTimeSetup = async () => {
|
||||
const setupSteps = [
|
||||
// 步骤1:初始化IndexedDB
|
||||
{
|
||||
step: 0,
|
||||
message: '正在初始化IndexedDB数据库...',
|
||||
action: async () => {
|
||||
await reinitializeIndexedDB();
|
||||
}
|
||||
},
|
||||
// 步骤2:创建默认配置
|
||||
{
|
||||
step: 1,
|
||||
message: '正在创建默认配置...',
|
||||
action: async () => {
|
||||
restoreDefaultSettings();
|
||||
}
|
||||
},
|
||||
// 步骤3:初始化技能库
|
||||
{
|
||||
step: 2,
|
||||
message: '正在初始化技能库...',
|
||||
action: async () => {
|
||||
await initializeSkills();
|
||||
}
|
||||
},
|
||||
// 步骤4:配置系统参数
|
||||
{
|
||||
step: 3,
|
||||
message: '正在配置系统参数...',
|
||||
action: async () => {
|
||||
await configureSystem();
|
||||
}
|
||||
},
|
||||
// 步骤5:验证系统状态
|
||||
{
|
||||
step: 4,
|
||||
message: '正在验证系统状态...',
|
||||
action: async () => {
|
||||
await verifySystem();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const { step, message, action } of setupSteps) {
|
||||
setCurrentStep(step);
|
||||
setInitStatus({ step: step + 1, message, status: 'process' });
|
||||
|
||||
await action();
|
||||
|
||||
setInitStatus({
|
||||
step: step + 1,
|
||||
message: `${message.replace('正在', '')}完成`,
|
||||
status: 'finish'
|
||||
});
|
||||
await sleep(800);
|
||||
}
|
||||
};
|
||||
|
||||
const systemReset = async () => {
|
||||
// 步骤1:清除IndexedDB数据
|
||||
setCurrentStep(0);
|
||||
setInitStatus({ step: 1, message: '正在清除IndexedDB数据...', status: 'process' });
|
||||
|
||||
await clearIndexedDB();
|
||||
|
||||
setInitStatus({ step: 1, message: 'IndexedDB数据清除完成', status: 'finish' });
|
||||
await sleep(500);
|
||||
|
||||
// 步骤2:清除本地存储配置
|
||||
setCurrentStep(1);
|
||||
setInitStatus({ step: 2, message: '正在清除本地存储配置...', status: 'process' });
|
||||
|
||||
clearLocalStorage();
|
||||
|
||||
setInitStatus({ step: 2, message: '本地存储配置清除完成', status: 'finish' });
|
||||
await sleep(500);
|
||||
|
||||
// 步骤3:重新初始化IndexedDB
|
||||
setCurrentStep(2);
|
||||
setInitStatus({ step: 3, message: '正在重新初始化IndexedDB...', status: 'process' });
|
||||
|
||||
await reinitializeIndexedDB();
|
||||
|
||||
setInitStatus({ step: 3, message: 'IndexedDB重新初始化完成', status: 'finish' });
|
||||
await sleep(500);
|
||||
|
||||
// 步骤4:恢复默认配置
|
||||
setCurrentStep(3);
|
||||
setInitStatus({ step: 4, message: '正在恢复默认配置...', status: 'process' });
|
||||
|
||||
restoreDefaultSettings();
|
||||
|
||||
setInitStatus({ step: 4, message: '默认配置恢复完成', status: 'finish' });
|
||||
};
|
||||
|
||||
const clearIndexedDB = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('AINovelDatabase');
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('IndexedDB删除成功');
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('IndexedDB删除失败');
|
||||
reject(new Error('IndexedDB删除失败'));
|
||||
};
|
||||
|
||||
request.onblocked = () => {
|
||||
console.warn('IndexedDB删除被阻止,正在重试...');
|
||||
setTimeout(() => {
|
||||
clearIndexedDB().then(resolve).catch(reject);
|
||||
}, 1000);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const clearLocalStorage = () => {
|
||||
// 清除所有Ollama相关的设置
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && (key.includes('ollama') || key.includes('model') || key.includes('settings'))) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
console.log('清除了', keysToRemove.length, '个本地存储项');
|
||||
};
|
||||
|
||||
const reinitializeIndexedDB = async () => {
|
||||
const { indexedDBStorage } = await import('../utils/indexedDB');
|
||||
await indexedDBStorage.init();
|
||||
console.log('IndexedDB重新初始化完成');
|
||||
};
|
||||
|
||||
const restoreDefaultSettings = () => {
|
||||
// 恢复默认的Ollama设置
|
||||
const defaultSettings = {
|
||||
apiUrl: 'http://localhost:11434',
|
||||
model: '',
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxTokens: 2000
|
||||
};
|
||||
|
||||
localStorage.setItem('ollamaSettings', JSON.stringify(defaultSettings));
|
||||
console.log('默认配置恢复完成');
|
||||
};
|
||||
|
||||
const initializeSkills = async () => {
|
||||
// 初始化默认技能库
|
||||
const defaultSkills = [
|
||||
{
|
||||
id: 'skill_1',
|
||||
name: '小说设定生成',
|
||||
description: '根据用户的小说创意,自动生成完整的小说设定',
|
||||
category: '创作辅助',
|
||||
prompt: '你是一个专业的小说设定助手...',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'skill_2',
|
||||
name: '章节内容创作',
|
||||
description: '根据章节标题和细纲,自动创作具体的章节内容',
|
||||
category: '内容创作',
|
||||
prompt: '你是一个专业的小说作家...',
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// 可以存储到 localStorage 或 IndexedDB
|
||||
localStorage.setItem('defaultSkills', JSON.stringify(defaultSkills));
|
||||
console.log('技能库初始化完成');
|
||||
};
|
||||
|
||||
const configureSystem = async () => {
|
||||
// 配置其他系统参数
|
||||
const systemConfig = {
|
||||
version: '0.0.1',
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
autoSave: true,
|
||||
maxHistory: 50
|
||||
};
|
||||
|
||||
localStorage.setItem('systemConfig', JSON.stringify(systemConfig));
|
||||
console.log('系统参数配置完成');
|
||||
};
|
||||
|
||||
const verifySystem = async () => {
|
||||
// 验证各个组件是否正常工作
|
||||
const checks = [
|
||||
{
|
||||
name: 'IndexedDB',
|
||||
check: async () => {
|
||||
const { indexedDBStorage } = await import('../utils/indexedDB');
|
||||
await indexedDBStorage.init();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '本地存储',
|
||||
check: () => {
|
||||
try {
|
||||
localStorage.setItem('test', 'test');
|
||||
localStorage.removeItem('test');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const { name, check } of checks) {
|
||||
const result = await check();
|
||||
if (!result) {
|
||||
throw new Error(`${name}验证失败`);
|
||||
}
|
||||
console.log(`${name}验证通过`);
|
||||
}
|
||||
};
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const getSteps = () => {
|
||||
if (initMode === 'setup') {
|
||||
return [
|
||||
{
|
||||
title: '初始化数据库',
|
||||
description: '创建IndexedDB数据库和存储结构',
|
||||
icon: <DatabaseOutlined />
|
||||
},
|
||||
{
|
||||
title: '创建默认配置',
|
||||
description: '设置系统默认的Ollama配置',
|
||||
icon: <SettingOutlined />
|
||||
},
|
||||
{
|
||||
title: '初始化技能库',
|
||||
description: '加载默认的AI创作技能',
|
||||
icon: <FundOutlined />
|
||||
},
|
||||
{
|
||||
title: '配置系统参数',
|
||||
description: '设置主题、语言等系统参数',
|
||||
icon: <CloudUploadOutlined />
|
||||
},
|
||||
{
|
||||
title: '验证系统状态',
|
||||
description: '检查所有组件是否正常工作',
|
||||
icon: <SafetyOutlined />
|
||||
}
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
title: '清除IndexedDB',
|
||||
description: '删除所有存储的小说、章节等数据',
|
||||
icon: <DatabaseOutlined />
|
||||
},
|
||||
{
|
||||
title: '清除本地配置',
|
||||
description: '清除所有本地存储的配置信息',
|
||||
icon: <DeleteOutlined />
|
||||
},
|
||||
{
|
||||
title: '重新初始化',
|
||||
description: '重新创建数据库和存储结构',
|
||||
icon: <ReloadOutlined />
|
||||
},
|
||||
{
|
||||
title: '恢复默认配置',
|
||||
description: '设置系统的默认配置参数',
|
||||
icon: <SettingOutlined />
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={2} style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
{initMode === 'setup' ? (
|
||||
<RocketOutlined style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<ClearOutlined style={{ color: '#1890ff' }} />
|
||||
)}
|
||||
{initMode === 'setup' ? '系统初始化配置' : '系统重置'}
|
||||
</Title>
|
||||
<Text type="secondary" style={{ marginTop: '8px', display: 'block' }}>
|
||||
{initMode === 'setup'
|
||||
? '首次使用本系统,一键配置所有必要的组件和参数'
|
||||
: '一键重置系统所有数据和配置到初始状态'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{initMode === 'setup' ? (
|
||||
<Alert
|
||||
message="欢迎使用AI小说创作系统"
|
||||
description="检测到您是首次使用本系统,我们将为您自动配置必要的组件和设置,整个过程大约需要10-20秒。"
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<RocketOutlined />}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
message="危险操作警告"
|
||||
description="此操作将清除所有数据,包括小说、章节、配置等,且不可恢复。请谨慎操作!"
|
||||
type="warning"
|
||||
showIcon
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="小说数量"
|
||||
value={dbStats.novels}
|
||||
prefix={<DatabaseOutlined />}
|
||||
valueStyle={{ color: dbStats.novels > 0 ? '#1890ff' : '#999' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="章节数量"
|
||||
value={dbStats.chapters}
|
||||
prefix={<DatabaseOutlined />}
|
||||
valueStyle={{ color: dbStats.chapters > 0 ? '#52c41a' : '#999' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="配置文件"
|
||||
value={dbStats.settings}
|
||||
prefix={<SettingOutlined />}
|
||||
valueStyle={{ color: dbStats.settings > 0 ? '#faad14' : '#999' }}
|
||||
suffix="个"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
title="初始化进度"
|
||||
extra={
|
||||
<Button
|
||||
type={initMode === 'setup' ? 'primary' : 'primary'}
|
||||
danger={initMode !== 'setup'}
|
||||
icon={initMode === 'setup' ? <RocketOutlined /> : <ClearOutlined />}
|
||||
onClick={handleInit}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
size="large"
|
||||
>
|
||||
{loading
|
||||
? (initMode === 'setup' ? '配置中...' : '初始化中...')
|
||||
: (initMode === 'setup' ? '开始配置' : '开始初始化')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{loading && (
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
status={initStatus.status}
|
||||
items={getSteps().map((step, index) => ({
|
||||
...step,
|
||||
status: index < currentStep ? 'finish' :
|
||||
index === currentStep ? initStatus.status : 'wait'
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
background: initStatus.status === 'error' ? '#fff2f0' :
|
||||
initStatus.status === 'finish' ? '#f6ffed' : '#e6f7ff',
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${initStatus.status === 'error' ? '#ffccc7' :
|
||||
initStatus.status === 'finish' ? '#b7eb8f' : '#91d5ff'}`
|
||||
}}>
|
||||
<Space>
|
||||
{initStatus.status === 'process' && <ReloadOutlined spin style={{ color: '#1890ff' }} />}
|
||||
{initStatus.status === 'finish' && <CheckCircleOutlined style={{ color: '#52c41a' }} />}
|
||||
{initStatus.status === 'error' && <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />}
|
||||
<Text strong>{initStatus.message}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{currentStep < getSteps().length && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Progress
|
||||
percent={Math.round(((currentStep + 1) / getSteps().length) * 100)}
|
||||
status={initStatus.status === 'error' ? 'exception' : 'active'}
|
||||
strokeColor={{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && currentStep === 0 && (
|
||||
<div>
|
||||
{initMode === 'setup' ? (
|
||||
<div>
|
||||
<Descriptions title="首次配置将执行以下操作" bordered column={1}>
|
||||
<Descriptions.Item
|
||||
label={<Space><DatabaseOutlined />初始化数据库</Space>}
|
||||
>
|
||||
创建IndexedDB数据库和存储结构,用于存储小说、章节等数据
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={<Space><SettingOutlined />创建默认配置</Space>}
|
||||
>
|
||||
设置Ollama服务连接参数和AI模型配置
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={<Space><FundOutlined />初始化技能库</Space>}
|
||||
>
|
||||
加载默认的AI创作技能模板,如小说设定生成、章节创作等
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={<Space><CloudUploadOutlined />配置系统参数</Space>}
|
||||
>
|
||||
设置界面主题、语言、自动保存等系统参数
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={<Space><SafetyOutlined />验证系统状态</Space>}
|
||||
>
|
||||
检查数据库连接、本地存储等组件是否正常工作
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Alert
|
||||
message="配置完成后您将获得"
|
||||
description={
|
||||
<ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
|
||||
<li>完整的数据存储系统</li>
|
||||
<li>预配置的AI模型连接参数</li>
|
||||
<li>丰富的AI创作技能库</li>
|
||||
<li>优化的系统使用体验</li>
|
||||
</ul>
|
||||
}
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Descriptions title="初始化将执行以下操作" bordered column={1}>
|
||||
<Descriptions.Item
|
||||
label={<Space><DatabaseOutlined />清除IndexedDB</Space>}
|
||||
>
|
||||
删除所有存储的小说、章节、草稿等数据
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={<Space><DeleteOutlined />清除本地配置</Space>}
|
||||
>
|
||||
清除所有本地存储的Ollama设置和用户配置
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={<Space><ReloadOutlined />重新初始化</Space>}
|
||||
>
|
||||
重新创建数据库结构和存储对象
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={<Space><SettingOutlined />恢复默认配置</Space>}
|
||||
>
|
||||
设置系统默认的参数配置
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Alert
|
||||
message="建议在以下情况下执行初始化"
|
||||
description={
|
||||
<ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
|
||||
<li>系统出现异常数据错误</li>
|
||||
<li>数据库损坏或无法正常访问</li>
|
||||
<li>需要完全清除所有数据重新开始</li>
|
||||
<li>升级到不兼容的新版本</li>
|
||||
</ul>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === getSteps().length && initStatus.status === 'finish' && (
|
||||
<div>
|
||||
<Alert
|
||||
message={initMode === 'setup' ? '系统配置完成!' : '系统初始化成功'}
|
||||
description={initMode === 'setup'
|
||||
? '所有必要的组件和参数已配置完成,系统现在可以正常使用了。'
|
||||
: '所有数据和配置已重置到初始状态,系统现在可以正常使用了。'}
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{initMode === 'setup' ? (
|
||||
<>
|
||||
<Card size="small" style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>数据库已初始化</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>IndexedDB数据库和存储结构已创建完成</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card size="small" style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>配置已设置</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>Ollama服务和AI模型配置已设置完成</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card size="small" style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>技能库已加载</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>默认AI创作技能模板已加载完成</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card size="small" style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>系统已就绪</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>所有组件运行正常,可以开始创作之旅</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Card size="small" style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>IndexedDB已重置</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>数据库已重新创建,可以开始新的创作</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card size="small" style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>配置已恢复</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>系统配置已恢复到默认设置</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card size="small" style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Space>
|
||||
<SafetyOutlined style={{ color: '#52c41a', fontSize: '20px' }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>系统就绪</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>所有组件运行正常,可以开始使用</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
{initMode === 'setup' ? (
|
||||
<RocketOutlined style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<ExclamationCircleOutlined style={{ color: '#faad14' }} />
|
||||
)}
|
||||
{initMode === 'setup' ? '确认开始系统配置' : '确认初始化系统'}
|
||||
</Space>
|
||||
}
|
||||
open={showConfirmModal}
|
||||
onOk={confirmInit}
|
||||
onCancel={() => setShowConfirmModal(false)}
|
||||
okText={initMode === 'setup' ? '开始配置' : '确认初始化'}
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: initMode !== 'setup' }}
|
||||
>
|
||||
{initMode === 'setup' ? (
|
||||
<Alert
|
||||
message="准备开始系统配置"
|
||||
description={
|
||||
<div>
|
||||
<p>系统将为您自动配置以下组件:</p>
|
||||
<ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
|
||||
<li>创建IndexedDB数据库结构</li>
|
||||
<li>设置Ollama服务连接参数</li>
|
||||
<li>加载默认AI创作技能库</li>
|
||||
<li>配置系统界面和功能参数</li>
|
||||
<li>验证所有组件正常工作</li>
|
||||
</ul>
|
||||
<p style={{ color: '#52c41a', fontWeight: 'bold' }}>
|
||||
整个过程大约需要10-20秒,请耐心等待。
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
message="此操作不可逆"
|
||||
description={
|
||||
<div>
|
||||
<p>您即将执行系统初始化操作,这将:</p>
|
||||
<ul style={{ margin: '8px 0', paddingLeft: '20px' }}>
|
||||
<li><strong>永久删除</strong>所有小说数据({dbStats.novels}个小说)</li>
|
||||
<li><strong>永久删除</strong>所有章节内容({dbStats.chapters}个章节)</li>
|
||||
<li><strong>清除所有</strong>配置和设置</li>
|
||||
<li><strong>重置系统</strong>到初始状态</li>
|
||||
</ul>
|
||||
<p style={{ color: '#ff4d4f', fontWeight: 'bold' }}>
|
||||
这些数据将无法恢复!请确保已做好数据备份。
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
{initMode !== 'setup' && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Text strong>请输入 "CONFIRM" 来确认此操作:</Text>
|
||||
<Input.Password
|
||||
placeholder="输入 CONFIRM 确认"
|
||||
onChange={(e) => {
|
||||
// 可以添加额外的确认逻辑
|
||||
}}
|
||||
style={{ marginTop: '8px' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitPage;
|
||||
468
src/pages/ModelSettings.tsx
Normal file
468
src/pages/ModelSettings.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Form, Input, InputNumber, Button, Space, Divider, Select, Spin, Tag, App } from 'antd';
|
||||
import { ApiOutlined, SaveOutlined, SyncOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { OllamaService } from '../utils/ollama';
|
||||
import { useOllama } from '../contexts/OllamaContext';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface ModelInfo {
|
||||
name: string;
|
||||
size?: number;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
const ModelSettings: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [detectingModels, setDetectingModels] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'success' | 'error'>('unknown');
|
||||
const [currentModel, setCurrentModel] = useState<string>('');
|
||||
const { status, refreshModels } = useOllama();
|
||||
const { message: messageApi } = App.useApp();
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
// 从 localStorage 加载配置
|
||||
const configData = localStorage.getItem('ai_system_config');
|
||||
const config = configData ? JSON.parse(configData) : {
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: '',
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxTokens: 2000
|
||||
};
|
||||
|
||||
form.setFieldsValue(config);
|
||||
setCurrentModel(config.model || '');
|
||||
|
||||
// 如果已连接且有模型,自动显示当前状态
|
||||
if (status.isConnected && status.availableModels.length > 0) {
|
||||
setAvailableModels(status.availableModels);
|
||||
setConnectionStatus('success');
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
}, [form, status]);
|
||||
|
||||
const handleDetectModels = async () => {
|
||||
setDetectingModels(true);
|
||||
setConnectionStatus('unknown');
|
||||
try {
|
||||
const values = await form.validateFields(['ollamaUrl']);
|
||||
const ollamaService = new OllamaService(values);
|
||||
|
||||
// 测试连接
|
||||
const isConnected = await ollamaService.testConnection();
|
||||
if (!isConnected) {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '无法连接到 Ollama 服务,请检查服务地址和状态',
|
||||
});
|
||||
setAvailableModels([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取模型列表
|
||||
const models = await ollamaService.getAvailableModelsWithInfo();
|
||||
setAvailableModels(models);
|
||||
setConnectionStatus('success');
|
||||
|
||||
if (models.length === 0) {
|
||||
messageApi.open({
|
||||
type: 'warning',
|
||||
content: '未检测到已安装的模型,请先使用 ollama pull 命令安装模型',
|
||||
});
|
||||
} else {
|
||||
messageApi.open({
|
||||
type: 'success',
|
||||
content: `成功检测到 ${models.length} 个已安装模型`,
|
||||
});
|
||||
|
||||
// 如果当前模型不在列表中,清空选择
|
||||
if (currentModel && !models.find(m => m.name === currentModel)) {
|
||||
form.setFieldValue('model', undefined);
|
||||
setCurrentModel('');
|
||||
messageApi.open({
|
||||
type: 'warning',
|
||||
content: '当前选择的模型未在本地安装,请重新选择',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新全局状态
|
||||
await refreshModels();
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '模型检测失败,请检查 Ollama 服务状态',
|
||||
});
|
||||
setAvailableModels([]);
|
||||
} finally {
|
||||
setDetectingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true);
|
||||
setConnectionStatus('unknown');
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const ollamaService = new OllamaService(values);
|
||||
const isConnected = await ollamaService.testConnection();
|
||||
|
||||
if (isConnected) {
|
||||
setConnectionStatus('success');
|
||||
messageApi.open({
|
||||
type: 'success',
|
||||
content: 'Ollama 服务连接成功!',
|
||||
});
|
||||
await refreshModels();
|
||||
} else {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: 'Ollama 服务连接失败,请检查服务地址',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '连接测试失败,请检查配置',
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelChange = (value: string) => {
|
||||
setCurrentModel(value);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证选择的模型是否可用
|
||||
if (currentModel && availableModels.length > 0) {
|
||||
const modelExists = availableModels.find(m => m.name === currentModel);
|
||||
if (!modelExists) {
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '请选择本地已安装的模型,或点击"检测模型"刷新列表',
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保包含当前选择的模型
|
||||
const configToSave = {
|
||||
...values,
|
||||
model: currentModel || values.model
|
||||
};
|
||||
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem('ai_system_config', JSON.stringify(configToSave));
|
||||
|
||||
// 显示保存成功提示
|
||||
messageApi.open({
|
||||
type: 'success',
|
||||
content: '配置保存成功!',
|
||||
});
|
||||
|
||||
// 更新当前模型显示
|
||||
setCurrentModel(configToSave.model);
|
||||
|
||||
// 刷新全局状态
|
||||
await refreshModels();
|
||||
} catch (error) {
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '配置保存失败,请检查输入',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatModelSize = (bytes?: number) => {
|
||||
if (!bytes) return '未知';
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
return `${gb.toFixed(2)} GB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600, color: '#262626' }}>模型设置</h2>
|
||||
<p style={{ margin: '8px 0 0 0', color: '#8c8c8c', fontSize: '14px' }}>配置本地 Ollama 服务和 AI 模型参数</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
marginBottom: '16px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '16px 24px',
|
||||
background: '#fafafa',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: connectionStatus === 'success' ? '#52c41a' : connectionStatus === 'error' ? '#ff4d4f' : '#faad14'
|
||||
}} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, color: '#262626' }}>
|
||||
{connectionStatus === 'success' ? '服务正常' : connectionStatus === 'error' ? '服务异常' : '未检测'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
|
||||
{availableModels.length > 0
|
||||
? `已安装 ${availableModels.length} 个模型`
|
||||
: connectionStatus === 'success'
|
||||
? '未安装模型'
|
||||
: '请先连接服务'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card bordered={false} style={{ boxShadow: 'none' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: '',
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxTokens: 2000
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
服务配置
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>Ollama 服务地址</span>}
|
||||
name="ollamaUrl"
|
||||
rules={[{ required: true, message: '请输入Ollama服务地址' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="http://localhost:11434"
|
||||
style={{ borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ marginBottom: '16px' }}>
|
||||
<Button
|
||||
icon={<ApiOutlined />}
|
||||
onClick={handleTestConnection}
|
||||
loading={testing}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
测试连接
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleDetectModels}
|
||||
loading={detectingModels}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
检测模型
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
模型选择
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<span style={{ color: '#595959' }}>AI 模型</span>
|
||||
{availableModels.length > 0 && (
|
||||
<Tag color="success">{availableModels.length} 个可用</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
name="model"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: '请选择AI模型'
|
||||
}]}
|
||||
tooltip="只能选择本地已安装的模型"
|
||||
>
|
||||
<Select
|
||||
placeholder={availableModels.length === 0 ? "请先点击检测模型" : "选择AI模型"}
|
||||
showSearch
|
||||
allowClear
|
||||
onChange={handleModelChange}
|
||||
notFoundContent={detectingModels ? <Spin size="small" /> : "未检测到可用模型"}
|
||||
disabled={availableModels.length === 0}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
{availableModels.map(model => (
|
||||
<Option key={model.name} value={model.name}>
|
||||
<Space>
|
||||
<span>{model.name}</span>
|
||||
{model.size && (
|
||||
<span style={{ color: '#8c8c8c', fontSize: '12px' }}>
|
||||
({formatModelSize(model.size)})
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{currentModel && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
<span style={{ color: '#52c41a', fontSize: '14px' }}>
|
||||
当前使用模型: {currentModel}
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
生成参数
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>温度</span>}
|
||||
name="temperature"
|
||||
rules={[{ required: true, message: '请输入温度值' }]}
|
||||
tooltip="控制生成文本的随机性"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
precision={1}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>Top P</span>}
|
||||
name="topP"
|
||||
rules={[{ required: true, message: '请输入Top P值' }]}
|
||||
tooltip="控制生成文本的多样性"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
precision={1}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>最大 Tokens</span>}
|
||||
name="maxTokens"
|
||||
rules={[{ required: true, message: '请输入最大生成长度' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={100}
|
||||
max={8000}
|
||||
step={100}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
size="large"
|
||||
style={{ borderRadius: '6px', minWidth: '120px' }}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider style={{ margin: '32px 0' }} />
|
||||
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
使用说明
|
||||
</div>
|
||||
<ul style={{
|
||||
margin: 0,
|
||||
paddingLeft: '20px',
|
||||
color: '#595959',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.8'
|
||||
}}>
|
||||
<li>确保 Ollama 服务正在运行(默认端口 11434)</li>
|
||||
<li>点击"测试连接"验证 Ollama 服务是否正常</li>
|
||||
<li>点击"检测模型"获取本地已安装的模型列表</li>
|
||||
<li>模型选择限制为只能使用本地已安装的模型</li>
|
||||
<li>如需新模型,请使用命令:ollama pull 模型名</li>
|
||||
<li>推荐:qwen3:8b起步</li>
|
||||
<li>温度参数控制生成文本的随机性,建议值为 0.7</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSettings;
|
||||
1421
src/pages/NovelDetail.tsx
Normal file
1421
src/pages/NovelDetail.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1339
src/pages/NovelGenerate.tsx
Normal file
1339
src/pages/NovelGenerate.tsx
Normal file
File diff suppressed because it is too large
Load Diff
414
src/pages/NovelList.tsx
Normal file
414
src/pages/NovelList.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Modal, Form, Input, message, Popconfirm, Row, Col, Tag, Select } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { storage } from '../utils/indexedDB';
|
||||
import { Novel } from '../types';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
// 小说题材选项
|
||||
const NOVEL_GENRES = [
|
||||
'穿越',
|
||||
'都市',
|
||||
'修仙',
|
||||
'武侠',
|
||||
'玄幻',
|
||||
'科幻',
|
||||
'言情',
|
||||
'历史',
|
||||
'游戏',
|
||||
'灵异',
|
||||
'军事',
|
||||
'悬疑',
|
||||
'其他'
|
||||
];
|
||||
|
||||
const NovelList: React.FC = () => {
|
||||
const [novels, setNovels] = useState<Novel[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [viewModalVisible, setViewModalVisible] = useState(false);
|
||||
const [editingNovel, setEditingNovel] = useState<Novel | null>(null);
|
||||
const [viewingNovel, setViewingNovel] = useState<Novel | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
loadNovels();
|
||||
}, []);
|
||||
|
||||
const loadNovels = async () => {
|
||||
try {
|
||||
const loadedNovels = await storage.getNovels();
|
||||
setNovels(loadedNovels);
|
||||
} catch (error) {
|
||||
console.error('加载小说列表失败:', error);
|
||||
message.error('加载小说列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNovel = () => {
|
||||
setEditingNovel(null);
|
||||
form.resetFields();
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleViewNovel = (novel: Novel) => {
|
||||
setViewingNovel(novel);
|
||||
setViewModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditNovel = (novel: Novel) => {
|
||||
setEditingNovel(novel);
|
||||
form.setFieldsValue({
|
||||
title: novel.title,
|
||||
genre: novel.genre === '未分类' ? undefined : novel.genre,
|
||||
outline: novel.outline
|
||||
});
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDeleteNovel = async (id: string) => {
|
||||
try {
|
||||
await storage.deleteNovel(id);
|
||||
await loadNovels();
|
||||
message.success('小说删除成功');
|
||||
} catch (error) {
|
||||
console.error('删除小说失败:', error);
|
||||
message.error('删除失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (editingNovel) {
|
||||
await storage.updateNovel(editingNovel.id, {
|
||||
...values,
|
||||
genre: values.genre || '未分类'
|
||||
});
|
||||
message.success('小说更新成功');
|
||||
} else {
|
||||
const newNovel: Novel = {
|
||||
id: Date.now().toString(),
|
||||
title: values.title,
|
||||
genre: values.genre || '未分类',
|
||||
outline: values.outline || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
await storage.addNovel(newNovel);
|
||||
message.success('小说创建成功,请前往AI生成页面完善设定');
|
||||
}
|
||||
|
||||
await loadNovels();
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
console.error('保存小说失败:', error);
|
||||
message.error('保存失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
setEditingNovel(null);
|
||||
};
|
||||
|
||||
const handleCardClick = (id: string) => {
|
||||
navigate(`/novels/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600, color: '#262626' }}>我的小说</h2>
|
||||
<p style={{ margin: '8px 0 0 0', color: '#8c8c8c', fontSize: '14px' }}>管理和创作您的 AI 小说作品</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<div style={{ fontSize: '14px', color: '#595959' }}>
|
||||
共 {novels.length} 部小说
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddNovel}
|
||||
size="large"
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
新建小说
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{novels.length === 0 ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '80px 20px',
|
||||
background: 'white',
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9'
|
||||
}}>
|
||||
<BookOutlined style={{ fontSize: '48px', color: '#d9d9d9', marginBottom: '16px' }} />
|
||||
<div style={{ fontSize: '16px', color: '#595959', marginBottom: '8px' }}>
|
||||
还没有小说,开始创作您的第一部作品吧!
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>
|
||||
点击"新建小说"按钮开始您的创作之旅
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Row gutter={[16, 16]}>
|
||||
{novels.map((novel) => (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={novel.id}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
height: '100%',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
onClick={() => handleCardClick(novel.id)}
|
||||
actions={[
|
||||
<EyeOutlined
|
||||
key="view"
|
||||
style={{ color: '#52c41a' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleViewNovel(novel);
|
||||
}}
|
||||
/>,
|
||||
<EditOutlined
|
||||
key="edit"
|
||||
style={{ color: '#1890ff' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditNovel(novel);
|
||||
}}
|
||||
/>,
|
||||
<Popconfirm
|
||||
title="确认删除"
|
||||
description="确定要删除这部小说吗?所有章节内容也将被删除。"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDeleteNovel(novel.id);
|
||||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<DeleteOutlined
|
||||
key="delete"
|
||||
style={{ color: '#ff4d4f' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={
|
||||
<div style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '8px',
|
||||
background: '#e6f7ff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<BookOutlined style={{ fontSize: '20px', color: '#1890ff' }} />
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{novel.title}
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Tag color="blue">{novel.genre}</Tag>
|
||||
{novel.generatedSettings && (
|
||||
<Tag color="green">已完善设定</Tag>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#8c8c8c',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
{novel.outline || '暂无大纲'}
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', fontSize: '11px', color: '#bfbfbf' }}>
|
||||
创建于 {new Date(novel.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={editingNovel ? '编辑小说' : '新建小说'}
|
||||
open={isModalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
width={600}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
label="书名"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '请输入书名' }]}
|
||||
>
|
||||
<Input placeholder="请输入书名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="题材类型"
|
||||
name="genre"
|
||||
rules={[{ required: true, message: '请选择题材类型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择题材类型"
|
||||
size="large"
|
||||
showSearch
|
||||
allowClear
|
||||
>
|
||||
{NOVEL_GENRES.map(genre => (
|
||||
<Option key={genre} value={genre}>
|
||||
{genre}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="初步想法"
|
||||
name="outline"
|
||||
extra="可以简单描述您的初步想法,也可以留空,后续让AI帮您生成"
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="简单描述您的创作想法..."
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="小说详情"
|
||||
open={viewModalVisible}
|
||||
onCancel={() => setViewModalVisible(false)}
|
||||
width={700}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setViewModalVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{viewingNovel && (
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
||||
书名
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
{viewingNovel.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
||||
题材
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<Tag color="blue">{viewingNovel.genre}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewingNovel.generatedSettings && (
|
||||
<>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
||||
AI生成设定
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f0f9ff',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
<div><strong>目标字数:</strong>{viewingNovel.generatedSettings.targetWordCount}字</div>
|
||||
<div><strong>章节数量:</strong>{viewingNovel.generatedSettings.chapterCount}章</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ fontSize: '14px', color: '#595959', marginBottom: '8px' }}>
|
||||
大纲
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
lineHeight: '1.8',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{viewingNovel.outline || '暂无大纲'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', paddingTop: '16px', borderTop: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
|
||||
创建时间:{new Date(viewingNovel.createdAt).toLocaleString()}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c', marginTop: '4px' }}>
|
||||
更新时间:{new Date(viewingNovel.updatedAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
478
src/pages/SkillsPage.tsx
Normal file
478
src/pages/SkillsPage.tsx
Normal file
@@ -0,0 +1,478 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Tag,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Row,
|
||||
Col,
|
||||
Popconfirm,
|
||||
Divider
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ThunderboltOutlined,
|
||||
CodeOutlined,
|
||||
BulbOutlined,
|
||||
RobotOutlined,
|
||||
BookOutlined,
|
||||
EditOutlined as EditIcon
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface Skill {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
prompt: string;
|
||||
examples: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const SkillsPage: React.FC = () => {
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingSkill, setEditingSkill] = useState<Skill | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
loadSkills();
|
||||
}, []);
|
||||
|
||||
const loadSkills = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 这里将来可以连接到实际的数据库
|
||||
// 现在先使用模拟数据
|
||||
const mockSkills: Skill[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '小说设定生成',
|
||||
description: '根据用户的小说创意,自动生成完整的小说设定,包括角色、世界观、故事大纲等',
|
||||
category: '创作辅助',
|
||||
prompt: '你是一个专业的小说设定助手。根据用户提供的小说创意,生成详细的小说设定,包括:1. 故事大纲(150字内)2. 主要角色设定(至少3个角色)3. 世界观背景 4. 核心冲突 5. 情节发展建议',
|
||||
examples: '用户输入:写一个现代都市修仙小说\n\n生成结果:\n故事大纲:普通程序员意外获得修仙传承,在现代都市中一边工作一边修炼,逐渐发现都市中隐藏的修仙世界...',
|
||||
createdAt: '2024-01-15',
|
||||
updatedAt: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '章节内容创作',
|
||||
description: '根据章节标题和细纲,自动创作具体的章节内容,保持风格连贯和情节合理',
|
||||
category: '内容创作',
|
||||
prompt: '你是一个专业的小说作家。根据提供的章节标题、细纲和前文内容,创作符合要求的章节内容。要求:1. 严格遵守细纲要求 2. 保持人物性格一致 3. 语言生动流畅 4. 字数控制在900-1200字',
|
||||
examples: '章节标题:第一章 意外穿越\n细纲:主角意外穿越到修仙世界,获得神秘传承\n前文:无\n\n生成结果:林明醒来时,发现自己躺在一片陌生的森林中...',
|
||||
createdAt: '2024-01-16',
|
||||
updatedAt: '2024-01-16'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '角色对话优化',
|
||||
description: '优化小说中的人物对话,使其更符合角色性格和场景氛围',
|
||||
category: '内容优化',
|
||||
prompt: '你是一个对话优化专家。根据提供的对话内容和角色设定,优化对话表达,使其:1. 更符合角色性格 2. 更贴合场景氛围 3. 语言更自然流畅 4. 保持原有意思不变',
|
||||
examples: '原对话:"你是什么人?"林明问。\n优化后:"你是谁?"林明的声音里带着警惕,眼神紧盯着对方...',
|
||||
createdAt: '2024-01-17',
|
||||
updatedAt: '2024-01-17'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '情节建议生成',
|
||||
description: '为小说创作提供情节发展建议,帮助解决创作瓶颈',
|
||||
category: '创作辅助',
|
||||
prompt: '你是一个创意写作顾问。根据用户提供的当前情节和创作瓶颈,提供3-5个情节发展建议,每个建议都要:1. 符合故事逻辑 2. 具有戏剧冲突 3. 推动情节发展 4. 保持人物一致性',
|
||||
examples: '当前情节:主角刚刚获得修仙传承,但不知道如何修炼\n\n建议1:安排一位神秘导师,指点主角入门...\n建议2:主角在修炼过程中遇到困难,需要寻找资源...',
|
||||
createdAt: '2024-01-18',
|
||||
updatedAt: '2024-01-18'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '文笔风格调整',
|
||||
description: '调整小说的文笔风格,如简洁、华丽、幽默等不同风格',
|
||||
category: '内容优化',
|
||||
prompt: '你是一个文笔风格调整专家。根据用户指定的风格要求,调整文本的表达方式。可选风格:简洁明快、华丽优美、幽默风趣、严肃深沉等。要求保持原意不变,只改变表达方式。',
|
||||
examples: '原文:他走进了房间,看到了一个人坐在那里。\n简洁风格:他进屋,见一人独坐。\n华丽风格:他缓步入室,目光所及,见一人静坐其间...',
|
||||
createdAt: '2024-01-19',
|
||||
updatedAt: '2024-01-19'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: '章节大纲生成',
|
||||
description: '为指定章节生成详细的章节大纲,包括主要情节、转折点、人物发展等',
|
||||
category: '创作辅助',
|
||||
prompt: '你是一个章节大纲专家。根据小说总体设定和章节要求,生成详细的章节大纲。大纲应包含:1. 章节主题 2. 主要情节发展 3. 重要转折点 4. 人物心理变化 5. 与前后章节的衔接',
|
||||
examples: '小说:都市修仙\n章节:第5章\n要求:主角首次展示修仙能力\n\n大纲:章节主题:初露锋芒\n主要情节:主角在公司遇到危机,情急之下使用修仙能力...',
|
||||
createdAt: '2024-01-20',
|
||||
updatedAt: '2024-01-20'
|
||||
}
|
||||
];
|
||||
|
||||
setSkills(mockSkills);
|
||||
} catch (error) {
|
||||
message.error('加载技能列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingSkill(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (skill: Skill) => {
|
||||
setEditingSkill(skill);
|
||||
form.setFieldsValue(skill);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
// 这里将来连接到实际的数据库删除操作
|
||||
setSkills(skills.filter(skill => skill.id !== id));
|
||||
message.success('技能删除成功');
|
||||
} catch (error) {
|
||||
message.error('删除技能失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (editingSkill) {
|
||||
// 编辑模式
|
||||
const updatedSkill: Skill = {
|
||||
...editingSkill,
|
||||
...values,
|
||||
updatedAt: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
setSkills(skills.map(skill =>
|
||||
skill.id === editingSkill.id ? updatedSkill : skill
|
||||
));
|
||||
message.success('技能更新成功');
|
||||
} else {
|
||||
// 新增模式
|
||||
const newSkill: Skill = {
|
||||
id: Date.now().toString(),
|
||||
...values,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
updatedAt: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
setSkills([...skills, newSkill]);
|
||||
message.success('技能添加成功');
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
message.error('表单验证失败');
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch (category) {
|
||||
case '创作辅助':
|
||||
return <BulbOutlined style={{ color: '#faad14', fontSize: '20px' }} />;
|
||||
case '内容创作':
|
||||
return <EditIcon style={{ color: '#52c41a', fontSize: '20px' }} />;
|
||||
case '内容优化':
|
||||
return <CodeOutlined style={{ color: '#1890ff', fontSize: '20px' }} />;
|
||||
default:
|
||||
return <ThunderboltOutlined style={{ color: '#8c8c8c', fontSize: '20px' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
switch (category) {
|
||||
case '创作辅助':
|
||||
return 'orange';
|
||||
case '内容创作':
|
||||
return 'green';
|
||||
case '内容优化':
|
||||
return 'blue';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Title level={2} style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<ThunderboltOutlined style={{ color: '#1890ff' }} />
|
||||
技能管理
|
||||
</Title>
|
||||
<Text type="secondary" style={{ marginTop: '8px', display: 'block' }}>
|
||||
管理和配置 AI 智能体的各种创作技能
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAdd}
|
||||
size="large"
|
||||
>
|
||||
新建技能
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ background: '#f0f9ff', border: '1px solid #91d5ff' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#1890ff' }}>
|
||||
{skills.length}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||
总技能数
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#52c41a' }}>
|
||||
{skills.filter(s => s.category === '创作辅助').length}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||
创作辅助
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ background: '#fff9e6', border: '1px solid #ffe58f' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#faad14' }}>
|
||||
{skills.filter(s => s.category === '内容创作' || s.category === '内容优化').length}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||
内容处理
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
加载中...
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
|
||||
gap: '16px'
|
||||
}}>
|
||||
{skills.map((skill) => (
|
||||
<Card
|
||||
key={skill.id}
|
||||
hoverable
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
actions={[
|
||||
<Button
|
||||
key="edit"
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(skill)}
|
||||
>
|
||||
编辑
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确认删除"
|
||||
description="确定要删除这个技能吗?"
|
||||
onConfirm={() => handleDelete(skill.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '16px' }}>
|
||||
<div style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '8px',
|
||||
background: '#f0f9ff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: '12px'
|
||||
}}>
|
||||
{getCategoryIcon(skill.category)}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Title level={4} style={{ margin: 0, marginBottom: '8px' }}>
|
||||
{skill.name}
|
||||
</Title>
|
||||
<Tag color={getCategoryColor(skill.category)}>
|
||||
{skill.category}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
ellipsis={{ rows: 2 }}
|
||||
style={{
|
||||
color: '#666',
|
||||
marginBottom: '16px',
|
||||
minHeight: '44px'
|
||||
}}
|
||||
>
|
||||
{skill.description}
|
||||
</Paragraph>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<BookOutlined style={{ marginRight: '4px' }} />
|
||||
提示词模板:{skill.prompt.length} 字符
|
||||
</div>
|
||||
{skill.examples && (
|
||||
<div>
|
||||
<BulbOutlined style={{ marginRight: '4px' }} />
|
||||
包含示例
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
更新时间:{skill.updatedAt}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{skills.length === 0 && !loading && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '100px 0',
|
||||
color: '#999'
|
||||
}}>
|
||||
<RobotOutlined style={{ fontSize: '64px', color: '#d9d9d9', marginBottom: '16px' }} />
|
||||
<div>还没有技能,点击上方按钮创建第一个技能吧!</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<ThunderboltOutlined style={{ color: '#1890ff' }} />
|
||||
{editingSkill ? '编辑技能' : '新建技能'}
|
||||
</Space>
|
||||
}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={800}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ marginTop: '24px' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="技能名称"
|
||||
rules={[{ required: true, message: '请输入技能名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:小说设定生成" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="category"
|
||||
label="技能分类"
|
||||
rules={[{ required: true, message: '请选择技能分类' }]}
|
||||
>
|
||||
<Select placeholder="选择分类">
|
||||
<Select.Option value="创作辅助">创作辅助</Select.Option>
|
||||
<Select.Option value="内容创作">内容创作</Select.Option>
|
||||
<Select.Option value="内容优化">内容优化</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="技能描述"
|
||||
rules={[{ required: true, message: '请输入技能描述' }]}
|
||||
>
|
||||
<TextArea
|
||||
placeholder="简单描述这个技能的功能和用途"
|
||||
rows={2}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="prompt"
|
||||
label="提示词模板"
|
||||
rules={[{ required: true, message: '请输入提示词模板' }]}
|
||||
>
|
||||
<TextArea
|
||||
placeholder="输入 AI 使用的提示词模板,可以使用变量占位符"
|
||||
rows={6}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="examples"
|
||||
label="使用示例"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="输入使用示例,帮助用户理解如何使用这个技能"
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillsPage;
|
||||
399
src/pages/SystemConfig.tsx
Normal file
399
src/pages/SystemConfig.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Form, Input, InputNumber, Button, Space, message, Divider, Select, Spin, Tag } from 'antd';
|
||||
import { ApiOutlined, SaveOutlined, SyncOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { storage } from '../utils/indexedDB';
|
||||
import { OllamaService } from '../utils/ollama';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface ModelInfo {
|
||||
name: string;
|
||||
size?: number;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
const SystemConfig: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [detectingModels, setDetectingModels] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'success' | 'error'>('unknown');
|
||||
const [currentModel, setCurrentModel] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
const config = await storage.getSystemConfig();
|
||||
form.setFieldsValue(config);
|
||||
setCurrentModel(config.model || '');
|
||||
};
|
||||
loadConfig();
|
||||
}, [form]);
|
||||
|
||||
const handleDetectModels = async () => {
|
||||
setDetectingModels(true);
|
||||
setConnectionStatus('unknown');
|
||||
try {
|
||||
const values = await form.validateFields(['ollamaUrl']);
|
||||
const ollamaService = new OllamaService(values);
|
||||
|
||||
// 测试连接
|
||||
const isConnected = await ollamaService.testConnection();
|
||||
if (!isConnected) {
|
||||
setConnectionStatus('error');
|
||||
message.error('无法连接到 Ollama 服务,请检查服务地址和状态');
|
||||
setAvailableModels([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取模型列表
|
||||
const models = await ollamaService.getAvailableModelsWithInfo();
|
||||
setAvailableModels(models);
|
||||
setConnectionStatus('success');
|
||||
|
||||
if (models.length === 0) {
|
||||
message.warning('未检测到已安装的模型,请先使用 ollama pull 命令安装模型');
|
||||
} else {
|
||||
message.success(`成功检测到 ${models.length} 个已安装模型`);
|
||||
|
||||
// 如果当前模型不在列表中,清空选择
|
||||
if (currentModel && !models.find(m => m.name === currentModel)) {
|
||||
form.setFieldValue('model', undefined);
|
||||
setCurrentModel('');
|
||||
message.warning('当前选择的模型未在本地安装,请重新选择');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
message.error('模型检测失败,请检查 Ollama 服务状态');
|
||||
setAvailableModels([]);
|
||||
} finally {
|
||||
setDetectingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true);
|
||||
setConnectionStatus('unknown');
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const ollamaService = new OllamaService(values);
|
||||
const isConnected = await ollamaService.testConnection();
|
||||
|
||||
if (isConnected) {
|
||||
setConnectionStatus('success');
|
||||
message.success('Ollama 服务连接成功!');
|
||||
} else {
|
||||
setConnectionStatus('error');
|
||||
message.error('Ollama 服务连接失败,请检查服务地址');
|
||||
}
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
message.error('连接测试失败,请检查配置');
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelChange = (value: string) => {
|
||||
setCurrentModel(value);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证选择的模型是否可用
|
||||
if (currentModel && availableModels.length > 0) {
|
||||
const modelExists = availableModels.find(m => m.name === currentModel);
|
||||
if (!modelExists) {
|
||||
message.error('请选择本地已安装的模型,或点击"检测模型"刷新列表');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await storage.saveSystemConfig(values);
|
||||
message.success('配置保存成功!');
|
||||
} catch (error) {
|
||||
message.error('配置保存失败,请检查输入');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatModelSize = (bytes?: number) => {
|
||||
if (!bytes) return '未知';
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
return `${gb.toFixed(2)} GB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600, color: '#262626' }}>系统配置</h2>
|
||||
<p style={{ margin: '8px 0 0 0', color: '#8c8c8c', fontSize: '14px' }}>配置 Ollama 服务和 AI 模型参数</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
marginBottom: '16px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '16px 24px',
|
||||
background: '#fafafa',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: connectionStatus === 'success' ? '#52c41a' : connectionStatus === 'error' ? '#ff4d4f' : '#faad14'
|
||||
}} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, color: '#262626' }}>
|
||||
{connectionStatus === 'success' ? '服务正常' : connectionStatus === 'error' ? '服务异常' : '未检测'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
|
||||
{availableModels.length > 0
|
||||
? `已安装 ${availableModels.length} 个模型`
|
||||
: connectionStatus === 'success'
|
||||
? '未安装模型'
|
||||
: '请先连接服务'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card bordered={false} style={{ boxShadow: 'none' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: '',
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxTokens: 2000
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
服务配置
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>Ollama 服务地址</span>}
|
||||
name="ollamaUrl"
|
||||
rules={[{ required: true, message: '请输入Ollama服务地址' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="http://localhost:11434"
|
||||
style={{ borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ marginBottom: '16px' }}>
|
||||
<Button
|
||||
icon={<ApiOutlined />}
|
||||
onClick={handleTestConnection}
|
||||
loading={testing}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
测试连接
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleDetectModels}
|
||||
loading={detectingModels}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
检测模型
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
模型选择
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<span style={{ color: '#595959' }}>AI 模型</span>
|
||||
{availableModels.length > 0 && (
|
||||
<Tag color="success">{availableModels.length} 个可用</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
name="model"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: '请选择AI模型'
|
||||
}]}
|
||||
tooltip="只能选择本地已安装的模型"
|
||||
>
|
||||
<Select
|
||||
placeholder={availableModels.length === 0 ? "请先点击检测模型" : "选择AI模型"}
|
||||
showSearch
|
||||
allowClear
|
||||
onChange={handleModelChange}
|
||||
notFoundContent={detectingModels ? <Spin size="small" /> : "未检测到可用模型"}
|
||||
disabled={availableModels.length === 0}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
{availableModels.map(model => (
|
||||
<Option key={model.name} value={model.name}>
|
||||
<Space>
|
||||
<span>{model.name}</span>
|
||||
{model.size && (
|
||||
<span style={{ color: '#8c8c8c', fontSize: '12px' }}>
|
||||
({formatModelSize(model.size)})
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{currentModel && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
<span style={{ color: '#52c41a', fontSize: '14px' }}>
|
||||
当前使用模型: {currentModel}
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
生成参数
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>温度</span>}
|
||||
name="temperature"
|
||||
rules={[{ required: true, message: '请输入温度值' }]}
|
||||
tooltip="控制生成文本的随机性"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
precision={1}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>Top P</span>}
|
||||
name="topP"
|
||||
rules={[{ required: true, message: '请输入Top P值' }]}
|
||||
tooltip="控制生成文本的多样性"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
precision={1}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>最大 Tokens</span>}
|
||||
name="maxTokens"
|
||||
rules={[{ required: true, message: '请输入最大生成长度' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={100}
|
||||
max={8000}
|
||||
step={100}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
size="large"
|
||||
style={{ borderRadius: '6px', minWidth: '120px' }}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider style={{ margin: '32px 0' }} />
|
||||
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
使用说明
|
||||
</div>
|
||||
<ul style={{
|
||||
margin: 0,
|
||||
paddingLeft: '20px',
|
||||
color: '#595959',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.8'
|
||||
}}>
|
||||
<li>确保 Ollama 服务正在运行(默认端口 11434)</li>
|
||||
<li>点击"测试连接"验证 Ollama 服务是否正常</li>
|
||||
<li>点击"检测模型"获取本地已安装的模型列表</li>
|
||||
<li>模型选择限制为只能使用本地已安装的模型</li>
|
||||
<li>如需新模型,请使用命令:ollama pull 模型名</li>
|
||||
<li>推荐:qwen3:8b起步</li>
|
||||
<li>温度参数控制生成文本的随机性,建议值为 0.7</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemConfig;
|
||||
810
src/pages/UserManual.tsx
Normal file
810
src/pages/UserManual.tsx
Normal file
@@ -0,0 +1,810 @@
|
||||
import React from 'react';
|
||||
import { Card, Typography, Divider, Space, Tag, List, Steps, Alert, Tabs } from 'antd';
|
||||
import { BookOutlined, SettingOutlined, RobotOutlined, EditOutlined, AppstoreOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
const UserManual: React.FC = () => {
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: (
|
||||
<span>
|
||||
<RobotOutlined /> 系统概述
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<Card>
|
||||
<Title level={3}>AI 小说创作系统概述</Title>
|
||||
<Paragraph>
|
||||
AI 小说创作系统是一个基于人工智能的辅助创作平台,通过本地部署的 Ollama 模型帮助用户完成小说创作的各个环节。
|
||||
系统提供从小说创建、设定生成、章节创作到完整作品管理的全流程支持。
|
||||
</Paragraph>
|
||||
|
||||
<Title level={4}>主要功能</Title>
|
||||
<List
|
||||
dataSource={[
|
||||
'智能小说设定生成(角色、世界观、故事线)',
|
||||
'分章节内容创作与续写',
|
||||
'本地化部署,数据安全可控',
|
||||
'多项目管理,便于同时处理多个作品',
|
||||
'灵活的参数配置,适应不同创作风格'
|
||||
]}
|
||||
renderItem={(item: string) => (
|
||||
<List.Item>
|
||||
<Text>{item}</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'model',
|
||||
label: (
|
||||
<span>
|
||||
<SettingOutlined /> 模型配置指南
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<Alert
|
||||
message="重要提示"
|
||||
description="配置模型是使用本系统的第一步,必须先完成模型配置才能使用 AI 创作功能。"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Card style={{ marginBottom: '16px' }}>
|
||||
<Title level={4}>1. Ollama 服务安装</Title>
|
||||
<Paragraph>
|
||||
首先需要在本地安装 Ollama 服务,这是系统运行的基础。
|
||||
</Paragraph>
|
||||
|
||||
<Title level={5}>安装步骤:</Title>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={-1}
|
||||
items={[
|
||||
{
|
||||
title: '下载 Ollama',
|
||||
description: '访问 Ollama 官网(https://ollama.ai)下载适合您操作系统的版本'
|
||||
},
|
||||
{
|
||||
title: '安装 Ollama',
|
||||
description: '运行安装程序,按提示完成安装'
|
||||
},
|
||||
{
|
||||
title: '启动服务',
|
||||
description: '在终端中运行 ollama serve 命令启动服务'
|
||||
},
|
||||
{
|
||||
title: '验证安装',
|
||||
description: '确认服务运行在默认端口 11434'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Title level={4}>2. 模型安装与选择</Title>
|
||||
<Paragraph>
|
||||
安装完 Ollama 后,需要安装适合的 AI 模型用于小说创作。
|
||||
</Paragraph>
|
||||
|
||||
<Title level={5}>推荐模型配置:</Title>
|
||||
<Space direction="vertical" style={{ width: '100%', marginBottom: '16px' }}>
|
||||
<div style={{ padding: '12px', background: '#f6ffed', borderRadius: '6px', border: '1px solid #b7eb8f' }}>
|
||||
<div>
|
||||
<Tag color="green">推荐配置</Tag>
|
||||
<Text strong>qwen3:8b</Text>
|
||||
<Text type="secondary"> - 8B 参数,质量和速度平衡,适合大多数创作需求</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text code>ollama pull qwen3:8b</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '12px', background: '#e6f7ff', borderRadius: '6px', border: '1px solid #91d5ff' }}>
|
||||
<div>
|
||||
<Tag color="blue">进阶配置</Tag>
|
||||
<Text strong>qwen3:14b</Text>
|
||||
<Text type="secondary"> - 14B 参数,生成质量更高,需要更多内存</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text code>ollama pull qwen3:14b</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '12px', background: '#fff9e6', borderRadius: '6px', border: '1px solid #ffe58f' }}>
|
||||
<div>
|
||||
<Tag color="purple">高级配置</Tag>
|
||||
<Text strong>qwen3:32b</Text>
|
||||
<Text type="secondary"> - 32B 参数,最佳创作效果,需要较好的硬件配置</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text code>ollama pull qwen3:32b</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
<Title level={5}>其他可选模型:</Title>
|
||||
<List
|
||||
dataSource={[
|
||||
<div>
|
||||
<Text strong>llama3:8b</Text> - Meta 开源的 Llama 3 模型,英文表现优秀
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>mistral:7b</Text> - Mistral AI 开发的模型,推理能力强
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>gemma:7b</Text> - Google 开发的 Gemma 模型
|
||||
</div>
|
||||
]}
|
||||
renderItem={(item: React.ReactNode) => (
|
||||
<List.Item>
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Title level={4}>3. 系统配置参数详解</Title>
|
||||
<Paragraph>
|
||||
在"设置 → 模型"页面中,您可以配置以下关键参数来优化 AI 创作效果:
|
||||
</Paragraph>
|
||||
|
||||
<Title level={5}>服务配置参数:</Title>
|
||||
<List
|
||||
dataSource={[
|
||||
<div>
|
||||
<Text strong>Ollama 服务地址:</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">默认地址:http://localhost:11434</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px', padding: '8px', background: '#f5f5f5', borderRadius: '4px' }}>
|
||||
<Text code>http://localhost:11434</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">如果 Ollama 安装在其他机器或端口,请相应修改地址</Text>
|
||||
</div>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>AI 模型选择:</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">只能选择本地已安装的模型,需要先点击"检测模型"按钮</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px', padding: '8px', background: '#fffbe6', borderRadius: '4px', border: '1px solid #ffe58f' }}>
|
||||
<Text strong>注意:</Text>如果模型列表为空,请先用命令行安装模型
|
||||
</div>
|
||||
</div>
|
||||
]}
|
||||
renderItem={(item: React.ReactNode) => (
|
||||
<List.Item>
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Title level={5}>生成参数配置:</Title>
|
||||
<List
|
||||
dataSource={[
|
||||
<div>
|
||||
<Text strong>温度参数:</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">范围:0-2,推荐值:0.7</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">• 低值(0.3-0.5):生成内容更加确定、保守</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">• 中值(0.6-0.8):平衡创造性和连贯性(推荐)</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">• 高值(0.9-1.2):内容更加随机、创新</Text>
|
||||
</div>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>Top P 参数:</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">范围:0-1,推荐值:0.9</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">控制生成文本的多样性,值越高内容越多样</Text>
|
||||
</div>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>最大 Tokens:</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">范围:100-8000,推荐值:2000</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">控制单次生成的最大长度,1 Token ≈ 0.75 个中文字符</Text>
|
||||
</div>
|
||||
</div>
|
||||
]}
|
||||
renderItem={(item: React.ReactNode) => (
|
||||
<List.Item>
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Title level={4}>4. 配置验证完整流程</Title>
|
||||
<Alert
|
||||
message="配置验证步骤"
|
||||
description="请按顺序完成以下步骤,确保每个步骤都成功后再进行下一步"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={-1}
|
||||
items={[
|
||||
{
|
||||
title: '步骤 1:测试 Ollama 服务连接',
|
||||
description: (
|
||||
<div>
|
||||
<div>点击"测试连接"按钮验证 Ollama 服务状态</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f6ffed', borderRadius: '4px' }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: '4px' }} />
|
||||
<Text>成功提示:Ollama 服务连接成功!</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#fff1f0', borderRadius: '4px' }}>
|
||||
<Text>失败提示:请检查 ollama serve 是否正在运行</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 2:检测已安装模型',
|
||||
description: (
|
||||
<div>
|
||||
<div>点击"检测模型"按钮获取本地已安装的模型列表</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f6ffed', borderRadius: '4px' }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: '4px' }} />
|
||||
<Text>成功提示:成功检测到 N 个已安装模型</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#fffbe6', borderRadius: '4px' }}>
|
||||
<Text>警告提示:未检测到已安装的模型,请先使用 ollama pull 命令安装模型</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 3:选择 AI 模型',
|
||||
description: (
|
||||
<div>
|
||||
<div>从下拉列表中选择要使用的模型</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">• 模型列表显示:模型名称(模型大小)</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">• 选择已安装的模型,系统会显示当前使用模型信息</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 4:调整生成参数',
|
||||
description: (
|
||||
<div>
|
||||
<div>根据您的创作需求调整参数</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">• 初次使用建议保持默认参数</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">• 有经验后可以根据题材特点微调参数</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 5:保存配置',
|
||||
description: (
|
||||
<div>
|
||||
<div>点击"保存配置"按钮完成设置</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f6ffed', borderRadius: '4px' }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: '4px' }} />
|
||||
<Text>成功提示:配置保存成功!</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">保存后系统会自动刷新状态,页面底部会显示当前配置信息</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'novel',
|
||||
label: (
|
||||
<span>
|
||||
<EditOutlined /> 小说管理功能
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<Alert
|
||||
message="核心功能"
|
||||
description="小说管理是本系统的核心功能,支持从创意到成稿的完整创作流程。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Card style={{ marginBottom: '16px' }}>
|
||||
<Title level={4}>1. 创建新小说(第一步)</Title>
|
||||
<Paragraph>
|
||||
开始创作前,首先需要创建一个新的小说项目。这是整个创作流程的起点。
|
||||
</Paragraph>
|
||||
|
||||
<Title level={5}>详细步骤:</Title>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={-1}
|
||||
items={[
|
||||
{
|
||||
title: '步骤 1:进入小说管理页面',
|
||||
description: (
|
||||
<div>
|
||||
<div>点击左侧菜单的"小说管理"进入小说列表页面</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f0f9ff', borderRadius: '4px' }}>
|
||||
<Text type="secondary">📍 位置:左侧菜单 → 第一项"小说管理"</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 2:点击"新建小说"按钮',
|
||||
description: (
|
||||
<div>
|
||||
<div>在页面右上角找到蓝色的"新建小说"按钮</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f0f9ff', borderRadius: '4px' }}>
|
||||
<Text type="secondary">📍 位置:页面右上角,带有 ➕ 图标的蓝色按钮</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 3:填写小说基本信息',
|
||||
description: (
|
||||
<div>
|
||||
<div>在弹出的对话框中填写以下信息:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>书名:</Text>
|
||||
<Text type="secondary">为您的小说起一个吸引人的名字(必填)</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>题材类型:</Text>
|
||||
<Text type="secondary">从下拉列表中选择题材(必填)</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>初步想法:</Text>
|
||||
<Text type="secondary">简单描述您的创作概念,可留空让 AI 帮助完善(选填)</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 4:确认创建',
|
||||
description: (
|
||||
<div>
|
||||
<div>点击"确定"按钮完成小说创建</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f6ffed', borderRadius: '4px' }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: '4px' }} />
|
||||
<Text>成功提示:小说创建成功,请前往AI生成页面完善设定</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<Title level={5}>题材选项说明:</Title>
|
||||
<List
|
||||
grid={{ gutter: 16, column: 3 }}
|
||||
dataSource={[
|
||||
'穿越', '都市', '修仙', '武侠', '玄幻',
|
||||
'科幻', '言情', '历史', '游戏', '灵异', '军事', '悬疑', '其他'
|
||||
]}
|
||||
renderItem={(item: string) => (
|
||||
<List.Item>
|
||||
<Tag color="blue">{item}</Tag>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: '16px' }}>
|
||||
<Title level={4}>2. 完善小说设定(第二步)</Title>
|
||||
<Paragraph>
|
||||
创建小说后,下一步是完善小说的详细设定。这是创作高质量小说的基础。
|
||||
</Paragraph>
|
||||
|
||||
<Title level={5}>详细步骤:</Title>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={-1}
|
||||
items={[
|
||||
{
|
||||
title: '步骤 1:进入 AI 生成页面',
|
||||
description: (
|
||||
<div>
|
||||
<div>在小说列表中找到刚创建的小说,点击卡片进入详情页</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">或者:点击卡片上的"编辑"按钮,然后选择"完善设定"</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#fffbe6', borderRadius: '4px', border: '1px solid #ffe58f' }}>
|
||||
<Text strong>⚠️ 重要:</Text>如果未完善设定,会显示黄色警告提示
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 2:配置小说参数',
|
||||
description: (
|
||||
<div>
|
||||
<div>在 AI 生成页面配置以下参数:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>小说名称:</Text>
|
||||
<Text type="secondary">自动显示创建时的书名(不可修改)</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>题材类型:</Text>
|
||||
<Text type="secondary">自动显示创建时的题材(不可修改)</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>目标字数:</Text>
|
||||
<Text type="secondary">选择小说总字数:5万字、10万字、20万字、30万字、50万字、100万字</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>特殊要求:</Text>
|
||||
<Text type="secondary">描述您对这部小说的特殊要求或想法(选填)</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 3:AI 生成基础设定',
|
||||
description: (
|
||||
<div>
|
||||
<div>点击"AI生成设定"按钮,AI 将为您生成:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>📖 故事大纲:</Text>
|
||||
<Text type="secondary">整个故事的核心情节和主题(150字以内)</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>🏗️ 情节结构:</Text>
|
||||
<Text type="secondary">按模块划分整个故事的结构</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>👥 人物设定:</Text>
|
||||
<Text type="secondary">主要人物的详细设定,包括主角、配角、反派</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f6ffed', borderRadius: '4px' }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: '4px' }} />
|
||||
<Text>成功提示:基础设定生成成功!现在可以开始手动生成章节标题和细纲。</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 4:生成章节规划',
|
||||
description: (
|
||||
<div>
|
||||
<div>基础设定生成后,可以开始生成章节标题和细纲:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>单个生成:</Text>
|
||||
<Text type="secondary">点击"生成下一章标题+细纲"逐章生成</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>批量生成:</Text>
|
||||
<Text type="secondary">点击"批量生成5章标题+细纲"一次生成多个章节</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>单独生成:</Text>
|
||||
<Text type="secondary">直接点击章节卡片单独生成某章的标题和细纲</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f0f9ff', borderRadius: '4px' }}>
|
||||
<Text type="secondary">💡 提示:章节规划包含章节标题、细纲、预计字数等信息</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 5:编辑和确认设定',
|
||||
description: (
|
||||
<div>
|
||||
<div>生成的内容可以手动编辑:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>编辑设定:</Text>
|
||||
<Text type="secondary">点击"编辑设定"按钮修改故事大纲、情节结构、人物设定</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>编辑章节:</Text>
|
||||
<Text type="secondary">点击章节卡片的"编辑"按钮修改章节标题和细纲</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>确认保存:</Text>
|
||||
<Text type="secondary">确认无误后点击"确认并保存"按钮</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: '16px' }}>
|
||||
<Title level={4}>3. 章节内容创作(第三步)</Title>
|
||||
<Paragraph>
|
||||
完成设定后,就可以开始具体的章节内容创作了。
|
||||
</Paragraph>
|
||||
|
||||
<Title level={5}>详细步骤:</Title>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={-1}
|
||||
items={[
|
||||
{
|
||||
title: '步骤 1:进入小说详情页',
|
||||
description: (
|
||||
<div>
|
||||
<div>点击"前往创作"按钮或返回小说列表,点击小说卡片</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f0f9ff', borderRadius: '4px' }}>
|
||||
<Text type="secondary">📍 位置:小说列表 → 点击小说卡片</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 2:查看章节规划',
|
||||
description: (
|
||||
<div>
|
||||
<div>在"章节列表"中可以看到所有已规划的章节:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Tag color="green">绿色边框</Tag>
|
||||
<Text type="secondary">:已生成标题和细纲的章节</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Tag color="orange">黄色边框</Tag>
|
||||
<Text type="secondary">:待生成标题和细纲的章节</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Tag color="blue">蓝色边框</Tag>
|
||||
<Text type="secondary">:已创作内容的章节</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 3:生成章节内容',
|
||||
description: (
|
||||
<div>
|
||||
<div>选择一个已规划标题和细纲的章节,点击"AI生成"按钮:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>生成过程:</Text>
|
||||
<Text type="secondary">AI 会基于小说设定和章节规划生成具体内容</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>生成时间:</Text>
|
||||
<Text type="secondary">大约需要10-30秒,请耐心等待</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>内容质量:</Text>
|
||||
<Text type="secondary">生成的内容约900-1200字,符合章节规划要求</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', padding: '8px', background: '#f6ffed', borderRadius: '4px' }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: '4px' }} />
|
||||
<Text>成功提示:第X章生成成功</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 4:查看和编辑内容',
|
||||
description: (
|
||||
<div>
|
||||
<div>生成完成后,可以查看和编辑内容:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>查看内容:</Text>
|
||||
<Text type="secondary">点击"查看内容"按钮查看生成的章节内容</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>手动编辑:</Text>
|
||||
<Text type="secondary">可以对AI生成的内容进行手动修改和润色</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>重新生成:</Text>
|
||||
<Text type="secondary">如果不满意,可以删除后重新生成</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '步骤 5:继续创作其他章节',
|
||||
description: (
|
||||
<div>
|
||||
<div>重复步骤3-4,完成所有章节的创作:</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text strong>建议顺序:</Text>
|
||||
<Text type="secondary">按章节顺序从第1章开始创作</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>批量创作:</Text>
|
||||
<Text type="secondary">可以连续创作多个章节,保持创作连贯性</Text>
|
||||
</div>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text strong>定期保存:</Text>
|
||||
<Text type="secondary">系统会自动保存,但建议定期手动确认</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<Title level={5}>章节管理功能:</Title>
|
||||
<List
|
||||
dataSource={[
|
||||
<div>
|
||||
<Text strong>查看章节:</Text>
|
||||
<Text type="secondary">点击章节卡片的"查看内容"按钮查看完整章节内容</Text>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>编辑章节:</Text>
|
||||
<Text type="secondary">手动修改AI生成的内容,添加自己的创意和风格</Text>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>删除章节:</Text>
|
||||
<Text type="secondary">删除不满意的章节,重新生成</Text>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>下载章节:</Text>
|
||||
<Text type="secondary">将单个章节导出为 Markdown 文件</Text>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>下载整书:</Text>
|
||||
<Text type="secondary">将所有章节合并导出为完整的 Markdown 文件</Text>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>查看规划:</Text>
|
||||
<Text type="secondary">查看章节的标题、细纲、预计字数等规划信息</Text>
|
||||
</div>
|
||||
]}
|
||||
renderItem={(item: React.ReactNode) => (
|
||||
<List.Item>
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'tips',
|
||||
label: (
|
||||
<span>
|
||||
<AppstoreOutlined /> 使用技巧
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<Card>
|
||||
<Title level={3}>提高创作质量的技巧</Title>
|
||||
<List
|
||||
dataSource={[
|
||||
'充分完善设定:详细的角色和世界观设定有助于生成更连贯的内容',
|
||||
'逐步生成章节:不要一次性生成太多内容,分章节逐步完善质量更高',
|
||||
'适当手动编辑:AI生成后进行手动修改,提升整体质量和风格统一性',
|
||||
'参数调优:根据不同题材调整温度和Top P参数,找到最佳配置',
|
||||
'保持风格一致:在续写时参考前文风格,保持整本小说的风格统一',
|
||||
'定期备份:虽然系统有自动保存,但重要节点建议手动导出备份'
|
||||
]}
|
||||
renderItem={(item: string) => (
|
||||
<List.Item>
|
||||
<Text>{item}</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Title level={3}>常见问题解决</Title>
|
||||
<List
|
||||
dataSource={[
|
||||
<div>
|
||||
<Text strong>问题:生成内容不符合预期</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">解决:重新编辑章节细纲,提供更详细的指导,然后重新生成</Text>
|
||||
</div>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>问题:章节之间连贯性差</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">解决:手动编辑前一章的结尾,为下一章做铺垫,然后重新生成</Text>
|
||||
</div>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>问题:人物性格不一致</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">解决:完善人物设定,详细描述性格特点,并在章节细纲中强调</Text>
|
||||
</div>
|
||||
</div>,
|
||||
<div>
|
||||
<Text strong>问题:创作进度缓慢</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">解决:使用批量生成功能,但要注意定期检查质量</Text>
|
||||
</div>
|
||||
</div>
|
||||
]}
|
||||
renderItem={(item: React.ReactNode) => (
|
||||
<List.Item>
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '24px' }}>
|
||||
<div style={{ marginBottom: '32px', textAlign: 'center' }}>
|
||||
<Title level={2} style={{ color: '#1890ff', marginBottom: '8px' }}>
|
||||
<BookOutlined /> AI 小说创作系统使用手册
|
||||
</Title>
|
||||
<Text type="secondary">完整的系统功能使用指南</Text>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultActiveKey="overview"
|
||||
items={tabItems}
|
||||
size="large"
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Title level={3}>技术支持</Title>
|
||||
<Paragraph>
|
||||
如遇到问题或需要更多帮助,请参考:
|
||||
</Paragraph>
|
||||
<List
|
||||
dataSource={[
|
||||
'Ollama 官方文档:https://github.com/ollama/ollama',
|
||||
'Qwen 模型文档:https://huggingface.co/Qwen',
|
||||
'系统反馈:通过底部联系信息反馈问题'
|
||||
]}
|
||||
renderItem={(item: string) => (
|
||||
<List.Item>
|
||||
<Text>{item}</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<Paragraph style={{ textAlign: 'center', color: '#8c8c8c' }}>
|
||||
AI 小说创作系统 v0.0.1 | 代码老中医出品
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManual;
|
||||
Reference in New Issue
Block a user