fix:初始化

This commit is contained in:
Your Name
2026-04-16 21:32:21 +08:00
parent 0b4772cac2
commit 235c98517d
51 changed files with 29453 additions and 0 deletions

View 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:messageim: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
View 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
View 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

File diff suppressed because it is too large Load Diff

1339
src/pages/NovelGenerate.tsx Normal file

File diff suppressed because it is too large Load Diff

414
src/pages/NovelList.tsx Normal file
View 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
View 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
View 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
View 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-20.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-10.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-80002000</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">510203050100</Text>
</div>
<div style={{ marginTop: '4px' }}>
<Text strong></Text>
<Text type="secondary"></Text>
</div>
</div>
)
},
{
title: '步骤 3AI 生成基础设定',
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;