Add per-agent skill workspaces and TraderView management
This commit is contained in:
765
frontend/src/components/TraderView.jsx
Normal file
765
frontend/src/components/TraderView.jsx
Normal file
@@ -0,0 +1,765 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
||||
|
||||
export default function TraderView({
|
||||
agents,
|
||||
agentProfilesByAgent,
|
||||
agentSkillsByAgent,
|
||||
workspaceFilesByAgent,
|
||||
selectedAgentId,
|
||||
selectedAgentProfile,
|
||||
selectedAgentSkills,
|
||||
skillDetailsByName,
|
||||
localSkillDraftsByKey,
|
||||
skillDetailLoadingKey,
|
||||
editableFiles,
|
||||
selectedWorkspaceFile,
|
||||
workspaceFileContent,
|
||||
workspaceDraftContent,
|
||||
isConnected,
|
||||
isAgentSkillsLoading,
|
||||
agentSkillsSavingKey,
|
||||
agentSkillsFeedback,
|
||||
isWorkspaceFileLoading,
|
||||
workspaceFileSavingKey,
|
||||
workspaceFileFeedback,
|
||||
onAgentChange,
|
||||
onCreateLocalSkill,
|
||||
onSkillDetailRequest,
|
||||
onLocalSkillDraftChange,
|
||||
onLocalSkillDelete,
|
||||
onLocalSkillSave,
|
||||
onRemoveSharedSkill,
|
||||
onSkillToggle,
|
||||
onWorkspaceFileChange,
|
||||
onWorkspaceDraftChange,
|
||||
onWorkspaceFileSave
|
||||
}) {
|
||||
const [expandedSkillKey, setExpandedSkillKey] = useState(null);
|
||||
const [newLocalSkillName, setNewLocalSkillName] = useState('');
|
||||
const [isSkillPickerOpen, setIsSkillPickerOpen] = useState(false);
|
||||
|
||||
const selectedAgent = useMemo(
|
||||
() => agents.find((agent) => agent.id === selectedAgentId) || agents[0] || null,
|
||||
[agents, selectedAgentId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedSkillKey(null);
|
||||
}, [selectedAgentId]);
|
||||
|
||||
if (!selectedAgent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = selectedAgentProfile || {};
|
||||
const modelInfo = getModelIcon(profile.model_name, profile.model_provider);
|
||||
const activeSkills = selectedAgentSkills.filter((item) => item.status === 'enabled' || item.status === 'active');
|
||||
const installedSkills = selectedAgentSkills.filter((item) => item.status !== 'available');
|
||||
const availableSkills = selectedAgentSkills.filter((item) => item.status === 'available');
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
padding: '18px',
|
||||
background: 'linear-gradient(180deg, #ffffff 0%, #f4f7fb 100%)',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto minmax(0, 1fr)',
|
||||
gap: 18
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, letterSpacing: '0.5px', color: '#111111' }}>
|
||||
交易员档案
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
聚焦查看每个 Agent 的模型、工具组、技能编排和工作区记忆,不展示交易表现数据
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '120px minmax(0, 1fr)',
|
||||
gap: 16,
|
||||
alignItems: 'stretch',
|
||||
minHeight: 0
|
||||
}}>
|
||||
<div style={{
|
||||
border: '1px solid #D9E0E7',
|
||||
borderRadius: 14,
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 10px 24px rgba(15, 23, 42, 0.06)',
|
||||
padding: 12,
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
alignContent: 'start'
|
||||
}}>
|
||||
{agents.map((agent) => {
|
||||
const isSelected = agent.id === selectedAgentId;
|
||||
return (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => onAgentChange(agent.id)}
|
||||
title={agent.name}
|
||||
style={{
|
||||
border: isSelected ? `2px solid ${agent.colors.accent}` : '1px solid #D9E0E7',
|
||||
borderRadius: 16,
|
||||
background: isSelected ? `${agent.colors.accent}10` : '#FFFFFF',
|
||||
boxShadow: isSelected ? `0 10px 20px ${agent.colors.accent}18` : 'none',
|
||||
padding: 8,
|
||||
display: 'grid',
|
||||
gap: 6,
|
||||
justifyItems: 'center',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={agent.avatar}
|
||||
alt={agent.name}
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
objectFit: 'cover',
|
||||
border: `1px solid ${agent.colors.accent}33`
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
color: isSelected ? agent.colors.accent : '#374151',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.4
|
||||
}}>
|
||||
{agent.name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #D9E0E7',
|
||||
borderRadius: 14,
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 10px 24px rgba(15, 23, 42, 0.06)',
|
||||
padding: 18,
|
||||
display: 'grid',
|
||||
gap: 16,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
alignContent: 'start'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<img
|
||||
src={selectedAgent.avatar}
|
||||
alt={selectedAgent.name}
|
||||
style={{
|
||||
width: 58,
|
||||
height: 58,
|
||||
borderRadius: 12,
|
||||
objectFit: 'cover',
|
||||
border: `1px solid ${selectedAgent.colors.accent}33`
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 800, color: '#111111' }}>{selectedAgent.name}</div>
|
||||
<div style={{ fontSize: 12, color: '#6B7280' }}>{selectedAgent.role}</div>
|
||||
<div style={{ fontSize: 11, color: selectedAgent.colors.accent, fontWeight: 700 }}>
|
||||
当前档案已展开
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: `1px solid ${modelInfo.color}2e`,
|
||||
background: modelInfo.bgColor,
|
||||
borderRadius: 12,
|
||||
padding: '10px 12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10
|
||||
}}>
|
||||
{modelInfo.logoPath && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
style={{ width: 26, height: 26, borderRadius: 999 }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'grid', gap: 2 }}>
|
||||
<div style={{ fontSize: 11, color: '#4B5563', fontWeight: 700 }}>模型</div>
|
||||
<div style={{ fontSize: 12, color: '#111111', fontWeight: 800 }}>
|
||||
{getShortModelName(profile.model_name)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(300px, 420px) minmax(0, 1fr)',
|
||||
gap: 16,
|
||||
alignItems: 'start'
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }}>
|
||||
<div style={{ display: 'grid', gap: 2 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>技能</div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>
|
||||
已启用: {activeSkills.length} / 已安装: {installedSkills.length}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSkillPickerOpen(true)}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: '#EFF6FF',
|
||||
color: '#1565C0',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
aria-label="管理技能"
|
||||
>
|
||||
⚙ 技能管理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5E7EB',
|
||||
background: '#F8FAFC',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
maxHeight: 520,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{isAgentSkillsLoading ? (
|
||||
<div style={{ fontSize: 11, color: '#666666', fontFamily: '"Courier New", monospace' }}>加载技能中...</div>
|
||||
) : installedSkills.length === 0 ? (
|
||||
<div style={{ fontSize: 11, color: '#666666', fontFamily: '"Courier New", monospace' }}>暂无技能</div>
|
||||
) : installedSkills.map((skill) => {
|
||||
const isEnabled = skill.status === 'enabled' || skill.status === 'active';
|
||||
const saving = agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:content` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:delete` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:remove`;
|
||||
const isExpanded = expandedSkillKey === skill.skill_name;
|
||||
const detailKey = `${selectedAgentId}:${skill.skill_name}`;
|
||||
const skillDetail = skillDetailsByName?.[detailKey] || null;
|
||||
const skillDraft = localSkillDraftsByKey?.[detailKey] ?? '';
|
||||
const isDetailLoading = skillDetailLoadingKey === detailKey;
|
||||
const isLocalSkill = skill.source === 'local';
|
||||
return (
|
||||
<div
|
||||
key={skill.skill_name}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: 7,
|
||||
paddingBottom: 10,
|
||||
borderBottom: '1px dashed #D7DEE7'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'flex-start' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isExpanded && !skillDetail && onSkillDetailRequest) {
|
||||
onSkillDetailRequest(skill.skill_name);
|
||||
}
|
||||
setExpandedSkillKey((prev) => (prev === skill.skill_name ? null : skill.skill_name));
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
padding: 0,
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
display: 'grid',
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, color: '#6B7280', fontWeight: 700 }}>
|
||||
{isExpanded ? '▾' : '▸'}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>
|
||||
{skill.name || '未命名技能'}
|
||||
</span>
|
||||
<span style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${isLocalSkill ? selectedAgent.colors.accent : '#D0D7DE'}`,
|
||||
color: isLocalSkill ? selectedAgent.colors.accent : '#6B7280',
|
||||
fontSize: 9,
|
||||
fontWeight: 700
|
||||
}}>
|
||||
{isLocalSkill ? '本地' : '共享'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#4B5563', marginLeft: 20 }}>
|
||||
{skill.description || '-'}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', marginLeft: 20 }}>
|
||||
{isExpanded ? '点击收起详情' : '点击展开详情'}
|
||||
</div>
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSkillToggle(skill.skill_name, !isEnabled)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${isEnabled ? '#C62828' : '#1565C0'}`,
|
||||
background: isConnected && !saving ? (isEnabled ? '#FFF5F5' : '#EFF6FF') : '#E5E7EB',
|
||||
color: isEnabled ? '#C62828' : '#1565C0',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : isEnabled ? '禁用' : '启用'}
|
||||
</button>
|
||||
{isLocalSkill ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onLocalSkillDelete(skill.skill_name)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #C62828',
|
||||
background: isConnected && !saving ? '#FFF5F5' : '#E5E7EB',
|
||||
color: '#C62828',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : '删除'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveSharedSkill(skill.skill_name)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #C62828',
|
||||
background: isConnected && !saving ? '#FFF5F5' : '#E5E7EB',
|
||||
color: '#C62828',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : '移除'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div style={{
|
||||
marginLeft: 20,
|
||||
borderRadius: 8,
|
||||
border: '1px solid #E5E7EB',
|
||||
background: '#FFFFFF',
|
||||
padding: '10px 12px',
|
||||
display: 'grid',
|
||||
gap: 8
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 11,
|
||||
color: '#1F2937',
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{isDetailLoading
|
||||
? '加载技能说明中...'
|
||||
: (skillDetail?.content || '暂无更详细的技能说明')}
|
||||
</div>
|
||||
{isLocalSkill && !isDetailLoading && (
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', fontWeight: 700 }}>
|
||||
本地技能 SKILL.md
|
||||
</div>
|
||||
<textarea
|
||||
value={skillDraft}
|
||||
onChange={(e) => onLocalSkillDraftChange(skill.skill_name, e.target.value)}
|
||||
style={{
|
||||
minHeight: 220,
|
||||
resize: 'vertical',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
padding: '10px 12px',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.6,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onLocalSkillSave(skill.skill_name)}
|
||||
disabled={!isConnected || saving || skillDraft === (skillDetail?.content || '')}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !saving && skillDraft !== (skillDetail?.content || '') ? '#0D47A1' : '#94A3B8',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving && skillDraft !== (skillDetail?.content || '') ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{saving ? '保存中' : '保存本地技能'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{agentSkillsFeedback && (
|
||||
<span style={{
|
||||
color: agentSkillsFeedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: 11,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{agentSkillsFeedback.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>工作区文件编辑</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
直接调整该交易员的人设、协作方式和长期记忆文件
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{editableFiles.map((filename) => {
|
||||
const isActive = filename === selectedWorkspaceFile;
|
||||
return (
|
||||
<button
|
||||
key={filename}
|
||||
onClick={() => onWorkspaceFileChange(filename)}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${isActive ? selectedAgent.colors.accent : '#D0D7DE'}`,
|
||||
background: isActive ? `${selectedAgent.colors.accent}12` : '#FFFFFF',
|
||||
color: isActive ? selectedAgent.colors.accent : '#4B5563',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
>
|
||||
{filename}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={workspaceDraftContent}
|
||||
onChange={(e) => onWorkspaceDraftChange(e.target.value)}
|
||||
placeholder={isWorkspaceFileLoading ? '加载中...' : '输入 markdown 内容'}
|
||||
style={{
|
||||
minHeight: 280,
|
||||
resize: 'vertical',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
padding: '12px 14px',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>
|
||||
当前文件: {selectedWorkspaceFile}
|
||||
</span>
|
||||
<button
|
||||
onClick={onWorkspaceFileSave}
|
||||
disabled={!isConnected || isWorkspaceFileLoading || workspaceFileSavingKey !== null || workspaceDraftContent === workspaceFileContent}
|
||||
style={{
|
||||
padding: '9px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? '#0D47A1' : '#94A3B8',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{workspaceFileSavingKey ? '保存中' : '保存文件'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{workspaceFileFeedback && (
|
||||
<span style={{
|
||||
color: workspaceFileFeedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: 11,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{workspaceFileFeedback.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSkillPickerOpen && createPortal((
|
||||
<div
|
||||
onClick={() => setIsSkillPickerOpen(false)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(15, 23, 42, 0.28)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
zIndex: 9998
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: 'min(760px, 92vw)',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
borderRadius: 16,
|
||||
border: '1px solid #D9E0E7',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 24px 60px rgba(15, 23, 42, 0.18)',
|
||||
padding: 18,
|
||||
paddingTop: 22,
|
||||
display: 'grid',
|
||||
gap: 16,
|
||||
position: 'relative',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSkillPickerOpen(false)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
borderRadius: 999,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
lineHeight: 1,
|
||||
color: '#111111',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 12px rgba(15, 23, 42, 0.08)'
|
||||
}}
|
||||
aria-label="关闭技能管理"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', paddingRight: 56 }}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 800, color: '#111111' }}>技能管理</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
为 {selectedAgent.name} 添加共享技能,或创建本地技能
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>创建本地技能</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<input
|
||||
value={newLocalSkillName}
|
||||
onChange={(e) => setNewLocalSkillName(e.target.value)}
|
||||
placeholder="输入技能名,例如 event_playbook"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: 11,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (onCreateLocalSkill) {
|
||||
onCreateLocalSkill(newLocalSkillName);
|
||||
setNewLocalSkillName('');
|
||||
}
|
||||
}}
|
||||
disabled={!isConnected || !newLocalSkillName.trim()}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && newLocalSkillName.trim() ? '#EFF6FF' : '#E5E7EB',
|
||||
color: '#1565C0',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && newLocalSkillName.trim() ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>添加共享技能</div>
|
||||
<div style={{
|
||||
border: '1px solid #E5E7EB',
|
||||
background: '#FFFFFF',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
maxHeight: 360,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{availableSkills.length === 0 ? (
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>没有可添加的共享技能</div>
|
||||
) : availableSkills.map((skill) => {
|
||||
const saving = agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}`;
|
||||
return (
|
||||
<div
|
||||
key={skill.skill_name}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
alignItems: 'flex-start',
|
||||
paddingBottom: 10,
|
||||
borderBottom: '1px dashed #D7DEE7'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>
|
||||
{skill.name || skill.skill_name}
|
||||
</span>
|
||||
<span style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid #D0D7DE',
|
||||
color: '#6B7280',
|
||||
fontSize: 9,
|
||||
fontWeight: 700
|
||||
}}>
|
||||
共享
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#4B5563' }}>
|
||||
{skill.description || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSkillToggle(skill.skill_name, true)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !saving ? '#EFF6FF' : '#E5E7EB',
|
||||
color: '#1565C0',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : '添加'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user