feat: add desktop document skills workspace

This commit is contained in:
boos
2026-06-29 14:35:34 +08:00
parent 2b181344b2
commit 258f4c1a00
11 changed files with 537 additions and 96 deletions
+200
View File
@@ -0,0 +1,200 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { App, Button, Card, Input, Radio, Space, Tag, Typography, theme } from 'antd';
import { BookOpen, FileText, Presentation, Sparkles, Wrench } from 'lucide-react';
import { invoke } from '@/lib/invoke';
import {
findSkillByPreset,
getSkillPresetStatus,
SKILL_PRESETS,
type SkillPreset,
} from '@/lib/skillPresets';
import { useConversationStore, useProviderStore, useSkillStore } from '@/stores';
import { useUIStore } from '@/stores/uiStore';
import type { Model } from '@/types';
const { Text, Title } = Typography;
function isUsableChatModel(model: Model): boolean {
return model.enabled && (model.model_type === 'Chat' || model.capabilities.includes('TextChat'));
}
function statusLabel(status: 'enabled' | 'installed' | 'missing'): string {
if (status === 'enabled') return '已启用';
if (status === 'installed') return '已安装';
return '待安装';
}
export function DocumentsPage() {
const { token } = theme.useToken();
const { message } = App.useApp();
const providers = useProviderStore((s) => s.providers);
const fetchProviders = useProviderStore((s) => s.fetchProviders);
const skills = useSkillStore((s) => s.skills);
const loadSkills = useSkillStore((s) => s.loadSkills);
const installSkill = useSkillStore((s) => s.installSkill);
const toggleSkill = useSkillStore((s) => s.toggleSkill);
const createConversation = useConversationStore((s) => s.createConversation);
const updateConversation = useConversationStore((s) => s.updateConversation);
const sendAgentMessage = useConversationStore((s) => s.sendAgentMessage);
const setActivePage = useUIStore((s) => s.setActivePage);
const [selectedPresetKey, setSelectedPresetKey] = useState('gorden-ppt');
const [prompt, setPrompt] = useState('');
const [submitting, setSubmitting] = useState(false);
const [installingKey, setInstallingKey] = useState<string | null>(null);
useEffect(() => {
if (providers.length === 0) void fetchProviders().catch((error) => message.error(String(error)));
}, [fetchProviders, message, providers.length]);
useEffect(() => {
void loadSkills();
}, [loadSkills]);
const documentPresets = useMemo(() => (
SKILL_PRESETS.filter((preset) => preset.category === 'document')
), []);
const selectedPreset = useMemo(() => (
documentPresets.find((preset) => preset.key === selectedPresetKey) ?? documentPresets[0]
), [documentPresets, selectedPresetKey]);
const defaultChatModel = useMemo(() => {
for (const provider of providers) {
if (!provider.enabled) continue;
const model = provider.models.find(isUsableChatModel);
if (model) return { providerId: provider.id, modelId: model.model_id, providerName: provider.name, modelName: model.name || model.model_id };
}
return null;
}, [providers]);
const ensureSkillReady = useCallback(async (preset: SkillPreset) => {
const existing = findSkillByPreset(useSkillStore.getState().skills, preset);
if (!existing) {
setInstallingKey(preset.key);
await installSkill(preset.source);
await loadSkills();
message.success(`${preset.title} Skill 已安装`);
return;
}
if (!existing.enabled) {
setInstallingKey(preset.key);
await toggleSkill(preset.name, true);
await loadSkills();
message.success(`${preset.title} Skill 已启用`);
}
}, [installSkill, loadSkills, message, toggleSkill]);
const handleSubmit = useCallback(async () => {
const text = prompt.trim();
if (!text) {
message.warning('先输入要生成的文档主题、资料或大纲');
return;
}
if (!selectedPreset || submitting) return;
if (!defaultChatModel) {
message.warning('先在服务商管理里启用一个可对话模型');
return;
}
setSubmitting(true);
try {
await ensureSkillReady(selectedPreset);
const conversation = await createConversation(
`${selectedPreset.title} · ${text.slice(0, 18)}`,
defaultChatModel.modelId,
defaultChatModel.providerId,
);
await updateConversation(conversation.id, {
mode: 'agent',
provider_id: defaultChatModel.providerId,
model_id: defaultChatModel.modelId,
});
await invoke('agent_update_session', {
conversationId: conversation.id,
permissionMode: 'accept_edits',
});
await sendAgentMessage(`${selectedPreset.promptPrefix}${text}`);
setPrompt('');
setActivePage('chat');
} catch (error) {
message.error(`生成文档任务启动失败:${String(error)}`);
} finally {
setSubmitting(false);
setInstallingKey(null);
}
}, [
createConversation,
defaultChatModel,
ensureSkillReady,
message,
prompt,
selectedPreset,
sendAgentMessage,
setActivePage,
submitting,
updateConversation,
]);
return (
<div className="documents-page" style={{ backgroundColor: token.colorBgElevated }}>
<section className="documents-hero">
<div>
<Tag color="green" icon={<Sparkles size={12} />}>BoosAPI Skills</Tag>
<Title level={2}>生成文档</Title>
<Text type="secondary">
用 Gorden PPT、Knowledge Cat PPT 等 Skill 把主题、资料和大纲转成可继续编辑的 PPT / 文档任务。
</Text>
</div>
<Button icon={<BookOpen size={16} />} onClick={() => setActivePage('skills')}>
管理技能
</Button>
</section>
<div className="documents-layout">
<Card className="documents-card" title={<span><Presentation size={16} /> 文档类型</span>}>
<Radio.Group
value={selectedPresetKey}
onChange={(event) => setSelectedPresetKey(event.target.value)}
className="documents-preset-list"
>
{documentPresets.map((preset) => {
const status = getSkillPresetStatus(skills, preset);
return (
<Radio.Button key={preset.key} value={preset.key} className="documents-preset">
<strong>{preset.title}</strong>
<small>{preset.description}</small>
<Tag color={status === 'enabled' ? 'green' : status === 'installed' ? 'blue' : 'default'}>
{statusLabel(status)}
</Tag>
</Radio.Button>
);
})}
</Radio.Group>
</Card>
<Card className="documents-card documents-compose-card" title={<span><FileText size={16} /> 生成内容</span>}>
<Input.TextArea
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
autoSize={{ minRows: 8, maxRows: 14 }}
placeholder="例如:根据 BossAI 的产品介绍生成一份 12 页融资路演 PPT,包含市场、产品、技术架构、商业模式和路线图。"
/>
<div className="documents-compose-footer">
<Space size={8} wrap>
<Tag>{selectedPreset?.name}</Tag>
<Tag>{defaultChatModel ? `${defaultChatModel.providerName} · ${defaultChatModel.modelName}` : '未选择对话模型'}</Tag>
</Space>
<Button
type="primary"
icon={<Wrench size={16} />}
loading={submitting || Boolean(installingKey)}
onClick={() => void handleSubmit()}
>
生成文档
</Button>
</div>
</Card>
</div>
</div>
);
}