forked from boos/BoosAPI
chore: sync current workspace changes
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { App, Button, Card, Input, Radio, Space, Tag, Typography, theme } from 'antd';
|
||||
import { App, Button, Card, Input, Radio, Tag, Typography, theme } from 'antd';
|
||||
import { BookOpen, FileText, Presentation, Sparkles, Wrench } from 'lucide-react';
|
||||
import { ModelSelect, parseModelValue } from '@/components/shared/ModelSelect';
|
||||
import { invoke } from '@/lib/invoke';
|
||||
import {
|
||||
findSkillByPreset,
|
||||
@@ -39,6 +40,7 @@ export function DocumentsPage() {
|
||||
const setActivePage = useUIStore((s) => s.setActivePage);
|
||||
const [selectedPresetKey, setSelectedPresetKey] = useState('gorden-ppt');
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [selectedModelValue, setSelectedModelValue] = useState<string | undefined>();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [installingKey, setInstallingKey] = useState<string | null>(null);
|
||||
|
||||
@@ -67,6 +69,33 @@ export function DocumentsPage() {
|
||||
return null;
|
||||
}, [providers]);
|
||||
|
||||
const selectedChatModel = useMemo(() => {
|
||||
const parsed = parseModelValue(selectedModelValue);
|
||||
if (!parsed) return null;
|
||||
const provider = providers.find((item) => item.enabled && item.id === parsed.providerId);
|
||||
const model = provider?.models.find((item) => item.model_id === parsed.modelId && isUsableChatModel(item));
|
||||
if (!provider || !model) return null;
|
||||
return {
|
||||
providerId: provider.id,
|
||||
modelId: model.model_id,
|
||||
providerName: provider.name,
|
||||
modelName: model.name || model.model_id,
|
||||
};
|
||||
}, [providers, selectedModelValue]);
|
||||
|
||||
const defaultChatModelValue = defaultChatModel
|
||||
? `${defaultChatModel.providerId}::${defaultChatModel.modelId}`
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultChatModelValue) return;
|
||||
if (!selectedModelValue || !selectedChatModel) {
|
||||
setSelectedModelValue(defaultChatModelValue);
|
||||
}
|
||||
}, [defaultChatModelValue, selectedChatModel, selectedModelValue]);
|
||||
|
||||
const activeChatModel = selectedChatModel ?? defaultChatModel;
|
||||
|
||||
const ensureSkillReady = useCallback(async (preset: SkillPreset) => {
|
||||
const existing = findSkillByPreset(useSkillStore.getState().skills, preset);
|
||||
if (!existing) {
|
||||
@@ -91,7 +120,7 @@ export function DocumentsPage() {
|
||||
return;
|
||||
}
|
||||
if (!selectedPreset || submitting) return;
|
||||
if (!defaultChatModel) {
|
||||
if (!activeChatModel) {
|
||||
message.warning('先在服务商管理里启用一个可对话模型');
|
||||
return;
|
||||
}
|
||||
@@ -101,13 +130,13 @@ export function DocumentsPage() {
|
||||
await ensureSkillReady(selectedPreset);
|
||||
const conversation = await createConversation(
|
||||
`${selectedPreset.title} · ${text.slice(0, 18)}`,
|
||||
defaultChatModel.modelId,
|
||||
defaultChatModel.providerId,
|
||||
activeChatModel.modelId,
|
||||
activeChatModel.providerId,
|
||||
);
|
||||
await updateConversation(conversation.id, {
|
||||
mode: 'agent',
|
||||
provider_id: defaultChatModel.providerId,
|
||||
model_id: defaultChatModel.modelId,
|
||||
provider_id: activeChatModel.providerId,
|
||||
model_id: activeChatModel.modelId,
|
||||
});
|
||||
await invoke('agent_update_session', {
|
||||
conversationId: conversation.id,
|
||||
@@ -124,7 +153,7 @@ export function DocumentsPage() {
|
||||
}
|
||||
}, [
|
||||
createConversation,
|
||||
defaultChatModel,
|
||||
activeChatModel,
|
||||
ensureSkillReady,
|
||||
message,
|
||||
prompt,
|
||||
@@ -180,10 +209,18 @@ export function DocumentsPage() {
|
||||
placeholder="例如:根据 BossAI 的产品介绍生成一份 12 页融资路演 PPT,包含市场、产品、技术架构、商业模式和路线图。"
|
||||
/>
|
||||
<div className="documents-compose-footer">
|
||||
<Space size={8} wrap>
|
||||
<div className="documents-compose-meta">
|
||||
<Tag>{selectedPreset?.name}</Tag>
|
||||
<Tag>{defaultChatModel ? `${defaultChatModel.providerName} · ${defaultChatModel.modelName}` : '未选择对话模型'}</Tag>
|
||||
</Space>
|
||||
<span className="documents-model-label">对话模型</span>
|
||||
<ModelSelect
|
||||
value={selectedModelValue}
|
||||
onChange={setSelectedModelValue}
|
||||
placeholder="选择对话模型"
|
||||
allowClear={false}
|
||||
filterModel={isUsableChatModel}
|
||||
style={{ minWidth: 280, maxWidth: 460, flex: '1 1 280px' }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Wrench size={16} />}
|
||||
|
||||
@@ -13,6 +13,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSkillStore } from '@/stores';
|
||||
import type { Skill, MarketplaceSkill } from '@/types';
|
||||
import { CopyButton } from '@/components/common/CopyButton';
|
||||
import {
|
||||
findSkillByPreset,
|
||||
getSkillPresetStatus,
|
||||
SKILL_PRESETS,
|
||||
type SkillPreset,
|
||||
} from '@/lib/skillPresets';
|
||||
|
||||
const INSTALL_TARGETS = [
|
||||
{ key: 'boosapi', label: '~/.boosapi/skills/', desc: 'BoosAPI Desktop', icon: <Sparkles size={14} /> },
|
||||
@@ -265,6 +271,90 @@ function MarketplaceCard({
|
||||
);
|
||||
}
|
||||
|
||||
function PresetSkillCard({
|
||||
preset,
|
||||
status,
|
||||
installing,
|
||||
onInstall,
|
||||
onOpenGithub,
|
||||
}: {
|
||||
preset: SkillPreset;
|
||||
status: 'enabled' | 'installed' | 'missing';
|
||||
installing: string | null;
|
||||
onInstall: (preset: SkillPreset, target: string) => void;
|
||||
onOpenGithub: (source: string) => void;
|
||||
}) {
|
||||
const categoryLabel = preset.category === 'document'
|
||||
? '文档'
|
||||
: preset.category === 'cad'
|
||||
? 'CAD'
|
||||
: preset.category === 'media'
|
||||
? '媒体'
|
||||
: '视频';
|
||||
const installed = status !== 'missing';
|
||||
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
className="skill-card-hover"
|
||||
style={{ marginBottom: 8 }}
|
||||
styles={{ body: { padding: '12px 16px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||
<Text strong className="skill-card-title">{preset.title}</Text>
|
||||
<Tag>{categoryLabel}</Tag>
|
||||
{status === 'enabled' && <Tag color="green">已启用</Tag>}
|
||||
{status === 'installed' && <Tag color="blue">已安装</Tag>}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 2 }}>
|
||||
{preset.description}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{preset.source}</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<Github size={14} />}
|
||||
onClick={() => onOpenGithub(preset.source)}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
{installed && status === 'enabled' ? (
|
||||
<Button size="small" disabled>
|
||||
已可用
|
||||
</Button>
|
||||
) : (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: INSTALL_TARGETS.map((target) => ({
|
||||
key: target.key,
|
||||
icon: target.icon,
|
||||
label: `${target.desc} (${target.label})`,
|
||||
})),
|
||||
onClick: ({ key }) => onInstall(preset, key),
|
||||
}}
|
||||
trigger={['click']}
|
||||
disabled={installing === preset.key}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type={status === 'installed' ? 'default' : 'primary'}
|
||||
loading={installing === preset.key}
|
||||
icon={<Download size={14} />}
|
||||
>
|
||||
{status === 'installed' ? '启用' : '安装'}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkillsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { token } = theme.useToken();
|
||||
@@ -331,6 +421,29 @@ export function SkillsPage() {
|
||||
}
|
||||
}, [installSkill, messageApi, t]);
|
||||
|
||||
const handleInstallPreset = useCallback(async (preset: SkillPreset, target: string) => {
|
||||
if (installing) return;
|
||||
setInstalling(preset.key);
|
||||
try {
|
||||
const existingSkill = findSkillByPreset(skills, preset);
|
||||
if (existingSkill && !existingSkill.enabled) {
|
||||
await toggleSkill(existingSkill.name, true);
|
||||
messageApi.success(`${preset.title} Skill 已启用`);
|
||||
} else if (!existingSkill) {
|
||||
const name = await installSkill(preset.source, target);
|
||||
messageApi.success(t('skills.installSuccess', { name }));
|
||||
}
|
||||
} catch (e) {
|
||||
messageApi.error(String(e));
|
||||
} finally {
|
||||
setInstalling(null);
|
||||
}
|
||||
}, [installSkill, installing, messageApi, skills, t, toggleSkill]);
|
||||
|
||||
const handleOpenPresetGithub = useCallback((source: string) => {
|
||||
openExternalUrl(`https://github.com/${source}`);
|
||||
}, []);
|
||||
|
||||
const handleToggle = useCallback((name: string, enabled: boolean) => {
|
||||
toggleSkill(name, enabled);
|
||||
}, [toggleSkill]);
|
||||
@@ -394,6 +507,13 @@ export function SkillsPage() {
|
||||
return skills.filter(s => s.source === sourceFilter);
|
||||
}, [skills, sourceFilter]);
|
||||
|
||||
const recommendedSkillPresets = useMemo(() => (
|
||||
SKILL_PRESETS.map((preset) => ({
|
||||
preset,
|
||||
status: getSkillPresetStatus(skills, preset),
|
||||
}))
|
||||
), [skills]);
|
||||
|
||||
const groupedSkills = useMemo(() => {
|
||||
const groups = new Map<string, Skill[]>();
|
||||
const ungrouped: Skill[] = [];
|
||||
@@ -505,15 +625,36 @@ export function SkillsPage() {
|
||||
<Spin />
|
||||
</div>
|
||||
) : filteredSkills.length === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={
|
||||
<div>
|
||||
<div>{t('skills.empty')}</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{t('skills.emptyDesc')}</Text>
|
||||
(sourceFilter === 'all' || sourceFilter === 'boosapi') ? (
|
||||
<div>
|
||||
<div style={{ margin: '8px 0 12px' }}>
|
||||
<Text strong>BoosAPI 内置推荐技能</Text>
|
||||
<Text type="secondary" style={{ display: 'block', fontSize: 12, marginTop: 2 }}>
|
||||
这些技能已内置到项目清单里,点击即可安装到 Desktop 并在 Agent 对话中调用。
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{recommendedSkillPresets.map(({ preset, status }) => (
|
||||
<PresetSkillCard
|
||||
key={preset.key}
|
||||
preset={preset}
|
||||
status={status}
|
||||
installing={installing}
|
||||
onInstall={handleInstallPreset}
|
||||
onOpenGithub={handleOpenPresetGithub}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={
|
||||
<div>
|
||||
<div>{t('skills.empty')}</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{t('skills.emptyDesc')}</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{groupedSkills.ungrouped.map((skill) => (
|
||||
|
||||
Reference in New Issue
Block a user