codexChatToolbar.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <template>
  2. <header class="codex-topbar">
  3. <div class="codex-topbar__title">
  4. Codex 数据分析
  5. <span v-if="sessionTitle" class="codex-topbar__meta">{{ sessionTitle }}</span>
  6. </div>
  7. <span class="codex-topbar__spacer"></span>
  8. <!-- 运行时状态 -->
  9. <div class="codex-topbar__status" :title="runtimeTitle">
  10. <span class="codex-dot" :class="`codex-dot--${dotTone}`"></span>
  11. <span class="codex-topbar__meta">{{ statusText }}</span>
  12. <span v-if="version" class="codex-topbar__meta">v{{ version }}</span>
  13. </div>
  14. <!-- 工作目录 -->
  15. <div v-if="cwd" class="codex-topbar__status" :title="cwd">
  16. <Icon icon="mdi:folder-outline" :size="14" class="codex-topbar__icon" />
  17. <span class="codex-topbar__meta codex-topbar__path">{{ cwd }}</span>
  18. <button class="codex-btn" type="button" @click="onCopyCwd">复制</button>
  19. </div>
  20. <div class="codex-topbar__actions">
  21. <span v-if="runBadge" class="codex-badge" :class="runBadge.tone" :title="runBadge.title">
  22. {{ runBadge.text }}
  23. </span>
  24. <span v-if="runningCount" class="codex-badge codex-badge--running" :title="`${runningCount} 个会话正在执行`">
  25. {{ runningCount }} 个会话运行中
  26. </span>
  27. <a-button v-if="needsStart" size="small" type="primary" :loading="busy" @click="onStart">
  28. <template #icon><Icon icon="mdi:play" :size="14" /></template>
  29. 启动运行时
  30. </a-button>
  31. <a-button v-else size="small" :loading="busy" @click="onRestart">
  32. <template #icon><Icon icon="mdi:restart" :size="14" /></template>
  33. 重启
  34. </a-button>
  35. <a-dropdown :trigger="['click']" placement="bottomRight">
  36. <a-button size="small" :disabled="!hasMessages">
  37. <template #icon><Icon icon="mdi:download-outline" :size="14" /></template>
  38. 导出
  39. </a-button>
  40. <template #overlay>
  41. <a-menu @click="onExportClick">
  42. <a-menu-item key="md">Markdown 报告</a-menu-item>
  43. <a-menu-item key="html">HTML 报告</a-menu-item>
  44. <a-menu-item key="xlsx">Excel(表格合集)</a-menu-item>
  45. <a-menu-item key="png">对话长图</a-menu-item>
  46. </a-menu>
  47. </template>
  48. </a-dropdown>
  49. <a-button size="small" @click="emit('diagnostics')">
  50. <template #icon><Icon icon="mdi:bug-outline" :size="14" /></template>
  51. 诊断
  52. </a-button>
  53. <a-button size="small" @click="emit('create')">
  54. <template #icon><Icon icon="mdi:plus" :size="14" /></template>
  55. 新建会话
  56. </a-button>
  57. </div>
  58. </header>
  59. </template>
  60. <script lang="ts" setup>
  61. /**
  62. * 顶部状态条:运行时状态 / 工作目录 / 导出 / 诊断 / 新建会话。
  63. *
  64. * 状态直接读 store(这里没有第二份状态),只有需要 DOM 的动作(导出长图)才上抛给页面。
  65. */
  66. import { computed, ref } from 'vue';
  67. import { Button, Dropdown, Menu, MenuItem } from 'ant-design-vue';
  68. import Icon from '@/core/components/Icon/src/Icon.vue';
  69. import { useMessage } from '@/core/hooks/web/useMessage';
  70. import { useCodexChatStore } from '../store/codexChatStore';
  71. import type { CodexReportFormat } from '../types/codexTypes';
  72. import { copyCodexText } from '../utils/codexFormat';
  73. const emit = defineEmits<{
  74. (e: 'create'): void;
  75. (e: 'diagnostics'): void;
  76. (e: 'export', format: CodexReportFormat): void;
  77. }>();
  78. const AButton = Button;
  79. const ADropdown = Dropdown;
  80. const AMenu = Menu;
  81. const AMenuItem = MenuItem;
  82. const store = useCodexChatStore();
  83. const { showMessage } = useMessage();
  84. const busy = ref(false);
  85. const sessionTitle = computed(() => store.activeSession?.title || '');
  86. const cwd = computed(() => store.activeCwd);
  87. const version = computed(() => store.runtime?.version || '');
  88. const hasMessages = computed(() => store.hasMessages);
  89. const runningCount = computed(() => store.runningCount);
  90. /**
  91. * 非默认运行参数的徽标。
  92. * 把「只读 / 全权 / 免审批 / 每步确认」摆在顶栏,是为了让用户随时看得见当前会话的安全姿态。
  93. */
  94. const runBadge = computed<{ text: string; tone: string; title: string } | null>(() => {
  95. const options = store.activeRunOptions;
  96. const parts: string[] = [];
  97. let tone = 'codex-badge--muted';
  98. if (options.sandbox === 'read-only') parts.push('只读');
  99. if (options.sandbox === 'danger-full-access') {
  100. parts.push('全权访问');
  101. tone = 'codex-badge--error';
  102. }
  103. if (options.approvalPolicy === 'never') {
  104. parts.push('免审批');
  105. tone = 'codex-badge--error';
  106. } else if (options.approvalPolicy === 'untrusted') {
  107. parts.push('每步确认');
  108. }
  109. if (!parts.length) return null;
  110. return { text: parts.join(' · '), tone, title: '当前运行参数,可在输入区的「参数」里修改' };
  111. });
  112. const needsStart = computed(() => (store.runtime?.state ?? 'stopped') !== 'ready');
  113. const statusText = computed(() => {
  114. const status = store.runtime;
  115. if (!status) return '未检测';
  116. if (status.state === 'ready') return status.degraded ? '就绪(模型降级)' : '就绪';
  117. if (status.state === 'starting') return '启动中';
  118. if (status.state === 'error') return '运行时异常';
  119. return '未启动';
  120. });
  121. const dotTone = computed(() => {
  122. const status = store.runtime;
  123. if (!status) return 'idle';
  124. if (status.state === 'ready') return 'ready';
  125. if (status.state === 'starting') return 'busy';
  126. if (status.state === 'error') return 'error';
  127. return 'idle';
  128. });
  129. const runtimeTitle = computed(() => {
  130. const status = store.runtime;
  131. const parts: string[] = [];
  132. if (status?.binaryPath) parts.push(`二进制:${status.binaryPath}`);
  133. if (status?.applied) parts.push(`已应用模型:${status.applied.model}`);
  134. if (status?.error) parts.push(`错误:${status.error}`);
  135. if (store.runtimeError && !status?.error) parts.push(store.runtimeError);
  136. return parts.join('\n') || 'Codex 运行时状态';
  137. });
  138. async function onStart() {
  139. busy.value = true;
  140. try {
  141. await store.startRuntime();
  142. showMessage('Codex 运行时已启动', 'success');
  143. } catch (error: any) {
  144. showMessage(error?.message || '启动失败', 'error');
  145. } finally {
  146. busy.value = false;
  147. }
  148. }
  149. async function onRestart() {
  150. busy.value = true;
  151. try {
  152. await store.restartRuntime();
  153. showMessage('Codex 运行时已重启', 'success');
  154. } catch (error: any) {
  155. showMessage(error?.message || '重启失败', 'error');
  156. } finally {
  157. busy.value = false;
  158. }
  159. }
  160. async function onCopyCwd() {
  161. const ok = await copyCodexText(cwd.value);
  162. showMessage(ok ? '已复制工作目录' : '复制失败', ok ? 'success' : 'warning');
  163. }
  164. function onExportClick(info: { key: string | number }) {
  165. emit('export', String(info.key) as CodexReportFormat);
  166. }
  167. </script>
  168. <style lang="less">
  169. .codex-topbar {
  170. &__icon {
  171. flex: 0 0 auto;
  172. color: var(--codex-text-2);
  173. }
  174. &__path {
  175. display: inline-block;
  176. max-width: 220px;
  177. overflow: hidden;
  178. white-space: nowrap;
  179. text-overflow: ellipsis;
  180. direction: rtl;
  181. text-align: left;
  182. }
  183. }
  184. </style>