|
|
|
@@ -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' && (
|
|
|
|
|