1
0

SkillPanel.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <template>
  2. <div class="skill-panel">
  3. <div class="skill-panel-header">
  4. <div class="skill-panel-header-left">
  5. <span class="skill-panel-title">技能</span>
  6. <span class="skill-panel-desc">管理模型技能,启用后可在聊天里通过$skill-name使用</span>
  7. </div>
  8. <div class="skill-panel-close" @click="emit('close')">
  9. <div class="close-icon"></div>
  10. </div>
  11. </div>
  12. <div class="skill-filter-bar">
  13. <a-input v-model:value="searchKeyword" placeholder="搜索技能..." allow-clear size="small" class="skill-search-input" />
  14. <a-select v-model:value="filterStatus" size="small" class="skill-filter-select">
  15. <a-select-option value="all">全部</a-select-option>
  16. <a-select-option value="enabled">已启用</a-select-option>
  17. <a-select-option value="disabled">未启用</a-select-option>
  18. </a-select>
  19. </div>
  20. <div class="skill-summary">
  21. <span class="skill-summary-label">技能</span>
  22. <span class="skill-summary-count">{{ filteredSkills.length }}项</span>
  23. </div>
  24. <div class="skill-list" v-if="!loading">
  25. <div v-for="skill in filteredSkills" :key="skill.name" class="skill-item" @click="handleSkillClick(skill)">
  26. <div class="skill-header">
  27. <div class="skill-name-wrapper">
  28. <div class="skill-item-icon"></div>
  29. <span class="skill-name">{{ skill.display_name }}</span>
  30. </div>
  31. <a-switch :checked="skill.enabled" size="small" class="skill-switch" @change="handleToggleSkill(skill)" />
  32. </div>
  33. <div class="skill-desc">{{ skill.description }}</div>
  34. <div v-if="skill.agents && skill.agents.length > 0" class="skill-agents">
  35. <div class="agents-label">
  36. <div class="agents-label-icon"></div>
  37. <span>关联智能体</span>
  38. </div>
  39. <div class="agents-tags">
  40. <span v-for="agent in skill.agents" :key="agent" class="agent-tag">{{ agent }}</span>
  41. </div>
  42. </div>
  43. <div v-if="skillDetailMap[skill.name]" class="skill-detail">
  44. <div class="skill-detail-content" v-html="renderMarkdown(skillDetailMap[skill.name])"></div>
  45. </div>
  46. <div v-else-if="detailLoadingMap[skill.name]" class="skill-detail-loading">
  47. <a-spin size="small" />
  48. </div>
  49. </div>
  50. <a-empty v-if="filteredSkills.length === 0" description="暂无技能" />
  51. </div>
  52. <div class="skill-loading" v-else>
  53. <a-spin />
  54. </div>
  55. </div>
  56. </template>
  57. <script setup lang="ts">
  58. import { ref, reactive, computed, watch } from 'vue';
  59. import { getSkills, toggleSkill, getSkillDetails } from '../../api';
  60. import { renderMarkdown } from './utils';
  61. import { message } from 'ant-design-vue';
  62. interface Skill {
  63. name: string;
  64. display_name: string;
  65. path: string;
  66. description: string;
  67. enabled: boolean;
  68. agents: string[];
  69. has_skill_md: boolean;
  70. }
  71. const emit = defineEmits<{ (e: 'close'): void }>();
  72. const skills = ref<Skill[]>([]);
  73. const loading = ref(false);
  74. const skillDetailMap = reactive<Record<string, string>>({});
  75. const detailLoadingMap = reactive<Record<string, boolean>>({});
  76. const searchKeyword = ref('');
  77. const filterStatus = ref<'all' | 'enabled' | 'disabled'>('all');
  78. const filteredSkills = computed(() => {
  79. return skills.value.filter((skill) => {
  80. const keyword = searchKeyword.value.trim();
  81. const matchKeyword = !keyword || skill.display_name.includes(keyword) || skill.name.includes(keyword);
  82. const matchStatus = filterStatus.value === 'all' || (filterStatus.value === 'enabled' ? skill.enabled : !skill.enabled);
  83. return matchKeyword && matchStatus;
  84. });
  85. });
  86. const fetchSkills = async () => {
  87. loading.value = true;
  88. try {
  89. const res = await getSkills();
  90. skills.value = res?.skills || [];
  91. } catch {
  92. skills.value = [];
  93. } finally {
  94. loading.value = false;
  95. }
  96. };
  97. const handleToggleSkill = async (skill: Skill) => {
  98. try {
  99. await toggleSkill(skill.name);
  100. skill.enabled = !skill.enabled;
  101. message.success(`${skill.display_name} 已${skill.enabled ? '启用' : '禁用'}`);
  102. } catch {
  103. message.error('操作失败,请重试');
  104. }
  105. };
  106. const handleSkillClick = async (skill: Skill) => {
  107. return;
  108. if (skillDetailMap[skill.name]) {
  109. delete skillDetailMap[skill.name];
  110. return;
  111. }
  112. detailLoadingMap[skill.name] = true;
  113. try {
  114. const res = await getSkillDetails(skill.name);
  115. skillDetailMap[skill.name] = res?.content || '';
  116. } catch {
  117. message.error('获取技能详情失败');
  118. } finally {
  119. detailLoadingMap[skill.name] = false;
  120. }
  121. };
  122. fetchSkills();
  123. </script>
  124. <style scoped lang="less">
  125. .skill-panel {
  126. height: 100%;
  127. display: flex;
  128. flex-direction: column;
  129. overflow: hidden;
  130. background-image: var(--img-chat-panel-bg);
  131. background-repeat: no-repeat;
  132. background-size: 100% 100%;
  133. border-radius: 4px;
  134. }
  135. .skill-panel-header {
  136. display: flex;
  137. align-items: center;
  138. justify-content: space-between;
  139. padding: 14px 20px;
  140. border-bottom: 1px solid rgba(63, 80, 106, 0.25);
  141. flex-shrink: 0;
  142. .skill-panel-header-left {
  143. display: flex;
  144. align-items: baseline;
  145. gap: 10px;
  146. min-width: 0;
  147. flex: 1;
  148. }
  149. .skill-panel-title {
  150. font-size: 16px;
  151. font-weight: 600;
  152. color: #e8edf3;
  153. letter-spacing: 1px;
  154. flex-shrink: 0;
  155. }
  156. .skill-panel-desc {
  157. font-size: 12px;
  158. color: #6a7a8a;
  159. overflow: hidden;
  160. text-overflow: ellipsis;
  161. white-space: nowrap;
  162. }
  163. .skill-panel-close {
  164. width: 28px;
  165. height: 28px;
  166. display: flex;
  167. align-items: center;
  168. justify-content: center;
  169. cursor: pointer;
  170. border-radius: 4px;
  171. transition: background 0.2s;
  172. &:hover {
  173. background: rgba(255, 77, 79, 0.15);
  174. }
  175. .close-icon {
  176. width: 16px;
  177. height: 16px;
  178. background-image: var(--img-chat-close-icon-btn);
  179. background-repeat: no-repeat;
  180. background-size: 100% 100%;
  181. }
  182. }
  183. }
  184. .skill-filter-bar {
  185. display: flex;
  186. gap: 12px;
  187. margin: 16px 20px 0;
  188. flex-shrink: 0;
  189. .skill-search-input {
  190. flex: 1;
  191. height: 36px;
  192. background: rgba(10, 132, 255, 0.08);
  193. border: 1px solid rgba(63, 80, 106, 0.4);
  194. border-radius: 6px;
  195. color: #e0e6ed;
  196. transition: all 0.3s;
  197. &:hover,
  198. &:focus {
  199. border-color: rgba(10, 132, 255, 0.5);
  200. background: rgba(10, 132, 255, 0.12);
  201. }
  202. :deep(.zxm-input) {
  203. background: transparent;
  204. color: #e0e6ed;
  205. font-size: 13px;
  206. }
  207. }
  208. .skill-filter-select {
  209. width: 130px;
  210. height: 36px;
  211. :deep(.zxm-select-selector) {
  212. background: rgba(10, 132, 255, 0.08) !important;
  213. border: 1px solid rgba(63, 80, 106, 0.4) !important;
  214. border-radius: 6px !important;
  215. color: #e0e6ed;
  216. height: 36px;
  217. }
  218. :deep(.zxm-select-selection-item) {
  219. line-height: 34px !important;
  220. }
  221. }
  222. }
  223. .skill-summary {
  224. display: flex;
  225. align-items: center;
  226. gap: 6px;
  227. padding: 12px 20px 0;
  228. flex-shrink: 0;
  229. font-size: 13px;
  230. .skill-summary-label {
  231. color: #e8edf3;
  232. font-weight: 500;
  233. }
  234. .skill-summary-count {
  235. color: #6a7a8a;
  236. }
  237. }
  238. .skill-list {
  239. flex: 1;
  240. overflow-y: auto;
  241. padding: 12px 20px 20px;
  242. &::-webkit-scrollbar {
  243. width: 6px;
  244. }
  245. &::-webkit-scrollbar-track {
  246. background: transparent;
  247. }
  248. &::-webkit-scrollbar-thumb {
  249. background: rgba(63, 80, 106, 0.4);
  250. border-radius: 3px;
  251. &:hover {
  252. background: rgba(63, 80, 106, 0.6);
  253. }
  254. }
  255. }
  256. .skill-loading {
  257. flex: 1;
  258. display: flex;
  259. justify-content: center;
  260. align-items: center;
  261. padding: 80px 0;
  262. }
  263. .skill-item {
  264. padding: 16px 18px;
  265. margin-bottom: 8px;
  266. border-radius: 8px;
  267. border: 1px solid rgba(63, 80, 106, 0.15);
  268. background: rgba(10, 132, 255, 0.03);
  269. cursor: pointer;
  270. transition: all 0.25s ease;
  271. &:hover {
  272. background: rgba(10, 132, 255, 0.08);
  273. border-color: rgba(10, 132, 255, 0.25);
  274. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  275. }
  276. &:last-child {
  277. margin-bottom: 0;
  278. }
  279. }
  280. .skill-header {
  281. display: flex;
  282. align-items: center;
  283. justify-content: space-between;
  284. margin-bottom: 10px;
  285. }
  286. .skill-name-wrapper {
  287. display: flex;
  288. align-items: center;
  289. gap: 10px;
  290. }
  291. .skill-item-icon {
  292. width: 24px;
  293. height: 24px;
  294. flex-shrink: 0;
  295. background-image: var(--img-chat-skill-item-icon);
  296. background-repeat: no-repeat;
  297. background-size: 100% 100%;
  298. background-position: center;
  299. filter: drop-shadow(0 0 4px rgba(10, 132, 255, 0.3));
  300. }
  301. .skill-name {
  302. font-size: 15px;
  303. font-weight: 600;
  304. color: #e8edf3;
  305. letter-spacing: 0.3px;
  306. }
  307. .skill-switch {
  308. :deep(.zxm-switch) {
  309. min-width: 36px;
  310. height: 20px;
  311. background: rgba(63, 80, 106, 0.5);
  312. &::after {
  313. width: 16px;
  314. height: 16px;
  315. top: 2px;
  316. left: 2px;
  317. border-radius: 50%;
  318. background: #fff;
  319. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  320. transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  321. }
  322. &.zxm-switch-checked {
  323. background: rgba(10, 132, 255, 0.6);
  324. &::after {
  325. left: calc(100% - 18px);
  326. }
  327. }
  328. }
  329. }
  330. .skill-desc {
  331. font-size: 13px;
  332. color: #8b949e;
  333. line-height: 1.7;
  334. margin-bottom: 10px;
  335. padding-left: 34px;
  336. }
  337. .skill-agents {
  338. margin-top: 10px;
  339. margin-left: 34px;
  340. padding: 10px 14px;
  341. background: rgba(10, 132, 255, 0.04);
  342. border: 1px solid rgba(63, 80, 106, 0.18);
  343. border-radius: 6px;
  344. display: flex;
  345. align-items: center;
  346. gap: 12px;
  347. .agents-label {
  348. display: flex;
  349. align-items: center;
  350. gap: 6px;
  351. font-size: 12px;
  352. color: #6a7a8a;
  353. white-space: nowrap;
  354. flex-shrink: 0;
  355. .agents-label-icon {
  356. width: 14px;
  357. height: 14px;
  358. background-image: var(--img-chat-agent-icon);
  359. background-repeat: no-repeat;
  360. background-size: 100% 100%;
  361. background-position: center;
  362. opacity: 0.6;
  363. }
  364. }
  365. .agents-tags {
  366. display: flex;
  367. align-items: center;
  368. flex-wrap: wrap;
  369. gap: 6px;
  370. .agent-tag {
  371. display: inline-flex;
  372. align-items: center;
  373. padding: 3px 10px;
  374. font-size: 12px;
  375. color: #5cb8ff;
  376. background: rgba(10, 132, 255, 0.1);
  377. border: 1px solid rgba(10, 132, 255, 0.2);
  378. border-radius: 4px;
  379. letter-spacing: 0.3px;
  380. transition: all 0.2s;
  381. &:hover {
  382. background: rgba(10, 132, 255, 0.18);
  383. border-color: rgba(10, 132, 255, 0.35);
  384. }
  385. }
  386. }
  387. }
  388. .skill-detail {
  389. margin-top: 14px;
  390. margin-left: 34px;
  391. padding: 14px 16px;
  392. background: rgba(10, 132, 255, 0.05);
  393. border: 1px solid rgba(10, 132, 255, 0.15);
  394. border-radius: 6px;
  395. position: relative;
  396. overflow: hidden;
  397. .skill-detail-content {
  398. font-size: 13px;
  399. color: #c9d1d9;
  400. line-height: 1.8;
  401. word-break: break-word;
  402. :deep(h1),
  403. :deep(h2),
  404. :deep(h3),
  405. :deep(h4) {
  406. color: #e8edf3;
  407. margin: 12px 0 8px;
  408. font-weight: 600;
  409. }
  410. :deep(h1) {
  411. font-size: 18px;
  412. }
  413. :deep(h2) {
  414. font-size: 16px;
  415. }
  416. :deep(h3) {
  417. font-size: 14px;
  418. }
  419. :deep(p) {
  420. margin: 6px 0;
  421. }
  422. :deep(ul),
  423. :deep(ol) {
  424. padding-left: 20px;
  425. margin: 6px 0;
  426. }
  427. :deep(code) {
  428. background: rgba(10, 132, 255, 0.12);
  429. padding: 1px 5px;
  430. border-radius: 3px;
  431. font-size: 12px;
  432. color: #79c0ff;
  433. }
  434. :deep(pre) {
  435. background: rgba(0, 0, 0, 0.25);
  436. padding: 12px;
  437. border-radius: 6px;
  438. overflow-x: auto;
  439. margin: 8px 0;
  440. }
  441. :deep(pre code) {
  442. background: transparent;
  443. padding: 0;
  444. color: #c9d1d9;
  445. }
  446. :deep(blockquote) {
  447. border-left: 3px solid rgba(10, 132, 255, 0.4);
  448. padding-left: 12px;
  449. margin: 8px 0;
  450. color: #8b949e;
  451. }
  452. :deep(table) {
  453. border-collapse: collapse;
  454. margin: 8px 0;
  455. width: 100%;
  456. }
  457. :deep(th),
  458. :deep(td) {
  459. border: 1px solid rgba(63, 80, 106, 0.4);
  460. padding: 6px 10px;
  461. text-align: left;
  462. }
  463. :deep(th) {
  464. background: rgba(10, 132, 255, 0.1);
  465. font-weight: 600;
  466. }
  467. :deep(strong) {
  468. font-weight: 600;
  469. color: #e8edf3;
  470. }
  471. :deep(a) {
  472. color: #58a6ff;
  473. text-decoration: none;
  474. &:hover {
  475. text-decoration: underline;
  476. }
  477. }
  478. }
  479. }
  480. .skill-detail-loading {
  481. margin-top: 14px;
  482. margin-left: 34px;
  483. display: flex;
  484. justify-content: center;
  485. padding: 16px 0;
  486. }
  487. </style>