forked from boos/BoosAPI
feat: add desktop document skills workspace
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user