All files / src/components/Settings Skills.tsx

0% Statements 0/194
0% Branches 0/1
0% Functions 0/1
0% Lines 0/194

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224                                                                                                                                                                                                                                                                                                                                                                                                                                                               
import { useEffect, useState } from 'react';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { silentErrorHandler } from '../../lib/error-utils';
import { Wrench, Zap, FileCode, Globe, Mail, Database, Search, MessageSquare } from 'lucide-react';
 
// ZCLAW 内置系统技能
const SYSTEM_SKILLS = [
  {
    id: 'code-assistant',
    name: '代码助手',
    description: '代码编写、调试、重构和优化',
    category: '开发',
    icon: FileCode,
  },
  {
    id: 'web-search',
    name: '网络搜索',
    description: '实时搜索互联网信息',
    category: '信息',
    icon: Search,
  },
  {
    id: 'file-manager',
    name: '文件管理',
    description: '文件读写、搜索和整理',
    category: '系统',
    icon: Database,
  },
  {
    id: 'web-browsing',
    name: '网页浏览',
    description: '访问和解析网页内容',
    category: '信息',
    icon: Globe,
  },
  {
    id: 'email-handler',
    name: '邮件处理',
    description: '发送和管理电子邮件',
    category: '通讯',
    icon: Mail,
  },
  {
    id: 'chat-skill',
    name: '对话技能',
    description: '自然语言对话和问答',
    category: '交互',
    icon: MessageSquare,
  },
  {
    id: 'automation',
    name: '自动化任务',
    description: '自动化工作流程执行',
    category: '系统',
    icon: Zap,
  },
  {
    id: 'tool-executor',
    name: '工具执行器',
    description: '执行系统命令和脚本',
    category: '系统',
    icon: Wrench,
  },
];
 
