fix: align desktop chat with BoosAPI client flow
This commit is contained in:
@@ -30,7 +30,6 @@ import { useUserProfileStore } from '@/stores/userProfileStore';
|
||||
import { useResolvedDarkMode } from '@/hooks/useResolvedDarkMode';
|
||||
import { InputArea } from './InputArea';
|
||||
import { ModelSelector } from './ModelSelector';
|
||||
import { AgentWorkbenchComposer } from './AgentWorkbenchComposer';
|
||||
import { parseSearchContent } from '@/lib/searchUtils';
|
||||
import {
|
||||
CHAT_CUSTOM_HTML_TAGS,
|
||||
@@ -4141,8 +4140,16 @@ export function ChatView() {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full" style={{ padding: '0 24px' }}>
|
||||
<AgentWorkbenchComposer />
|
||||
<div
|
||||
className="flex flex-col items-center justify-center h-full"
|
||||
style={{ gap: 8, padding: '0 24px', color: token.colorTextSecondary }}
|
||||
>
|
||||
<Typography.Title level={3} style={{ margin: 0, color: token.colorText }}>
|
||||
{t('chat.welcome')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
{t('chat.emptyPrompt', '输入消息开始对话,或在下方切换 Agent / 创作模式。')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
@@ -4187,7 +4194,6 @@ export function ChatView() {
|
||||
)}
|
||||
|
||||
{/* Input Area */}
|
||||
{messages.length > 0 && (
|
||||
<div className="relative">
|
||||
{showScrollToBottom && (
|
||||
<Button
|
||||
@@ -4209,7 +4215,6 @@ export function ChatView() {
|
||||
)}
|
||||
<InputArea />
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
title={t('chat.compressionSummary')}
|
||||
open={summaryModalOpen}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Button, Tooltip, App, theme, Dropdown, Tag, Popover, Checkbox, Badge, Popconfirm } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Paperclip, Trash2, Mic, Eraser, Scissors, Globe, Brain, Atom, Plug, SlidersHorizontal, ArrowUp, Square, Check, Zap, ZapOff, Shrink, Upload, GitCompareArrows, X, BookOpen, GripHorizontal, CircleOff, SignalLow, SignalMedium, SignalHigh, Signal, Bot, MessageSquare, Shield, ShieldCheck, ShieldAlert, FolderOpen, ExternalLink } from 'lucide-react';
|
||||
import { Paperclip, Trash2, Mic, Eraser, Scissors, Globe, Brain, Atom, Plug, SlidersHorizontal, ArrowUp, Square, Check, Zap, ZapOff, Shrink, Upload, GitCompareArrows, X, BookOpen, GripHorizontal, CircleOff, SignalLow, SignalMedium, SignalHigh, Signal, MessageSquare, Shield, ShieldCheck, ShieldAlert, FolderOpen, ExternalLink, ImagePlus, Clapperboard, Music2, Mic2, UserRound, WandSparkles, Sparkles, Wrench } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConversationStore, useProviderStore, useSettingsStore, useSearchStore, useMcpStore, useMemoryStore, useKnowledgeStore } from '@/stores';
|
||||
import { useConversationStore, useProviderStore, useSettingsStore, useSearchStore, useMcpStore, useMemoryStore, useKnowledgeStore, useSkillStore } from '@/stores';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { findModelByIds, supportsReasoning, modelHasCapability } from '@/lib/modelCapabilities';
|
||||
import {
|
||||
@@ -41,6 +41,22 @@ const DOCUMENT_ATTACHMENT_MIME_BY_EXTENSION: Record<string, string> = {
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
};
|
||||
|
||||
type ComposerCreationMode = 'chat' | 'agent' | 'image' | 'video' | 'music' | 'voice' | 'digital-human' | 'action';
|
||||
|
||||
const MEDIA_ATTACHMENT_ACCEPT = 'image/*,video/*,audio/*';
|
||||
const ANY_ATTACHMENT_ACCEPT = `${MEDIA_ATTACHMENT_ACCEPT},${DOCUMENT_ATTACHMENT_ACCEPT}`;
|
||||
const JIANYING_SKILL_NAME = 'jianying-editor';
|
||||
const JIANYING_SKILL_SOURCE = 'luoluoluo22/jianying-editor-skill';
|
||||
const JIANYING_PROMPT_PREFIX = '请使用 jianying-editor Skill 自动创建剪映草稿:';
|
||||
const CREATIVE_PROMPT_PREFIX: Record<Exclude<ComposerCreationMode, 'chat' | 'agent'>, string> = {
|
||||
image: '请使用 BoosAPI 的图片生成能力创作:',
|
||||
video: '请使用 BoosAPI 的视频生成能力创作:',
|
||||
music: '请为下面内容生成配乐方案:',
|
||||
voice: '请使用配音/TTS 能力生成语音:',
|
||||
'digital-human': '请使用数字人能力生成口播视频:',
|
||||
action: '请使用动作模仿/动作迁移能力处理:',
|
||||
};
|
||||
|
||||
function getAttachmentMimeType(fileName: string, mimeType?: string): string {
|
||||
if (mimeType) return mimeType;
|
||||
const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
||||
@@ -50,11 +66,15 @@ function getAttachmentMimeType(fileName: string, mimeType?: string): string {
|
||||
function isAllowedAttachmentFile(
|
||||
fileName: string,
|
||||
mimeType: string | undefined,
|
||||
hasVision: boolean,
|
||||
_hasVision: boolean,
|
||||
documentAttachmentReadingEnabled: boolean,
|
||||
): boolean {
|
||||
const effectiveMimeType = getAttachmentMimeType(fileName, mimeType);
|
||||
if (hasVision && effectiveMimeType.startsWith('image/')) return true;
|
||||
if (
|
||||
effectiveMimeType.startsWith('image/')
|
||||
|| effectiveMimeType.startsWith('video/')
|
||||
|| effectiveMimeType.startsWith('audio/')
|
||||
) return true;
|
||||
if (!documentAttachmentReadingEnabled) return false;
|
||||
return Object.values(DOCUMENT_ATTACHMENT_MIME_BY_EXTENSION).includes(effectiveMimeType);
|
||||
}
|
||||
@@ -90,6 +110,9 @@ export function InputArea() {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [mcpPopoverOpen, setMcpPopoverOpen] = useState(false);
|
||||
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
|
||||
const [pendingMode, setPendingMode] = useState<ComposerCreationMode>('chat');
|
||||
const [skillsDropdownOpen, setSkillsDropdownOpen] = useState(false);
|
||||
const [installingJianying, setInstallingJianying] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const valueRef = useRef(value);
|
||||
@@ -143,6 +166,10 @@ export function InputArea() {
|
||||
const conversations = useConversationStore((s) => s.conversations);
|
||||
const providers = useProviderStore((s) => s.providers);
|
||||
const settings = useSettingsStore((s) => s.settings);
|
||||
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 shortcutHint = useCallback((label: string, action: ShortcutAction) => {
|
||||
if (!settings) return label;
|
||||
@@ -226,6 +253,10 @@ export function InputArea() {
|
||||
|
||||
const activeConversation = conversations.find((c) => c.id === activeConversationId);
|
||||
const currentMode = activeConversation?.mode || 'chat';
|
||||
const selectedComposerMode: ComposerCreationMode = currentMode === 'agent' ? 'agent' : pendingMode;
|
||||
const jianyingSkill = useMemo(() => (
|
||||
skills.find((item) => item.name === JIANYING_SKILL_NAME) ?? null
|
||||
), [skills]);
|
||||
|
||||
const setActivePage = useUIStore((s) => s.setActivePage);
|
||||
const setSettingsSection = useUIStore((s) => s.setSettingsSection);
|
||||
@@ -761,11 +792,63 @@ export function InputArea() {
|
||||
hasVision: modelHasCapability(currentModel, 'Vision'),
|
||||
}), [activeConversation, currentModel, providers]);
|
||||
const documentAttachmentReadingEnabled = settings.document_attachment_reading_enabled ?? false;
|
||||
const canAttachFiles = hasVision || documentAttachmentReadingEnabled;
|
||||
const fileInputAccept = [
|
||||
hasVision ? 'image/*' : null,
|
||||
documentAttachmentReadingEnabled ? DOCUMENT_ATTACHMENT_ACCEPT : null,
|
||||
].filter(Boolean).join(',');
|
||||
const fileInputAccept = documentAttachmentReadingEnabled ? ANY_ATTACHMENT_ACCEPT : MEDIA_ATTACHMENT_ACCEPT;
|
||||
|
||||
const creationModeItems = useMemo<MenuProps['items']>(() => [
|
||||
{
|
||||
key: 'chat',
|
||||
icon: <MessageSquare size={15} />,
|
||||
label: (
|
||||
<div>
|
||||
<strong>{t('common.chatMode')}</strong>
|
||||
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>普通问答与多模型对话</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'agent',
|
||||
icon: <WandSparkles size={15} />,
|
||||
label: (
|
||||
<div>
|
||||
<strong>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 4 }}>Beta</Tag></strong>
|
||||
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>拆解任务、读写文件、执行工具</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'image',
|
||||
icon: <ImagePlus size={15} />,
|
||||
label: <div><strong>图片生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>即梦图片、参考图与风格控制</div></div>,
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
icon: <Clapperboard size={15} />,
|
||||
label: <div><strong>视频生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>即梦视频、图生视频与分镜</div></div>,
|
||||
},
|
||||
{
|
||||
key: 'music',
|
||||
icon: <Music2 size={15} />,
|
||||
label: <div><strong>音乐生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>配乐、氛围与短片 BGM</div></div>,
|
||||
},
|
||||
{
|
||||
key: 'voice',
|
||||
icon: <Mic2 size={15} />,
|
||||
label: <div><strong>配音生成</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>TTS 音色、语速与试听</div></div>,
|
||||
},
|
||||
{
|
||||
key: 'digital-human',
|
||||
icon: <UserRound size={15} />,
|
||||
label: <div><strong>数字人</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>口播、形象与视频工作流</div></div>,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
icon: <Sparkles size={15} />,
|
||||
label: <div><strong>动作模仿</strong><div style={{ fontSize: 12, color: token.colorTextSecondary }}>动作迁移与主体参考</div></div>,
|
||||
},
|
||||
], [t, token.colorTextSecondary]);
|
||||
|
||||
// Current model key for excluding from multi-select (no longer used - users can select any model)
|
||||
|
||||
@@ -821,6 +904,7 @@ export function InputArea() {
|
||||
);
|
||||
|
||||
const handleModeSwitch = useCallback(async (mode: 'chat' | 'agent') => {
|
||||
setPendingMode(mode);
|
||||
if (!activeConversation) return;
|
||||
|
||||
await updateConversation(activeConversation.id, { mode });
|
||||
@@ -853,6 +937,50 @@ export function InputArea() {
|
||||
}
|
||||
}, [activeConversation, updateConversation, companionModels, companionStorageKey]);
|
||||
|
||||
const handleCreationModeSelect = useCallback(async (mode: ComposerCreationMode) => {
|
||||
if (mode === 'chat' || mode === 'agent') {
|
||||
await handleModeSwitch(mode);
|
||||
return;
|
||||
}
|
||||
setPendingMode(mode);
|
||||
if (activeConversation) {
|
||||
await updateConversation(activeConversation.id, { mode: 'chat' });
|
||||
}
|
||||
setValue((current) => {
|
||||
const prefix = CREATIVE_PROMPT_PREFIX[mode];
|
||||
const text = current.trim();
|
||||
if (!text || text.startsWith(prefix)) return current;
|
||||
return `${prefix}${text}`;
|
||||
});
|
||||
}, [activeConversation, handleModeSwitch, updateConversation]);
|
||||
|
||||
const applyJianyingSkill = useCallback(async () => {
|
||||
if (installingJianying) return;
|
||||
setInstallingJianying(true);
|
||||
try {
|
||||
if (!jianyingSkill) {
|
||||
await installSkill(JIANYING_SKILL_SOURCE);
|
||||
await loadSkills();
|
||||
messageApi.success('剪映 Skill 已安装');
|
||||
} else if (!jianyingSkill.enabled) {
|
||||
await toggleSkill(JIANYING_SKILL_NAME, true);
|
||||
await loadSkills();
|
||||
messageApi.success('剪映 Skill 已启用');
|
||||
}
|
||||
await handleModeSwitch('agent');
|
||||
setValue((current) => {
|
||||
const text = current.trim();
|
||||
if (text.includes(JIANYING_SKILL_NAME) || text.includes('剪映')) return current;
|
||||
return text ? `${JIANYING_PROMPT_PREFIX}${text}` : JIANYING_PROMPT_PREFIX;
|
||||
});
|
||||
setSkillsDropdownOpen(false);
|
||||
} catch (error) {
|
||||
messageApi.error(`剪映 Skill 集成失败:${String(error)}`);
|
||||
} finally {
|
||||
setInstallingJianying(false);
|
||||
}
|
||||
}, [handleModeSwitch, installSkill, installingJianying, jianyingSkill, loadSkills, messageApi, toggleSkill]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return;
|
||||
@@ -894,12 +1022,30 @@ export function InputArea() {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
}
|
||||
});
|
||||
if (currentMode === 'agent') {
|
||||
if (selectedComposerMode === 'agent') {
|
||||
const nextConversation = useConversationStore.getState().conversations.find(
|
||||
(conversation) => conversation.id === useConversationStore.getState().activeConversationId,
|
||||
);
|
||||
const nextConversationId = useConversationStore.getState().activeConversationId;
|
||||
if (nextConversation && nextConversation.mode !== 'agent') {
|
||||
await updateConversation(nextConversation.id, { mode: 'agent' });
|
||||
}
|
||||
if (nextConversationId) {
|
||||
try {
|
||||
await invoke('agent_update_session', { conversationId: nextConversationId });
|
||||
} catch (e) {
|
||||
console.warn('Failed to init agent session before send:', e);
|
||||
}
|
||||
}
|
||||
await sendAgentMessage(trimmed, attachments);
|
||||
} else if (companionModels.length > 0) {
|
||||
await sendMultiModelMessage(trimmed, companionModels, attachments, searchEnabled ? searchProviderId : null);
|
||||
const creativePrefix = selectedComposerMode !== 'chat' ? CREATIVE_PROMPT_PREFIX[selectedComposerMode] : '';
|
||||
const content = creativePrefix && !trimmed.startsWith(creativePrefix) ? `${creativePrefix}${trimmed}` : trimmed;
|
||||
await sendMultiModelMessage(content, companionModels, attachments, searchEnabled ? searchProviderId : null);
|
||||
} else {
|
||||
await sendMessage(trimmed, attachments, searchEnabled ? searchProviderId : null);
|
||||
const creativePrefix = selectedComposerMode !== 'chat' ? CREATIVE_PROMPT_PREFIX[selectedComposerMode] : '';
|
||||
const content = creativePrefix && !trimmed.startsWith(creativePrefix) ? `${creativePrefix}${trimmed}` : trimmed;
|
||||
await sendMessage(content, attachments, searchEnabled ? searchProviderId : null);
|
||||
}
|
||||
} catch (e) {
|
||||
setValue((current) => current || trimmed);
|
||||
@@ -918,7 +1064,7 @@ export function InputArea() {
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [value, attachedFiles, sendMessage, sendAgentMessage, sendMultiModelMessage, companionModels, activeConversationId, providers, settings, createConversation, messageApi, t, searchEnabled, searchProviderId, currentMode]);
|
||||
}, [value, attachedFiles, sendMessage, sendAgentMessage, sendMultiModelMessage, companionModels, activeConversationId, providers, settings, createConversation, messageApi, t, searchEnabled, searchProviderId, selectedComposerMode, updateConversation]);
|
||||
|
||||
const handleFillLastMessage = useCallback(() => {
|
||||
if (streaming) return;
|
||||
@@ -969,7 +1115,6 @@ export function InputArea() {
|
||||
}, []);
|
||||
|
||||
const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!canAttachFiles) return;
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
const files: File[] = [];
|
||||
@@ -993,14 +1138,12 @@ export function InputArea() {
|
||||
e.preventDefault();
|
||||
setAttachedFiles((prev) => [...prev, ...files]);
|
||||
}
|
||||
}, [canAttachFiles, documentAttachmentReadingEnabled, hasVision]);
|
||||
}, [documentAttachmentReadingEnabled, hasVision]);
|
||||
|
||||
// Drag-and-drop overlay (Tauri native)
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canAttachFiles) return;
|
||||
|
||||
let unlisten: (() => void) | undefined;
|
||||
|
||||
(async () => {
|
||||
@@ -1025,6 +1168,8 @@ export function InputArea() {
|
||||
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
||||
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
||||
bmp: 'image/bmp', ico: 'image/x-icon',
|
||||
mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm',
|
||||
mp3: 'audio/mpeg', wav: 'audio/wav', m4a: 'audio/mp4',
|
||||
pdf: 'application/pdf',
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
@@ -1061,7 +1206,7 @@ export function InputArea() {
|
||||
return () => {
|
||||
unlisten?.();
|
||||
};
|
||||
}, [canAttachFiles, documentAttachmentReadingEnabled, hasVision]);
|
||||
}, [documentAttachmentReadingEnabled, hasVision]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -1404,17 +1549,15 @@ export function InputArea() {
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
)}
|
||||
{canAttachFiles && (
|
||||
<Tooltip title={t('chat.attachFile')}>
|
||||
<Button
|
||||
aria-label={t('chat.attachFile')}
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<Paperclip size={14} />}
|
||||
onClick={handleFileSelect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('chat.attachFile')}>
|
||||
<Button
|
||||
aria-label={t('chat.attachFile')}
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<Paperclip size={14} />}
|
||||
onClick={handleFileSelect}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="topLeft"
|
||||
@@ -1599,30 +1742,78 @@ export function InputArea() {
|
||||
<div className="flex items-center gap-1">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'chat',
|
||||
icon: <MessageSquare size={14} />,
|
||||
label: t('common.chatMode'),
|
||||
},
|
||||
{
|
||||
key: 'agent',
|
||||
icon: <Bot size={14} />,
|
||||
label: <>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 2 }}>Beta</Tag></>,
|
||||
},
|
||||
],
|
||||
selectedKeys: [currentMode],
|
||||
onClick: ({ key }) => handleModeSwitch(key as 'chat' | 'agent'),
|
||||
items: creationModeItems,
|
||||
selectedKeys: [selectedComposerMode],
|
||||
onClick: ({ key }) => void handleCreationModeSelect(key as ComposerCreationMode),
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={currentMode === 'agent' ? <Bot size={14} /> : <MessageSquare size={14} />}
|
||||
icon={
|
||||
selectedComposerMode === 'agent' ? <WandSparkles size={14} />
|
||||
: selectedComposerMode === 'image' ? <ImagePlus size={14} />
|
||||
: selectedComposerMode === 'video' ? <Clapperboard size={14} />
|
||||
: selectedComposerMode === 'music' ? <Music2 size={14} />
|
||||
: selectedComposerMode === 'voice' ? <Mic2 size={14} />
|
||||
: selectedComposerMode === 'digital-human' ? <UserRound size={14} />
|
||||
: selectedComposerMode === 'action' ? <Sparkles size={14} />
|
||||
: <MessageSquare size={14} />
|
||||
}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}
|
||||
>
|
||||
{currentMode === 'agent' ? <>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 2, marginRight: 0 }}>Beta</Tag></> : t('common.chatMode')}
|
||||
{selectedComposerMode === 'agent' ? <>{t('common.agentMode')} <Tag color="blue" style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px', marginLeft: 2, marginRight: 0 }}>Beta</Tag></>
|
||||
: selectedComposerMode === 'image' ? '图片生成'
|
||||
: selectedComposerMode === 'video' ? '视频生成'
|
||||
: selectedComposerMode === 'music' ? '音乐生成'
|
||||
: selectedComposerMode === 'voice' ? '配音生成'
|
||||
: selectedComposerMode === 'digital-human' ? '数字人'
|
||||
: selectedComposerMode === 'action' ? '动作模仿'
|
||||
: t('common.chatMode')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
open={skillsDropdownOpen}
|
||||
onOpenChange={(open) => {
|
||||
setSkillsDropdownOpen(open);
|
||||
if (open) void loadSkills();
|
||||
}}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'jianying',
|
||||
icon: <Wrench size={14} />,
|
||||
label: (
|
||||
<div>
|
||||
<strong>剪映短片</strong>
|
||||
<div style={{ fontSize: 12, color: token.colorTextSecondary }}>
|
||||
{jianyingSkill?.enabled ? '已启用,可直接用 Agent 创建剪映草稿' : '安装/启用 jianying-editor Skill'}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onClick: () => void applyJianyingSkill(),
|
||||
},
|
||||
{
|
||||
key: 'skills-page',
|
||||
icon: <BookOpen size={14} />,
|
||||
label: '管理技能',
|
||||
onClick: () => setActivePage('skills'),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
loading={installingJianying}
|
||||
icon={<Wrench size={14} />}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}
|
||||
>
|
||||
使用技能
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{currentMode === 'agent' && (
|
||||
|
||||
Reference in New Issue
Block a user