ResourceCapsule.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <template>
  2. <div ref="wrapRef" class="resource-capsule">
  3. <!-- 胶囊按钮 -->
  4. <button type="button" :class="['capsule-btn', { open: isOpen }]" title="会话资源" :aria-expanded="isOpen" @click="isOpen = !isOpen">
  5. <div class="capsule-icon"></div>
  6. <span class="capsule-text"> </span>
  7. <span v-if="totalCount > 0" class="capsule-count">{{ totalCount }}</span>
  8. <div class="capsule-arrow"></div>
  9. </button>
  10. <!-- 聚合下拉面板 -->
  11. <Transition name="capsule-fade">
  12. <div v-if="isOpen" class="capsule-popup" :style="{ width: `${popupWidth}px` }" @click.stop>
  13. <div class="popup-header">
  14. <div class="popup-title">
  15. <div class="popup-title-icon"></div>
  16. <span>会话资源</span>
  17. </div>
  18. <CloseOutlined class="close-icon" @click="isOpen = false" />
  19. </div>
  20. <div class="popup-body">
  21. <template v-if="files.length || links.length || agents.length">
  22. <div v-if="files.length" class="popup-section">
  23. <div
  24. :class="['section-title', { collapsed: collapsedFiles }]"
  25. role="button"
  26. tabindex="0"
  27. :aria-expanded="!collapsedFiles"
  28. @click="collapsedFiles = !collapsedFiles"
  29. @keydown.enter.prevent="collapsedFiles = !collapsedFiles"
  30. @keydown.space.prevent="collapsedFiles = !collapsedFiles"
  31. >
  32. <span class="section-title-text">文件</span>
  33. <span class="section-count">{{ files.length }}</span>
  34. <div class="section-arrow"></div>
  35. </div>
  36. <Transition name="collapse">
  37. <div v-show="!collapsedFiles" class="section-body">
  38. <div
  39. v-for="item in files"
  40. :key="fileKey(item)"
  41. class="popup-item file-item"
  42. :title="`预览 ${item.name}`"
  43. @click="handleFileClick(item)"
  44. >
  45. <div v-if="previewingKey === fileKey(item)" class="row-loading"></div>
  46. <div v-else class="item-icon" :style="{ backgroundImage: getFileIconVar(item.name) }"></div>
  47. <div class="item-info">
  48. <div class="item-name" :title="item.name">{{ item.name }}</div>
  49. <div class="item-meta">
  50. <span class="item-tag" :class="item.kind">{{ item.kind === 'upload' ? '上传' : 'AI生成' }}</span>
  51. <span v-if="item.size != null">{{ formatFileSize(item.size) }}</span>
  52. </div>
  53. </div>
  54. <button class="download-btn" :title="`下载 ${item.name}`" @click.stop="downloadFile(item)">
  55. <div class="download-btn-icon"></div>
  56. </button>
  57. </div>
  58. </div>
  59. </Transition>
  60. </div>
  61. <div v-if="links.length" class="popup-section">
  62. <div
  63. :class="['section-title', { collapsed: collapsedLinks }]"
  64. role="button"
  65. tabindex="0"
  66. :aria-expanded="!collapsedLinks"
  67. @click="collapsedLinks = !collapsedLinks"
  68. @keydown.enter.prevent="collapsedLinks = !collapsedLinks"
  69. @keydown.space.prevent="collapsedLinks = !collapsedLinks"
  70. >
  71. <span class="section-title-text">链接</span>
  72. <span class="section-count">{{ links.length }}</span>
  73. <div class="section-arrow"></div>
  74. </div>
  75. <Transition name="collapse">
  76. <div v-show="!collapsedLinks" class="section-body">
  77. <div v-for="link in links" :key="link.href" class="popup-item link-item" @click="openLink(link)">
  78. <div class="link-glyph"></div>
  79. <div class="item-info">
  80. <div class="item-name" :title="link.title">{{ link.title }}</div>
  81. <div class="item-meta link-href" :title="link.href">{{ link.href }}</div>
  82. </div>
  83. <div class="link-arrow"></div>
  84. </div>
  85. </div>
  86. </Transition>
  87. </div>
  88. <div v-if="agents.length" class="popup-section">
  89. <div
  90. :class="['section-title', { collapsed: collapsedAgents }]"
  91. role="button"
  92. tabindex="0"
  93. :aria-expanded="!collapsedAgents"
  94. @click="collapsedAgents = !collapsedAgents"
  95. @keydown.enter.prevent="collapsedAgents = !collapsedAgents"
  96. @keydown.space.prevent="collapsedAgents = !collapsedAgents"
  97. >
  98. <span class="section-title-text">智能体</span>
  99. <span class="section-count">{{ agents.length }}</span>
  100. <div class="section-arrow"></div>
  101. </div>
  102. <Transition name="collapse">
  103. <div v-show="!collapsedAgents" class="section-body">
  104. <div v-for="item in agents" :key="agentKey(item)" class="popup-item agent-item" @click="handleAgentClick(item)">
  105. <div class="agent-icon"></div>
  106. <div class="item-info">
  107. <div class="item-name" :title="item.agent">{{ item.agent }}</div>
  108. <div class="item-meta">
  109. <span class="agent-status" :class="item.status">
  110. <span class="status-dot"></span>
  111. {{ agentStatusText(item.status) }}
  112. </span>
  113. <span>第{{ item.round }}轮</span>
  114. <span v-if="item.toolCount > 0">{{ item.toolCount }} 次工具调用</span>
  115. <span v-if="item.hiddenAnswer" class="item-tag folded">折叠回答</span>
  116. </div>
  117. </div>
  118. <button class="locate-btn" :title="`定位到第 ${item.round} 轮消息`" @click.stop="handleLocateClick(item)">
  119. <div class="locate-btn-icon"></div>
  120. </button>
  121. <div class="link-arrow"></div>
  122. </div>
  123. </div>
  124. </Transition>
  125. </div>
  126. </template>
  127. <div v-else class="popup-empty">
  128. <div class="popup-empty-title">暂无内容</div>
  129. <div class="popup-empty-desc">AI 生成的文件、可跳转链接与智能体调用记录会汇总到这里</div>
  130. </div>
  131. </div>
  132. </div>
  133. </Transition>
  134. </div>
  135. </template>
  136. <script setup lang="ts">
  137. import { ref, computed, watch, nextTick } from 'vue';
  138. import { CloseOutlined } from '@ant-design/icons-vue';
  139. import { onClickOutside, onKeyStroke } from '@vueuse/core';
  140. import { message } from 'ant-design-vue';
  141. import { formatFileSize, getFileIconVar, fetchPreviewFile, panelOpenableUrl, webTabTitle } from '/@/views/ventAI/manageAssistent/lib/index';
  142. import type { AttachedFile, CapsuleFileItem, CapsuleLinkItem, CapsuleAgentItem } from '/@/views/ventAI/manageAssistent/types/index';
  143. const props = defineProps<{
  144. files: CapsuleFileItem[];
  145. links: CapsuleLinkItem[];
  146. agents: CapsuleAgentItem[];
  147. // 当前任务 id:切换任务时收起弹层,避免用户误操作上一个任务的数据
  148. taskId?: string;
  149. }>();
  150. const emit = defineEmits<{
  151. (e: 'file-preview', file: AttachedFile): void;
  152. (e: 'download-attachment', file: AttachedFile): void;
  153. (e: 'agent-detail-click', data: { messageIndex: number; agent: string; agentId?: string }): void;
  154. (e: 'locate-message', messageIndex: number): void;
  155. (e: 'open-web', payload: { url: string; title?: string }): void;
  156. }>();
  157. const wrapRef = ref<HTMLElement>();
  158. const isOpen = ref(false);
  159. // 分区默认折叠,点击标题后才展开
  160. const collapsedFiles = ref(true);
  161. const collapsedLinks = ref(true);
  162. const collapsedAgents = ref(true);
  163. // 正在拉取预览内容的文件行(显示加载态、防重复点击)
  164. const previewingKey = ref<string | null>(null);
  165. const totalCount = computed(() => props.files.length + props.links.length + props.agents.length);
  166. onClickOutside(wrapRef, () => {
  167. isOpen.value = false;
  168. });
  169. // Esc 收起弹层(键盘可达性)
  170. onKeyStroke('Escape', () => {
  171. isOpen.value = false;
  172. });
  173. watch(
  174. () => props.taskId,
  175. () => {
  176. isOpen.value = false;
  177. // 切换任务后分区回到默认折叠状态
  178. collapsedFiles.value = true;
  179. collapsedLinks.value = true;
  180. collapsedAgents.value = true;
  181. }
  182. );
  183. // 弹层宽度自适应:窄窗口/拖拽缩放后避免超出聊天面板左边界被裁切。
  184. // 容器带 scale 缩放,getBoundingClientRect 是缩放后的视口坐标,需换算回布局坐标
  185. const popupWidth = ref(300);
  186. const fitPopupWidth = () => {
  187. const btn = wrapRef.value;
  188. const panel = btn?.closest('.chat-panel') as HTMLElement | null;
  189. if (!btn || !panel) return;
  190. const btnRect = btn.getBoundingClientRect();
  191. const panelRect = panel.getBoundingClientRect();
  192. const scale = panel.offsetHeight ? panelRect.height / panel.offsetHeight : 1;
  193. const available = (btnRect.right - panelRect.left - 8) / scale;
  194. popupWidth.value = Math.max(200, Math.min(300, available));
  195. };
  196. watch(isOpen, (open) => {
  197. if (open) nextTick(fitPopupWidth);
  198. });
  199. // 稳定列表 key:文件按来源唯一标识(上传附件 id / 下载地址),智能体按消息×身份标识
  200. const fileKey = (item: CapsuleFileItem): string =>
  201. item.kind === 'upload' ? `upload-${item.file?.id || item.name}` : `download-${item.url || item.name}`;
  202. const agentKey = (item: CapsuleAgentItem): string => `agent-${item.messageIndex}-${item.agentId || item.agent}`;
  203. // 下载锚点挂在胶囊根节点内而非 body:
  204. // 程序化 a.click() 的合成点击(detail === 0)会被 onClickOutside 当作"外部点击"而关闭弹层,
  205. // 挂载到组件内部后合成点击的路径包含组件根节点,不会被误判
  206. const triggerDownload = (href: string, filename: string) => {
  207. const a = document.createElement('a');
  208. a.href = href;
  209. a.download = filename;
  210. const mountEl = wrapRef.value || document.body;
  211. mountEl.appendChild(a);
  212. a.click();
  213. mountEl.removeChild(a);
  214. };
  215. const downloadFile = async (item: CapsuleFileItem) => {
  216. // AI 下载链接直接走地址下载
  217. if (item.kind === 'download') {
  218. if (!item.url) return;
  219. triggerDownload(item.url, item.name);
  220. return;
  221. }
  222. // 上传附件:优先用原始文件对象下载(字节保真),否则由本地缓存内容构造 Blob
  223. const file = item.file;
  224. if (!file) return;
  225. // 历史回显附件只有服务端 ID,下载需要带令牌,交给父组件统一处理
  226. if (file.fileId) {
  227. emit('download-attachment', file);
  228. return;
  229. }
  230. if (file.originalFile) {
  231. const url = URL.createObjectURL(file.originalFile);
  232. triggerDownload(url, file.originalFile.name || file.name);
  233. URL.revokeObjectURL(url);
  234. return;
  235. }
  236. let blob: Blob | null = null;
  237. if (file.content != null) {
  238. blob = new Blob([file.content], { type: file.type || 'text/plain' });
  239. } else if (file.arrayBuffer) {
  240. blob = new Blob([file.arrayBuffer], { type: file.type || 'application/octet-stream' });
  241. } else if (file.preview) {
  242. try {
  243. blob = await (await fetch(file.preview)).blob();
  244. } catch {
  245. blob = null;
  246. }
  247. }
  248. if (!blob) {
  249. emit('file-preview', file);
  250. return;
  251. }
  252. const url = URL.createObjectURL(blob);
  253. triggerDownload(url, file.name);
  254. URL.revokeObjectURL(url);
  255. };
  256. // 点击文件行:统一在右侧面板打开预览(上传附件直接用本地数据,下载链接先拉取内容)
  257. const handleFileClick = async (item: CapsuleFileItem) => {
  258. const key = fileKey(item);
  259. // 该行正在拉取预览内容时忽略重复点击
  260. if (previewingKey.value === key) return;
  261. if (item.kind === 'upload') {
  262. if (item.file) emit('file-preview', item.file);
  263. return;
  264. }
  265. if (!item.url) return;
  266. previewingKey.value = key;
  267. try {
  268. const previewFile = await fetchPreviewFile(item.url, item.name);
  269. if (!previewFile) {
  270. downloadFile(item);
  271. return;
  272. }
  273. emit('file-preview', previewFile);
  274. } catch (err) {
  275. console.error('获取文件内容失败:', err);
  276. message.error('文件预览失败,请尝试下载查看');
  277. } finally {
  278. previewingKey.value = null;
  279. }
  280. };
  281. // http(s) 链接一律在右侧面板打开(不区分站内站外),非 http 协议或非法地址仍按原行为新标签页打开
  282. const openLink = (link: CapsuleLinkItem) => {
  283. if (!link.href) return;
  284. const url = panelOpenableUrl(link.href);
  285. if (url) {
  286. emit('open-web', { url, title: webTabTitle(url, link.title) });
  287. return;
  288. }
  289. window.open(link.href, '_blank', 'noopener,noreferrer');
  290. };
  291. const agentStatusText = (status: CapsuleAgentItem['status']): string => {
  292. if (status === 'done') return '已完成';
  293. if (status === 'running') return '运行中';
  294. if (status === 'waiting') return '等待确认';
  295. return '已中断';
  296. };
  297. // 点击智能体条目:交给父组件打开右侧智能体详情面板(身份与思考卡片分组一致)
  298. const handleAgentClick = (item: CapsuleAgentItem) => {
  299. emit('agent-detail-click', { messageIndex: item.messageIndex, agent: item.agent, agentId: item.agentId });
  300. };
  301. // 定位到该智能体所在的消息(收起弹层后由父组件滚动消息列表)
  302. const handleLocateClick = (item: CapsuleAgentItem) => {
  303. isOpen.value = false;
  304. emit('locate-message', item.messageIndex);
  305. };
  306. </script>
  307. <style scoped lang="less">
  308. .resource-capsule {
  309. position: relative;
  310. flex-shrink: 0;
  311. margin-left: 8px;
  312. z-index: 10;
  313. .capsule-btn {
  314. display: flex;
  315. align-items: center;
  316. gap: 6px;
  317. height: 36px;
  318. padding: 0 12px;
  319. border-radius: 18px;
  320. background: linear-gradient(135deg, rgba(10, 132, 255, 0.22) 0%, rgba(10, 132, 255, 0.07) 100%);
  321. border: 1px solid rgba(10, 132, 255, 0.45);
  322. box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  323. cursor: pointer;
  324. user-select: none;
  325. font-family: inherit;
  326. transition:
  327. background 0.2s,
  328. border-color 0.2s,
  329. box-shadow 0.2s,
  330. transform 0.1s;
  331. &:hover {
  332. background: linear-gradient(135deg, rgba(10, 132, 255, 0.32) 0%, rgba(10, 132, 255, 0.14) 100%);
  333. border-color: rgba(10, 132, 255, 0.65);
  334. box-shadow:
  335. 0 0 10px rgba(10, 132, 255, 0.25),
  336. inset 0 1px 0 rgba(255, 255, 255, 0.08);
  337. }
  338. &:active {
  339. background: linear-gradient(135deg, rgba(10, 132, 255, 0.4) 0%, rgba(10, 132, 255, 0.2) 100%);
  340. transform: scale(0.97);
  341. }
  342. &:focus-visible {
  343. outline: 1px solid rgba(10, 132, 255, 0.8);
  344. outline-offset: 1px;
  345. }
  346. &.open {
  347. background: linear-gradient(135deg, rgba(10, 132, 255, 0.32) 0%, rgba(10, 132, 255, 0.14) 100%);
  348. border-color: rgba(10, 132, 255, 0.65);
  349. box-shadow:
  350. 0 0 10px rgba(10, 132, 255, 0.25),
  351. inset 0 1px 0 rgba(255, 255, 255, 0.08);
  352. }
  353. .capsule-icon {
  354. width: 14px;
  355. height: 14px;
  356. background-image: var(--img-chat-attach-icon);
  357. background-repeat: no-repeat;
  358. background-size: 100% 100%;
  359. flex-shrink: 0;
  360. filter: drop-shadow(0 0 3px rgba(10, 132, 255, 0.5));
  361. }
  362. .capsule-text {
  363. font-size: 12px;
  364. color: #e0e6ed;
  365. white-space: nowrap;
  366. }
  367. .capsule-count {
  368. min-width: 18px;
  369. height: 18px;
  370. line-height: 18px;
  371. padding: 0 5px;
  372. border-radius: 9px;
  373. background: linear-gradient(135deg, #2f9bff 0%, #0a84ff 100%);
  374. box-shadow: 0 1px 4px rgba(10, 132, 255, 0.45);
  375. color: #fff;
  376. font-size: 11px;
  377. text-align: center;
  378. flex-shrink: 0;
  379. }
  380. .capsule-arrow {
  381. width: 10px;
  382. height: 10px;
  383. background-image: var(--img-chat-arrow-icon);
  384. background-repeat: no-repeat;
  385. background-size: 100% 100%;
  386. flex-shrink: 0;
  387. transition: transform 0.2s;
  388. }
  389. &.open .capsule-arrow {
  390. transform: rotate(180deg);
  391. }
  392. }
  393. .capsule-popup {
  394. position: absolute;
  395. top: calc(100% + 8px);
  396. right: 0;
  397. width: 300px;
  398. max-height: 400px;
  399. background: linear-gradient(135deg, #0a1628 0%, #0d2137 50%, #0a1e35 100%);
  400. border: 1px solid #1a4a6f;
  401. border-radius: 10px;
  402. box-shadow:
  403. 0 8px 24px rgba(0, 0, 0, 0.5),
  404. 0 0 14px rgba(10, 132, 255, 0.08);
  405. z-index: 1000;
  406. display: flex;
  407. flex-direction: column;
  408. // 指向胶囊按钮的小箭头(与会话提示条箭头同一画法)
  409. &::before {
  410. content: '';
  411. position: absolute;
  412. top: -6px;
  413. right: 26px;
  414. width: 10px;
  415. height: 10px;
  416. transform: rotate(45deg);
  417. background: #0d2137;
  418. border-left: 1px solid #1a4a6f;
  419. border-top: 1px solid #1a4a6f;
  420. }
  421. .popup-header {
  422. padding: 12px 14px;
  423. border-bottom: 1px solid rgba(26, 74, 111, 0.55);
  424. background: linear-gradient(180deg, rgba(10, 132, 255, 0.1) 0%, rgba(10, 132, 255, 0.02) 100%);
  425. border-radius: 10px 10px 0 0;
  426. display: flex;
  427. justify-content: space-between;
  428. align-items: center;
  429. flex-shrink: 0;
  430. .popup-title {
  431. display: flex;
  432. align-items: center;
  433. gap: 8px;
  434. font-size: 13px;
  435. font-weight: 500;
  436. letter-spacing: 0.3px;
  437. color: #e6edf3;
  438. .popup-title-icon {
  439. width: 15px;
  440. height: 15px;
  441. background-image: var(--img-chat-attach-icon);
  442. background-repeat: no-repeat;
  443. background-size: 100% 100%;
  444. filter: drop-shadow(0 0 3px rgba(10, 132, 255, 0.5));
  445. flex-shrink: 0;
  446. }
  447. }
  448. .close-icon {
  449. cursor: pointer;
  450. font-size: 12px;
  451. color: #8b949e;
  452. width: 22px;
  453. height: 22px;
  454. display: flex;
  455. align-items: center;
  456. justify-content: center;
  457. border-radius: 50%;
  458. transition:
  459. color 0.2s,
  460. background 0.2s;
  461. &:hover {
  462. color: #e0e6ed;
  463. background: rgba(10, 132, 255, 0.2);
  464. }
  465. }
  466. }
  467. .popup-body {
  468. flex: 1;
  469. overflow-y: auto;
  470. padding: 6px;
  471. scrollbar-width: thin;
  472. scrollbar-color: rgba(139, 148, 158, 0.4) transparent;
  473. &::-webkit-scrollbar {
  474. width: 6px;
  475. }
  476. &::-webkit-scrollbar-track {
  477. background: transparent;
  478. }
  479. &::-webkit-scrollbar-thumb {
  480. background: rgba(139, 148, 158, 0.35);
  481. border-radius: 3px;
  482. &:hover {
  483. background: rgba(139, 148, 158, 0.6);
  484. }
  485. }
  486. .popup-section {
  487. & + .popup-section {
  488. margin-top: 4px;
  489. padding-top: 6px;
  490. border-top: 1px solid rgba(63, 80, 106, 0.35);
  491. }
  492. .section-title {
  493. display: flex;
  494. align-items: center;
  495. gap: 6px;
  496. padding: 6px 10px;
  497. border-radius: 6px;
  498. font-size: 12px;
  499. font-weight: 500;
  500. letter-spacing: 0.4px;
  501. color: #a9b9c9;
  502. cursor: pointer;
  503. user-select: none;
  504. transition:
  505. background 0.2s,
  506. color 0.2s;
  507. &:hover {
  508. background: rgba(10, 132, 255, 0.12);
  509. color: #e0e6ed;
  510. }
  511. .section-title-text {
  512. font-size: inherit;
  513. font-weight: inherit;
  514. letter-spacing: inherit;
  515. color: inherit;
  516. }
  517. .section-count {
  518. min-width: 16px;
  519. height: 16px;
  520. line-height: 16px;
  521. padding: 0 5px;
  522. border-radius: 8px;
  523. background: rgba(10, 132, 255, 0.14);
  524. color: #79c0ff;
  525. font-size: 11px;
  526. text-align: center;
  527. }
  528. .section-arrow {
  529. width: 10px;
  530. height: 10px;
  531. margin-left: auto;
  532. background-image: var(--img-chat-arrow-icon);
  533. background-repeat: no-repeat;
  534. background-size: 100% 100%;
  535. opacity: 0.7;
  536. transition:
  537. transform 0.2s,
  538. opacity 0.2s;
  539. }
  540. &:hover .section-arrow {
  541. opacity: 1;
  542. }
  543. &.collapsed .section-arrow {
  544. transform: rotate(180deg);
  545. }
  546. }
  547. }
  548. .popup-item {
  549. display: flex;
  550. align-items: center;
  551. gap: 10px;
  552. padding: 9px 10px;
  553. cursor: pointer;
  554. border-radius: 6px;
  555. transition: background 0.2s;
  556. &:hover {
  557. background: rgba(10, 132, 255, 0.14);
  558. }
  559. &:active {
  560. background: rgba(10, 132, 255, 0.22);
  561. }
  562. .item-icon {
  563. width: 26px;
  564. height: 26px;
  565. background-repeat: no-repeat;
  566. background-size: 100% 100%;
  567. flex-shrink: 0;
  568. }
  569. .row-loading {
  570. width: 16px;
  571. height: 16px;
  572. margin: 5px;
  573. border: 2px solid rgba(10, 132, 255, 0.3);
  574. border-top-color: #0a84ff;
  575. border-radius: 50%;
  576. flex-shrink: 0;
  577. animation: row-spin 0.8s linear infinite;
  578. }
  579. .item-info {
  580. flex: 1;
  581. min-width: 0;
  582. .item-name {
  583. font-size: 12px;
  584. color: #e0e6ed;
  585. overflow: hidden;
  586. text-overflow: ellipsis;
  587. white-space: nowrap;
  588. }
  589. .item-meta {
  590. display: flex;
  591. align-items: center;
  592. gap: 6px;
  593. margin-top: 2px;
  594. font-size: 11px;
  595. color: #8b949e;
  596. .item-tag {
  597. padding: 0 5px;
  598. border-radius: 3px;
  599. line-height: 16px;
  600. &.upload {
  601. color: #79c0ff;
  602. background: rgba(10, 132, 255, 0.15);
  603. }
  604. &.download {
  605. color: #8b949e;
  606. background: rgba(139, 148, 158, 0.15);
  607. }
  608. &.folded {
  609. color: #8b949e;
  610. background: rgba(139, 148, 158, 0.15);
  611. }
  612. }
  613. }
  614. .link-href {
  615. overflow: hidden;
  616. text-overflow: ellipsis;
  617. white-space: nowrap;
  618. }
  619. }
  620. .agent-icon {
  621. width: 26px;
  622. height: 26px;
  623. background-image: var(--img-chat-agent-icon);
  624. background-repeat: no-repeat;
  625. background-size: 100% 100%;
  626. flex-shrink: 0;
  627. }
  628. .agent-status {
  629. display: inline-flex;
  630. align-items: center;
  631. gap: 4px;
  632. padding: 0 5px;
  633. border-radius: 3px;
  634. line-height: 16px;
  635. .status-dot {
  636. width: 6px;
  637. height: 6px;
  638. border-radius: 50%;
  639. background: currentColor;
  640. }
  641. &.running {
  642. color: #0a84ff;
  643. background: rgba(10, 132, 255, 0.15);
  644. box-shadow: 0 0 6px rgba(10, 132, 255, 0.25);
  645. .status-dot {
  646. animation: status-pulse 1.2s ease-in-out infinite;
  647. }
  648. }
  649. &.waiting {
  650. color: #a371f7;
  651. background: rgba(163, 113, 247, 0.12);
  652. }
  653. &.done {
  654. color: #3fb950;
  655. background: rgba(63, 185, 80, 0.12);
  656. }
  657. &.interrupted {
  658. color: #d29922;
  659. background: rgba(210, 153, 34, 0.12);
  660. }
  661. }
  662. .download-btn {
  663. width: 24px;
  664. height: 24px;
  665. display: flex;
  666. align-items: center;
  667. justify-content: center;
  668. padding: 0;
  669. border: none;
  670. border-radius: 4px;
  671. background: transparent;
  672. cursor: pointer;
  673. flex-shrink: 0;
  674. transition:
  675. background 0.2s,
  676. transform 0.1s;
  677. .download-btn-icon {
  678. width: 14px;
  679. height: 14px;
  680. background-image: var(--img-chat-download-icon-btn);
  681. background-repeat: no-repeat;
  682. background-size: 100% 100%;
  683. }
  684. &:hover {
  685. background: rgba(10, 132, 255, 0.25);
  686. }
  687. &:active {
  688. background: rgba(10, 132, 255, 0.4);
  689. transform: scale(0.88);
  690. }
  691. &:focus-visible {
  692. outline: 1px solid rgba(10, 132, 255, 0.8);
  693. }
  694. }
  695. .locate-btn {
  696. width: 24px;
  697. height: 24px;
  698. display: flex;
  699. align-items: center;
  700. justify-content: center;
  701. padding: 0;
  702. border: none;
  703. border-radius: 4px;
  704. background: transparent;
  705. cursor: pointer;
  706. flex-shrink: 0;
  707. transition:
  708. background 0.2s,
  709. transform 0.1s;
  710. .locate-btn-icon {
  711. width: 14px;
  712. height: 14px;
  713. background-image: var(--img-chat-locate-icon);
  714. background-repeat: no-repeat;
  715. background-size: 100% 100%;
  716. opacity: 0.8;
  717. transition: opacity 0.2s;
  718. }
  719. &:hover {
  720. background: rgba(10, 132, 255, 0.25);
  721. .locate-btn-icon {
  722. opacity: 1;
  723. }
  724. }
  725. &:active {
  726. background: rgba(10, 132, 255, 0.4);
  727. transform: scale(0.88);
  728. }
  729. &:focus-visible {
  730. outline: 1px solid rgba(10, 132, 255, 0.8);
  731. }
  732. }
  733. .link-glyph {
  734. width: 26px;
  735. height: 26px;
  736. background-image: var(--img-chat-link-icon);
  737. background-repeat: no-repeat;
  738. background-size: 14px 14px;
  739. background-position: center;
  740. flex-shrink: 0;
  741. opacity: 0.8;
  742. transition: opacity 0.2s;
  743. }
  744. &:hover .link-glyph {
  745. opacity: 1;
  746. }
  747. .link-arrow {
  748. width: 10px;
  749. height: 10px;
  750. background-image: var(--img-chat-next-icon);
  751. background-repeat: no-repeat;
  752. background-size: 100% 100%;
  753. flex-shrink: 0;
  754. }
  755. }
  756. .popup-empty {
  757. padding: 20px 12px;
  758. text-align: center;
  759. .popup-empty-title {
  760. font-size: 12px;
  761. color: #c9d1d9;
  762. }
  763. .popup-empty-desc {
  764. margin-top: 4px;
  765. font-size: 11px;
  766. line-height: 1.5;
  767. color: #8b949e;
  768. }
  769. }
  770. }
  771. }
  772. }
  773. .capsule-fade-enter-active,
  774. .capsule-fade-leave-active {
  775. transition:
  776. opacity 0.15s ease,
  777. transform 0.15s ease;
  778. }
  779. .capsule-fade-enter-from,
  780. .capsule-fade-leave-to {
  781. opacity: 0;
  782. transform: translateY(-4px);
  783. }
  784. // 分区折叠过渡(与思考卡片折叠动画同一模式)
  785. .collapse-enter-active,
  786. .collapse-leave-active {
  787. transition: all 0.25s ease;
  788. overflow: hidden;
  789. }
  790. .collapse-enter-from,
  791. .collapse-leave-to {
  792. opacity: 0;
  793. max-height: 0;
  794. }
  795. .collapse-enter-to,
  796. .collapse-leave-from {
  797. opacity: 1;
  798. max-height: 600px;
  799. }
  800. @keyframes status-pulse {
  801. 0%,
  802. 100% {
  803. opacity: 1;
  804. }
  805. 50% {
  806. opacity: 0.3;
  807. }
  808. }
  809. @keyframes row-spin {
  810. to {
  811. transform: rotate(360deg);
  812. }
  813. }
  814. </style>