export function Skills() {
  const connectionState = useConnectionStore((s) => s.connectionState);
  const quickConfig = useConfigStore((s) => s.quickConfig);
  const skillsCatalog = useConfigStore((s) => s.skillsCatalog);
  const loadSkillsCatalog = useConfigStore((s) => s.loadSkillsCatalog);
  const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
  const connected = connectionState === 'connected';
  const [extraDir, setExtraDir] = useState('');
  const [activeFilter, setActiveFilter] = useState<'all' | 'system' | 'builtin' | 'extra'>('all');
 
  useEffect(() => {
    if (connected) {
      loadSkillsCatalog().catch(silentErrorHandler('Skills'));
    }
  }, [connected]);
 
  const extraDirs = quickConfig.skillsExtraDirs || [];
 
  const handleAddDir = async () => {
    const nextDir = extraDir.trim();
    if (!nextDir) return;
    const nextDirs = Array.from(new Set([...extraDirs, nextDir]));
    await saveQuickConfig({ skillsExtraDirs: nextDirs });
    setExtraDir('');
    await loadSkillsCatalog();
  };
 
  const filteredCatalog = skillsCatalog.filter(skill => {
    if (activeFilter === 'all') return true;
    if (activeFilter === 'builtin') return skill.source === 'builtin';
    if (activeFilter === 'extra') return skill.source === 'extra';
    return true;
  });
 
  return (
    <div className="max-w-3xl">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-xl font-bold text-gray-900">技能</h1>
        <button
          onClick={() => { loadSkillsCatalog().catch(silentErrorHandler('Skills')); }}
          className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors"
        >
          刷新
        </button>
      </div>
 
      {!connected && (
        <div className="bg-gray-50/50 border border-gray-200 rounded-xl p-4 mb-6 text-center text-sm text-gray-500 shadow-sm">
          Gateway 未连接。请先连接 Gateway 再管理技能。
        </div>
      )}
 
      {/* 系统技能 */}
      <div className="mb-6">
        <h3 className="text-sm font-semibold text-gray-700 mb-3">ZCLAW 系统技能</h3>
        <div className="grid grid-cols-2 gap-3">
          {SYSTEM_SKILLS.map((skill) => {
            const Icon = skill.icon;
            return (
              <div
                key={skill.id}
                className="bg-white rounded-xl border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow"
              >
                <div className="flex items-start gap-3">
                  <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center flex-shrink-0">
                    <Icon className="w-5 h-5 text-white" />
                  </div>
                  <div className="flex-1 min-w-0">
                    <div className="flex items-center gap-2">
                      <span className="text-sm font-medium text-gray-900">{skill.name}</span>
                      <span className="text-[10px] px-1.5 py-0.5 bg-purple-50 text-purple-600 rounded">
                        {skill.category}
                      </span>
                    </div>
                    <p className="text-xs text-gray-500 mt-1">{skill.description}</p>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
 
      <div className="bg-white rounded-xl border border-gray-200 p-6 mb-6 shadow-sm">
        <h3 className="font-medium mb-2 text-gray-900">额外技能目录</h3>
        <p className="text-xs text-gray-500 mb-4">包含 SKILL.md 文件的额外目录。保存到 Gateway 配置的 skills.load.extraDirs 中。</p>
        <div className="flex gap-2">
          <input
            type="text"
            value={extraDir}
            onChange={(e) => setExtraDir(e.target.value)}
            placeholder="输入额外技能目录"
            className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 focus:outline-none"
          />
          <button
            onClick={() => { handleAddDir().catch(silentErrorHandler('Skills')); }}
            className="text-xs text-gray-500 px-4 py-2 border border-gray-200 rounded-lg hover:text-gray-700 transition-colors"
          >
            添加
          </button>
        </div>
        {extraDirs.length > 0 && (
          <div className="mt-4 space-y-2">
            {extraDirs.map((dir) => (
              <div key={dir} className="text-xs text-gray-500 bg-gray-50 border border-gray-100 rounded-lg px-3 py-2">
                {dir}
              </div>
            ))}
          </div>
        )}
      </div>
 
      {/* Gateway 技能 */}
      <div className="mb-4">
        <div className="flex items-center justify-between mb-3">
          <h3 className="text-sm font-semibold text-gray-700">Gateway 技能</h3>
          <div className="flex items-center gap-2">
            {['all', 'builtin', 'extra'].map((filter) => (
              <button
                key={filter}
                onClick={() => setActiveFilter(filter as typeof activeFilter)}
                className={`text-xs px-2 py-1 rounded-md transition-colors ${
                  activeFilter === filter
                    ? 'bg-blue-100 text-blue-700'
                    : 'text-gray-500 hover:bg-gray-100'
                }`}
              >
                {filter === 'all' ? '全部' : filter === 'builtin' ? '内置' : '额外'}
              </button>
            ))}
          </div>
        </div>
      </div>
 
      <div className="bg-white rounded-xl border border-gray-200 shadow-sm divide-y divide-gray-100">
        {filteredCatalog.length > 0 ? filteredCatalog.map((skill) => (
          <div key={skill.id} className="p-4">
            <div className="flex items-center justify-between gap-4">
              <div>
                <div className="text-sm font-medium text-gray-900">{skill.name}</div>
                <div className="text-xs text-gray-500 mt-1 break-all">{skill.path}</div>
              </div>
              <span className={`text-xs px-2 py-1 rounded-full ${skill.source === 'builtin' ? 'bg-blue-50 text-blue-600' : 'bg-gray-100 text-gray-500'}`}>
                {skill.source === 'builtin' ? '内置' : '额外'}
              </span>
            </div>
          </div>
        )) : (
          <div className="bg-gray-50 rounded-xl p-8 text-center">
            <p className="text-sm text-gray-400">暂无技能</p>
            <p className="text-xs text-gray-300 mt-1">连接 Gateway 后将自动加载技能列表</p>
          </div>
        )}
      </div>
    </div>
  );
}