@templmf/temp-solf-lmf 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/fe-flow.md +348 -0
  2. package/package.json +1 -1
  3. package/ui-parse.md +308 -0
  4. package//345/242/236/351/207/217/351/234/200/346/261/202prompt +72 -0
  5. package/guanwang/README.md +0 -95
  6. package/guanwang/docs/changelog.md +0 -145
  7. package/guanwang/docs/doc-maintenance.md +0 -229
  8. package/guanwang/docs/product.md +0 -181
  9. package/guanwang/docs/test-cases.md +0 -395
  10. package/guanwang/docs/usage.md +0 -291
  11. package/guanwang/env.example +0 -27
  12. package/guanwang/index.html +0 -13
  13. package/guanwang/package-lock.json +0 -3825
  14. package/guanwang/package.json +0 -32
  15. package/guanwang/public/favicon.svg +0 -4
  16. package/guanwang/public/react-runtime/babel.min.js +0 -4
  17. package/guanwang/public/react-runtime/react-dom.min.js +0 -267
  18. package/guanwang/public/react-runtime/react.min.js +0 -31
  19. package/guanwang/public/vue-repl-assets/compiler-sfc.esm-browser.js +0 -50795
  20. package/guanwang/public/vue-repl-assets/runtime-dom.esm-browser.js +0 -12758
  21. package/guanwang/public/vue-repl-assets/server-renderer.esm-browser.js +0 -8600
  22. package/guanwang/public/vue-repl-assets/vue.esm-browser.js +0 -18672
  23. package/guanwang/src/App.vue +0 -61
  24. package/guanwang/src/chat-sdk/core/components/ChatBox.vue +0 -305
  25. package/guanwang/src/chat-sdk/core/components/ChatSidebar.vue +0 -84
  26. package/guanwang/src/chat-sdk/core/components/InputBar.vue +0 -354
  27. package/guanwang/src/chat-sdk/core/components/MessageBubble.vue +0 -703
  28. package/guanwang/src/chat-sdk/core/useTheme.js +0 -31
  29. package/guanwang/src/chat-sdk/features/artifact/ArtifactCard.vue +0 -172
  30. package/guanwang/src/chat-sdk/features/artifact/ArtifactPanel.vue +0 -963
  31. package/guanwang/src/chat-sdk/features/artifact/index.js +0 -13
  32. package/guanwang/src/chat-sdk/features/artifact/useArtifactStore.js +0 -275
  33. package/guanwang/src/chat-sdk/features/codepreview/CodePreview.vue +0 -523
  34. package/guanwang/src/chat-sdk/features/codepreview/index.js +0 -7
  35. package/guanwang/src/chat-sdk/features/markdown/index.js +0 -13
  36. package/guanwang/src/chat-sdk/features/markdown/useMarkdown.js +0 -724
  37. package/guanwang/src/chat-sdk/features/mermaid/MermaidZoom.vue +0 -254
  38. package/guanwang/src/chat-sdk/features/upload/FileAttachment.vue +0 -142
  39. package/guanwang/src/chat-sdk/features/upload/index.js +0 -17
  40. package/guanwang/src/chat-sdk/features/upload/useFileHandler.js +0 -336
  41. package/guanwang/src/chat-sdk/headless/api/adapters/openai.js +0 -76
  42. package/guanwang/src/chat-sdk/headless/api/chatApi.js +0 -126
  43. package/guanwang/src/chat-sdk/headless/buildSystemPrompt.js +0 -351
  44. package/guanwang/src/chat-sdk/headless/index.js +0 -15
  45. package/guanwang/src/chat-sdk/headless/useChat.js +0 -77
  46. package/guanwang/src/chat-sdk/headless/useChatDB.js +0 -147
  47. package/guanwang/src/chat-sdk/headless/useChatStore.js +0 -529
  48. package/guanwang/src/chat-sdk/index.js +0 -79
  49. package/guanwang/src/chat-sdk/modes/architect.js +0 -27
  50. package/guanwang/src/chat-sdk/modes/ask.js +0 -26
  51. package/guanwang/src/chat-sdk/modes/code.js +0 -25
  52. package/guanwang/src/chat-sdk/modes/index.js +0 -36
  53. package/guanwang/src/chat-sdk/modes/requirements.js +0 -175
  54. package/guanwang/src/chat-sdk/settings/SettingsPanel.vue +0 -170
  55. package/guanwang/src/chat-sdk/settings/index.js +0 -9
  56. package/guanwang/src/chat-sdk/settings/useSettings.js +0 -122
  57. package/guanwang/src/chat-sdk/tools/defaults.js +0 -89
  58. package/guanwang/src/chat-sdk/tools/index.js +0 -16
  59. package/guanwang/src/chat-sdk/tools/parser.js +0 -116
  60. package/guanwang/src/components/CustomCursor.vue +0 -69
  61. package/guanwang/src/components/Footer.vue +0 -24
  62. package/guanwang/src/components/LoginModal.vue +0 -109
  63. package/guanwang/src/components/Navbar.vue +0 -193
  64. package/guanwang/src/components/ThemeToggle.vue +0 -25
  65. package/guanwang/src/composables/useArtifactStore.js +0 -253
  66. package/guanwang/src/composables/useAuth.js +0 -88
  67. package/guanwang/src/composables/useChatDB.js +0 -147
  68. package/guanwang/src/composables/useCountUp.js +0 -24
  69. package/guanwang/src/composables/useFileHandler.js +0 -345
  70. package/guanwang/src/composables/useTheme.js +0 -31
  71. package/guanwang/src/config/api.js +0 -71
  72. package/guanwang/src/main.js +0 -23
  73. package/guanwang/src/router/index.js +0 -23
  74. package/guanwang/src/services/authApi.js +0 -27
  75. package/guanwang/src/services/chatApi.js +0 -66
  76. package/guanwang/src/styles/global.css +0 -478
  77. package/guanwang/src/tracker/analyze.js +0 -73
  78. package/guanwang/src/tracker/config.js +0 -82
  79. package/guanwang/src/tracker/index.js +0 -18
  80. package/guanwang/src/tracker/service.js +0 -102
  81. package/guanwang/src/tracker/useChatTracker.js +0 -179
  82. package/guanwang/src/tracker/useTracker.js +0 -45
  83. package/guanwang/src/views/ChatView.vue +0 -65
  84. package/guanwang/src/views/HomeView.vue +0 -156
  85. package/guanwang/src/views/MarketView.vue +0 -143
  86. package/guanwang/src/views/PracticesView.vue +0 -190
  87. package/guanwang/src/views/SkillsView.vue +0 -129
  88. package/guanwang/temp +0 -19
  89. package/guanwang/vite.config.js +0 -6
@@ -1,963 +0,0 @@
1
- <template>
2
- <transition name="panel-slide">
3
- <div v-if="store.panelVisible.value" class="artifact-panel">
4
-
5
- <!-- ① 文件 Tab 栏:只显示当前打开的文件 -->
6
- <div class="panel-tabbar">
7
- <div class="panel-tabbar__scroll">
8
- <button v-if="activeArtifact" class="panel-filetab active">
9
- <component :is="getIcon(activeArtifact.lang)" :size="12" :stroke-width="1.8" />
10
- <span class="tab-name">{{ activeArtifact.fileName }}</span>
11
- <span v-if="activeArtifact.streaming" class="tab-streaming-dot" />
12
- </button>
13
- </div>
14
-
15
- <!-- 右侧操作区 -->
16
- <div class="panel-tabbar__actions">
17
- <!-- Vue SFC:固定分屏,使用 @vue/repl -->
18
- <el-tooltip v-if="!isVueSfcReady" content="切换代码/预览" placement="bottom" :show-after="400">
19
- <button
20
- class="panel-icon-btn"
21
- :class="{ active: viewMode === 'preview' }"
22
- :disabled="!canPreview"
23
- @click="toggleView"
24
- >
25
- <Eye :size="14" />
26
- </button>
27
- </el-tooltip>
28
- <el-tooltip content="复制代码" placement="bottom" :show-after="400">
29
- <button class="panel-icon-btn" @click="copyCode">
30
- <Check v-if="copied" :size="14" color="#4ade80" />
31
- <Copy v-else :size="14" />
32
- </button>
33
- </el-tooltip>
34
- <el-tooltip content="下载文件" placement="bottom" :show-after="400">
35
- <button class="panel-icon-btn" :disabled="!activeArtifact?.code" @click="downloadFile">
36
- <Download :size="14" />
37
- </button>
38
- </el-tooltip>
39
- <el-tooltip v-if="isMermaid && mermaidSvgHtml" content="放大预览" placement="bottom" :show-after="400">
40
- <button class="panel-icon-btn" @click="mermaidZoomVisible = true">
41
- <Maximize2 :size="14" />
42
- </button>
43
- </el-tooltip>
44
- <el-tooltip content="收起面板" placement="bottom" :show-after="400">
45
- <button class="panel-icon-btn" @click="store.close()">
46
- <PanelRightClose :size="14" />
47
- </button>
48
- </el-tooltip>
49
- </div>
50
- </div>
51
-
52
- <!-- ② 内容区 -->
53
- <div class="panel-content">
54
-
55
- <!-- Vue SFC:固定分屏(仅 streaming 完成后显示) -->
56
- <template v-if="isVueSfcReady">
57
- <div v-if="!replStore" class="panel-waiting" style="flex:1;justify-content:center;">
58
- <span class="panel-waiting__dot" />
59
- <span class="panel-waiting__dot" />
60
- <span class="panel-waiting__dot" />
61
- </div>
62
- <Repl
63
- v-else
64
- :store="replStore"
65
- :editor="CodeMirror"
66
- layout="vertical"
67
- :show-compile-output="false"
68
- :show-import-map="false"
69
- :show-ts-config="false"
70
- :clear-console="false"
71
- :show-error="false"
72
- :auto-save="false"
73
- :theme="isDarkTheme() ? 'dark' : 'light'"
74
- class="panel-vue-repl"
75
- />
76
- </template>
77
-
78
- <!-- 代码视图(vue streaming 中 或 非 vue) -->
79
- <div v-else-if="viewMode === 'code'" class="panel-view panel-view--code">
80
- <!-- 流式:有内容时显示进度条,无内容时显示呼吸占位 -->
81
- <template v-if="activeArtifact?.streaming">
82
- <div v-if="highlightedHtml" class="streaming-bar">
83
- <span class="streaming-bar__dot" />
84
- <span>正在生成 {{ activeArtifact.fileName }}...</span>
85
- </div>
86
- <div v-else class="panel-waiting">
87
- <span class="panel-waiting__dot" />
88
- <span class="panel-waiting__dot" />
89
- <span class="panel-waiting__dot" />
90
- </div>
91
- </template>
92
- <div class="shiki-wrap" v-html="highlightedHtml" @click="onCodeClick" />
93
- </div>
94
-
95
- <!-- 预览视图(非 vue) -->
96
- <div v-else-if="viewMode === 'preview'" class="panel-view panel-view--preview">
97
- <div v-if="isMermaid" ref="mermaidRef" class="preview-mermaid" />
98
- <div v-else-if="isSvg" class="preview-svg" v-html="sanitizedSvg" />
99
- <div v-else-if="isMath" class="preview-math" v-html="renderedMath" />
100
- <iframe
101
- v-else-if="isHtmlPreviewable"
102
- :key="iframeKey"
103
- ref="iframeRef"
104
- sandbox="allow-scripts allow-modals allow-forms"
105
- class="preview-iframe"
106
- />
107
- <div v-else-if="isMarkdown" class="preview-markdown" v-html="renderedMarkdown" @click="onMarkdownClick" />
108
- <div v-else class="preview-unsupported">
109
- <FileCode :size="32" style="opacity:0.3;margin-bottom:8px;" />
110
- <p>该语言暂不支持预览</p>
111
- </div>
112
- </div>
113
- </div>
114
-
115
- <!-- ③ 底部状态栏 -->
116
- <div class="panel-statusbar">
117
- <span class="status-lang">{{ LANG_LABEL[activeArtifact?.lang] || activeArtifact?.lang }}</span>
118
- <span class="status-sep">·</span>
119
- <span v-if="activeArtifact?.streaming" class="status-streaming">
120
- <span class="status-dot" />生成中
121
- </span>
122
- <template v-else>
123
- <span class="status-lines">{{ activeArtifact?.lines }} 行</span>
124
- <span class="status-sep">·</span>
125
- <span class="status-cursor">行 {{ cursorLine }},列 {{ cursorCol }}</span>
126
- </template>
127
- <span class="status-sep">·</span>
128
- <span class="status-file">{{ activeArtifact?.fileName }}</span>
129
- </div>
130
- </div>
131
- </transition>
132
-
133
- <!-- mermaid 全屏放大 -->
134
- <MermaidZoom
135
- :visible="mermaidZoomVisible"
136
- :svg-html="mermaidSvgHtml"
137
- @close="mermaidZoomVisible = false"
138
- />
139
- </template>
140
-
141
- <script setup>
142
- import { ref, computed, watch, nextTick, onUnmounted, defineAsyncComponent } from 'vue'
143
- import {
144
- Eye, Copy, Check, PanelRightClose, Download,
145
- FileCode, FileText, Globe, Layers, BarChart2,
146
- Braces, Terminal, Maximize2,
147
- } from 'lucide-vue-next'
148
- import MermaidZoom from '../../features/mermaid/MermaidZoom.vue'
149
- import DOMPurify from 'dompurify'
150
- import { useArtifactStore, LANG_LABEL } from './useArtifactStore.js'
151
- import { renderMarkdown, handleCodeAction, sanitizeMermaidCode } from '../../features/markdown/useMarkdown.js'
152
- import { useChatTracker } from '../../../tracker/useChatTracker.js'
153
-
154
- // 内网 Vue ESM 资源路径(与 CodePreview.vue 保持一致)
155
- // @vue/repl 内部只通过 import map 消费这两个包:
156
- // vue → 运行时(用于预览 iframe 内的组件渲染)
157
- // vue/server-renderer → SSR 渲染器(编译输出检查用)
158
- // compiler-sfc 由 @vue/repl 自身打包的 defaultCompiler 处理,不走 import map
159
- const VUE_ASSETS_BASE = '/vue-repl-assets'
160
- const INTERNAL_IMPORT_MAP = {
161
- imports: {
162
- 'vue': `${VUE_ASSETS_BASE}/vue.esm-browser.js`,
163
- 'vue/server-renderer':`${VUE_ASSETS_BASE}/server-renderer.esm-browser.js`,
164
- },
165
- }
166
-
167
- // JSX/TSX iframe 预览用的本地 React 运行时
168
- // 文件由 vite-plugin-static-copy 在构建时从 node_modules 复制,
169
- // 开发时需手动执行:
170
- // mkdir -p public/react-runtime
171
- // cp node_modules/react/umd/react.production.min.js public/react-runtime/react.min.js
172
- // cp node_modules/react-dom/umd/react-dom.production.min.js public/react-runtime/react-dom.min.js
173
- // cp node_modules/@babel/standalone/babel.min.js public/react-runtime/babel.min.js
174
- const REACT_RUNTIME_BASE = '/react-runtime'
175
-
176
- // @vue/repl 按需加载
177
- const Repl = defineAsyncComponent(() => import('@vue/repl').then(m => m.Repl))
178
- const CodeMirror = defineAsyncComponent(() => import('@vue/repl/codemirror-editor'))
179
-
180
- const store = useArtifactStore()
181
- const activeArtifact = computed(() => store.activeArtifact.value)
182
- const tracker = useChatTracker()
183
-
184
- // ── 视图模式 ──────────────────────────────────────────────────────
185
- const viewMode = ref('code') // 'code' | 'preview'
186
- const mermaidZoomVisible = ref(false)
187
- const mermaidSvgHtml = ref('')
188
- const copied = ref(false)
189
- const iframeKey = ref(0)
190
- const mermaidRef = ref(null)
191
- const iframeRef = ref(null)
192
-
193
- // ── 语言属性 ──────────────────────────────────────────────────────
194
- const lang = computed(() => activeArtifact.value?.lang || 'text')
195
-
196
- const isVueSfc = computed(() => lang.value === 'vue')
197
- const isMermaid = computed(() => lang.value === 'mermaid')
198
- const isSvg = computed(() => lang.value === 'svg')
199
- const isMath = computed(() => lang.value === 'math' || lang.value === 'latex')
200
- const isHtmlPreviewable = computed(() =>
201
- ['html','javascript','js','jsx','tsx','css'].includes(lang.value)
202
- )
203
- const isMarkdown = computed(() => lang.value === 'markdown' || lang.value === 'md')
204
- const canPreview = computed(() =>
205
- isMermaid.value || isSvg.value || isMath.value || isHtmlPreviewable.value || isMarkdown.value
206
- )
207
-
208
- // ── Vue SFC Repl ──────────────────────────────────────────────────
209
- // streaming 期间走普通 shiki 代码视图,完成后才初始化 repl 并切换到分屏
210
- const replStore = ref(null)
211
- const replReady = ref(false) // repl 已初始化且代码已写入
212
- let _replStore = null
213
-
214
- // 仅当 vue + streaming 已完成时才展示分屏
215
- const isVueSfcReady = computed(() => isVueSfc.value && replReady.value)
216
-
217
- async function initReplStore(code) {
218
- try {
219
- const { useStore } = await import('@vue/repl')
220
- if (!_replStore) {
221
- _replStore = useStore({ builtinImportMap: ref(INTERNAL_IMPORT_MAP) })
222
- replStore.value = _replStore
223
- }
224
- const mainFile = _replStore.mainFile?.value ?? _replStore.mainFile ?? 'src/App.vue'
225
- if (_replStore.files?.[mainFile]) {
226
- _replStore.files[mainFile].code = code
227
- } else {
228
- await _replStore.setFiles?.({ [mainFile]: code }, mainFile)
229
- }
230
- replReady.value = true
231
- } catch (e) {
232
- console.warn('[ArtifactPanel] @vue/repl 加载失败', e)
233
- replStore.value = null
234
- replReady.value = false
235
- }
236
- }
237
-
238
- // ── 预览内容(shallowRef 驱动,避免 shallowRef map 内部属性追踪失效)────
239
- // sanitizedSvg / renderedMath / renderedMarkdown 均不能用 computed 自动追踪
240
- // 因为 activeArtifact.value.code 是普通对象属性,不是响应式 ref,computed 感知不到变化
241
- const sanitizedSvg = ref('')
242
- const renderedMath = ref('')
243
- const renderedMarkdown = ref('')
244
-
245
- async function computePreviewContent(code, l) {
246
- const curLang = l || lang.value
247
-
248
- if (curLang === 'svg') {
249
- sanitizedSvg.value = code
250
- ? DOMPurify.sanitize(code, { USE_PROFILES: { svg: true } })
251
- : ''
252
- } else {
253
- sanitizedSvg.value = ''
254
- }
255
-
256
- if (curLang === 'math' || curLang === 'latex') {
257
- try {
258
- renderedMath.value = code
259
- ? (window.katex?.renderToString(code, { displayMode: true, throwOnError: false }) || code)
260
- : ''
261
- } catch { renderedMath.value = code || '' }
262
- } else {
263
- renderedMath.value = ''
264
- }
265
-
266
- if (curLang === 'markdown' || curLang === 'md') {
267
- if (!code) { renderedMarkdown.value = ''; return }
268
- try {
269
- // 使用 renderMarkdown(内含 safeMarkdownParse + innerDepth 状态机)
270
- // 能正确处理 markdown 内嵌套多个代码块的情况,避免 marked 裸解析截断
271
- renderedMarkdown.value = renderMarkdown(code, { noPreview: true })
272
- } catch(e) {
273
- console.warn('[ArtifactPanel] markdown 预览失败', e)
274
- renderedMarkdown.value = '<p>预览失败</p>'
275
- }
276
- } else {
277
- renderedMarkdown.value = ''
278
- }
279
- }
280
-
281
- // ── 语言图标 ──────────────────────────────────────────────────────
282
- const ICON_MAP = {
283
- vue:Layers, html:Globe, jsx:Layers, tsx:Layers,
284
- javascript:FileCode, js:FileCode, typescript:FileCode, ts:FileCode,
285
- python:FileCode, go:FileCode, rust:FileCode, java:FileCode,
286
- css:Braces, scss:Braces, bash:Terminal, sh:Terminal,
287
- mermaid:BarChart2, svg:Globe, math:FileText, latex:FileText, markdown:FileText,
288
- }
289
- function getIcon(l) { return ICON_MAP[l] || FileCode }
290
-
291
- // ── Shiki 高亮 ────────────────────────────────────────────────────
292
- let _hl = null
293
- const highlightedHtml = ref('')
294
-
295
- // 取消令牌:每次新的 highlight 调用时递增,旧的异步结果发现令牌不匹配则丢弃
296
- // 解决流式阶段多次异步调用产生的竞态问题(后发先至导致内容定格在中间快照)
297
- let _highlightToken = 0
298
-
299
- async function ensureShiki() {
300
- if (_hl) return _hl
301
- const { createHighlighter } = await import('shiki')
302
- _hl = await createHighlighter({
303
- themes: ['one-dark-pro','github-light'],
304
- langs: ['javascript','typescript','vue','html','css','scss','json','bash','shell',
305
- 'python','java','go','rust','sql','yaml','toml','xml','jsx','tsx','mermaid',
306
- 'markdown','kotlin','swift','graphql'],
307
- })
308
- return _hl
309
- }
310
-
311
- function isDarkTheme() {
312
- return document.documentElement.getAttribute('data-theme') !== 'light'
313
- }
314
-
315
- function fallbackHtml(code) {
316
- const lines = code.split('\n')
317
- const rows = lines.map((line, i) =>
318
- `<tr><td class="ln">${i + 1}</td><td class="lc">${line.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}</td></tr>`
319
- ).join('')
320
- return `<table class="fallback-table"><tbody>${rows}</tbody></table>`
321
- }
322
-
323
- async function highlight(code, l) {
324
- if (!code) { highlightedHtml.value = ''; return }
325
- // 每次调用拿到当前令牌,异步完成后校验,不匹配则丢弃(说明有更新的调用在后面)
326
- const token = ++_highlightToken
327
- try {
328
- const hl = await ensureShiki()
329
- if (token !== _highlightToken) return // 已被更新的调用取代,丢弃
330
- const safe = hl.getLoadedLanguages().includes(l) ? l : null
331
- if (!safe) {
332
- highlightedHtml.value = fallbackHtml(code)
333
- return
334
- }
335
- const html = hl.codeToHtml(code, {
336
- lang: safe,
337
- theme: isDarkTheme() ? 'one-dark-pro' : 'github-light',
338
- })
339
- if (token !== _highlightToken) return // 再次校验,防止 codeToHtml 期间被抢占
340
- highlightedHtml.value = html
341
- } catch {
342
- if (token !== _highlightToken) return
343
- highlightedHtml.value = fallbackHtml(code)
344
- }
345
- }
346
-
347
- const PREVIEW_BY_DEFAULT = new Set(['markdown', 'md', 'mermaid', 'svg', 'math', 'latex'])
348
-
349
- // ── debounce highlight(流式期间限频,完成后立即刷新)────────────
350
- let _debounceTimer = null
351
- function scheduleHighlight(code, l) {
352
- if (_debounceTimer) clearTimeout(_debounceTimer)
353
- _debounceTimer = setTimeout(() => {
354
- _debounceTimer = null
355
- highlight(code, l)
356
- }, 80)
357
- }
358
-
359
- // ── 监听 artifact 内容变化(流式实时更新代码视图)────────────────
360
- watch(
361
- () => store.activeCode.value,
362
- (code) => {
363
- console.log('code========>', code)
364
- const l = activeArtifact.value?.lang
365
- // Vue SFC streaming 期间:走普通 shiki 高亮,不初始化 repl(避免每 token 编译)
366
- // Vue SFC 完成后:由 streaming watcher 统一触发 initReplStore
367
- if (code && l) scheduleHighlight(code, l)
368
- if (!isVueSfc.value) {
369
- // mermaid/svg/math 等:streaming 期间保持代码视图,避免每个 token 都触发重渲染闪屏
370
- // streaming 结束后由 streaming watcher 统一切换到预览
371
- const isStreaming = activeArtifact.value?.streaming
372
- if (PREVIEW_BY_DEFAULT.has(l) && isStreaming) {
373
- viewMode.value = 'code'
374
- } else if (PREVIEW_BY_DEFAULT.has(l) && !isStreaming) {
375
- viewMode.value = 'preview'
376
- computePreviewContent(code, l)
377
- nextTick(() => renderPreview())
378
- } else {
379
- computePreviewContent(code, l)
380
- if (viewMode.value === 'preview') {
381
- nextTick(() => renderPreview())
382
- }
383
- }
384
- }
385
- },
386
- { immediate: true }
387
- )
388
-
389
- // artifact 切换时重置状态
390
- watch(() => activeArtifact.value?.id, (newId, oldId) => {
391
- if (newId !== oldId) {
392
- copied.value = false
393
- highlightedHtml.value = ''
394
- replReady.value = false
395
- if (_debounceTimer) { clearTimeout(_debounceTimer); _debounceTimer = null }
396
- const art = activeArtifact.value
397
- if (!art) return
398
- if (isVueSfc.value) {
399
- // 切换到已完成的 vue artifact:直接初始化 repl
400
- if (art.code && !art.streaming) {
401
- initReplStore(art.code)
402
- } else {
403
- // 还在 streaming:先高亮展示代码片段
404
- highlight(art.code || '', 'vue')
405
- }
406
- return
407
- }
408
- // mermaid 等 streaming 中先显示代码,结束后再切预览
409
- viewMode.value = (PREVIEW_BY_DEFAULT.has(art.lang) && !art.streaming) ? 'preview' : 'code'
410
- if (art.code && art.lang) {
411
- highlight(art.code, art.lang)
412
- computePreviewContent(art.code, art.lang)
413
- if (viewMode.value === 'preview') nextTick(() => renderPreview())
414
- } else {
415
- computePreviewContent('', art.lang)
416
- }
417
- }
418
- })
419
-
420
- // streaming 结束:强制用完整代码刷新;vue sfc 此时才初始化 repl
421
- watch(
422
- () => activeArtifact.value?.streaming,
423
- (streaming) => {
424
- if (streaming === false && activeArtifact.value?.code) {
425
- if (_debounceTimer) { clearTimeout(_debounceTimer); _debounceTimer = null }
426
- const { code, lang: l } = activeArtifact.value
427
- if (isVueSfc.value) {
428
- // 代码完整了,现在才初始化 repl,切换到分屏
429
- initReplStore(code)
430
- } else {
431
- highlight(code, l)
432
- // mermaid/svg/math 等:streaming 结束后才切换到预览视图并渲染
433
- if (PREVIEW_BY_DEFAULT.has(l)) {
434
- viewMode.value = 'preview'
435
- computePreviewContent(code, l)
436
- nextTick(() => renderPreview())
437
- }
438
- }
439
- }
440
- }
441
- )
442
-
443
- // 主题切换重新高亮(onUnmounted 时断开,避免内存泄漏)
444
- const themeObs = new MutationObserver(() => {
445
- if (activeArtifact.value && !isVueSfc.value) {
446
- highlight(activeArtifact.value.code, activeArtifact.value.lang)
447
- computePreviewContent(activeArtifact.value.code, activeArtifact.value.lang)
448
- }
449
- })
450
- themeObs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
451
- onUnmounted(() => {
452
- themeObs.disconnect()
453
- if (_debounceTimer) clearTimeout(_debounceTimer)
454
- })
455
-
456
- // ── 预览渲染 ──────────────────────────────────────────────────────
457
- async function renderPreview() {
458
- const art = activeArtifact.value
459
- if (!art) return
460
-
461
- if (isMermaid.value && mermaidRef.value) {
462
- const { default: mermaid } = await import('mermaid')
463
- mermaid.initialize({
464
- startOnLoad: false,
465
- suppressErrorRendering: true,
466
- theme: isDarkTheme() ? 'dark' : 'default',
467
- fontFamily: "Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, 微软雅黑, Arial, sans-serif",
468
- })
469
- const id = 'mer-' + Date.now()
470
- try {
471
- const { svg } = await mermaid.render(id, sanitizeMermaidCode(art.code))
472
- // 检查是否返回了错误 SVG
473
- const tmp = document.createElement('div'); tmp.innerHTML = svg
474
- const isErr = tmp.querySelector('svg')?.getAttribute('aria-roledescription') === 'error'
475
- if (isErr) throw new Error('mermaid error svg')
476
- if (mermaidRef.value) mermaidRef.value.innerHTML = svg
477
- // 存储 SVG 供放大预览使用
478
- mermaidSvgHtml.value = svg
479
- } catch (e) {
480
- // 静默降级:不暴露 mermaid 内部报错,显示原始代码
481
- console.warn('[mermaid] render failed:', e.message)
482
- if (mermaidRef.value) {
483
- mermaidRef.value.innerHTML = `<pre style="margin:0;padding:16px;font-size:12px;font-family:monospace;white-space:pre-wrap;color:var(--text-secondary);">${art.code.replace(/</g,'&lt;')}</pre>`
484
- }
485
- mermaidSvgHtml.value = ''
486
- }
487
- return
488
- }
489
-
490
- if (isHtmlPreviewable.value && iframeRef.value) {
491
- const doc = buildDoc(art.code, art.lang)
492
- const blob = new Blob([doc], { type: 'text/html' })
493
- const url = URL.createObjectURL(blob)
494
- iframeRef.value.src = url
495
- setTimeout(() => URL.revokeObjectURL(url), 8000)
496
- }
497
- }
498
-
499
- function buildDoc(code, l) {
500
- if (l === 'html') return code
501
- if (l === 'css')
502
- return `<!DOCTYPE html><html><head><style>body{font-family:sans-serif;padding:20px;background:#fff;color:#333}${code}</style></head><body><div class="demo">示例内容</div></body></html>`
503
- if (l === 'javascript' || l === 'js')
504
- return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:monospace;padding:16px;background:#1a1a2e;color:#e2e8f0;}</style></head><body><script type="module">\n${code}\n<\/script></body></html>`
505
- if (l === 'jsx' || l === 'tsx')
506
- return `<!DOCTYPE html><html><head><meta charset="utf-8">
507
- <script crossorigin src="${REACT_RUNTIME_BASE}/react.min.js"><\/script>
508
- <script crossorigin src="${REACT_RUNTIME_BASE}/react-dom.min.js"><\/script>
509
- <script src="${REACT_RUNTIME_BASE}/babel.min.js"><\/script>
510
- <style>body{font-family:sans-serif;padding:16px;margin:0;}</style>
511
- </head><body><div id="root"></div>
512
- <script type="text/babel">
513
- ${code}
514
- const el=document.getElementById('root');
515
- if(typeof App!=='undefined')ReactDOM.createRoot(el).render(React.createElement(App))
516
- <\/script></body></html>`
517
- return `<!DOCTYPE html><html><body><pre style="padding:16px;font-family:monospace;">${code.replace(/</g,'&lt;')}</pre></body></html>`
518
- }
519
-
520
- function toggleView() {
521
- if (!canPreview.value) return
522
- if (viewMode.value === 'code') {
523
- viewMode.value = 'preview'
524
- nextTick(() => renderPreview())
525
- } else {
526
- viewMode.value = 'code'
527
- }
528
- }
529
-
530
- function copyCode() {
531
- navigator.clipboard?.writeText(activeArtifact.value?.code || '').catch(() => {})
532
- copied.value = true
533
- setTimeout(() => { copied.value = false }, 2000)
534
- // 埋点:采纳 - 复制 Artifact
535
- tracker.contentCopy({
536
- source: 'artifact_panel',
537
- lang: activeArtifact.value?.lang || '',
538
- })
539
- }
540
-
541
- function downloadFile() {
542
- const art = activeArtifact.value
543
- if (!art?.code) return
544
- const blob = new Blob([art.code], { type: 'text/plain;charset=utf-8' })
545
- const url = URL.createObjectURL(blob)
546
- const a = document.createElement('a')
547
- a.href = url
548
- a.download = art.fileName || `artifact.${art.lang || 'txt'}`
549
- document.body.appendChild(a)
550
- a.click()
551
- document.body.removeChild(a)
552
- setTimeout(() => URL.revokeObjectURL(url), 3000)
553
- // 埋点:采纳 - 下载文件
554
- tracker.fileDownload({ lang: art.lang || '' })
555
- }
556
-
557
- // ── markdown 预览区代码块点击委托(复制按钮)────────────────────
558
- function onMarkdownClick(e) {
559
- // 复用 useMarkdown 的事件委托,处理 [data-action="copy"] 点击
560
- // 预览按钮(data-action="preview")在 markdown artifact 中不启用,传 null 即可
561
- handleCodeAction(e, null)
562
- }
563
-
564
- // ── 光标位置追踪(点击代码区域时更新行列号)────────────────────
565
- const cursorLine = ref(1)
566
- const cursorCol = ref(1)
567
-
568
- function onCodeClick(e) {
569
- // 从点击的 .line 元素找到行号
570
- const lineEl = e.target.closest('.line')
571
- if (!lineEl) return
572
- const pre = lineEl.closest('pre')
573
- if (!pre) return
574
- const lines = [...pre.querySelectorAll('.line')]
575
- const lineIdx = lines.indexOf(lineEl)
576
- if (lineIdx === -1) return
577
- cursorLine.value = lineIdx + 1
578
- // 列号:根据点击位置粗略估算(基于字符宽度)
579
- cursorCol.value = 1
580
- }
581
- </script>
582
-
583
- <style scoped>
584
- /* ── 面板容器 ── */
585
- .artifact-panel {
586
- width: 46%;
587
- min-width: 360px;
588
- max-width: 640px;
589
- flex-shrink: 0;
590
- display: flex;
591
- flex-direction: column;
592
- border-left: 1px solid var(--border-subtle);
593
- background: var(--bg-primary);
594
- transition: background 0.3s;
595
- overflow: hidden;
596
- }
597
-
598
- /* 滑入动画 */
599
- .panel-slide-enter-active { transition: width 0.22s cubic-bezier(0.4,0,0.2,1), opacity 0.18s; }
600
- .panel-slide-leave-active { transition: width 0.18s cubic-bezier(0.4,0,1,1), opacity 0.12s; }
601
- .panel-slide-enter-from, .panel-slide-leave-to { width: 0 !important; opacity: 0; overflow: hidden; }
602
-
603
- /* ── Tab 栏(对标 Claude 顶部文件切换条)── */
604
- .panel-tabbar {
605
- display: flex;
606
- align-items: center;
607
- border-bottom: 1px solid var(--border-subtle);
608
- background: var(--bg-secondary);
609
- flex-shrink: 0;
610
- min-height: 40px;
611
- overflow: hidden;
612
- }
613
- .panel-tabbar__scroll {
614
- display: flex;
615
- align-items: center;
616
- flex: 1;
617
- overflow-x: auto;
618
- scrollbar-width: none;
619
- }
620
- .panel-tabbar__scroll::-webkit-scrollbar { display: none; }
621
-
622
- .panel-filetab {
623
- display: inline-flex;
624
- align-items: center;
625
- gap: 5px;
626
- padding: 0 14px;
627
- height: 40px;
628
- font-size: 12px;
629
- font-family: 'Cascadia Code', 'Consolas', monospace;
630
- color: var(--text-muted);
631
- background: transparent;
632
- border: none;
633
- border-bottom: 2px solid transparent;
634
- cursor: pointer;
635
- white-space: nowrap;
636
- transition: color 0.12s, border-color 0.12s, background 0.12s;
637
- flex-shrink: 0;
638
- }
639
- .panel-filetab:hover {
640
- color: var(--text-secondary);
641
- background: var(--border-subtle);
642
- }
643
- .panel-filetab.active {
644
- color: var(--text-primary);
645
- border-bottom-color: var(--brand-500);
646
- background: var(--bg-primary);
647
- }
648
- .tab-name { max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
649
- .tab-streaming-dot {
650
- width: 5px; height: 5px; border-radius: 50%;
651
- background: #4ade80; flex-shrink: 0;
652
- animation: pulseSlow 1.2s ease infinite;
653
- }
654
-
655
- .panel-tabbar__actions {
656
- display: flex;
657
- align-items: center;
658
- gap: 2px;
659
- padding: 0 8px;
660
- border-left: 1px solid var(--border-subtle);
661
- flex-shrink: 0;
662
- }
663
- .panel-icon-btn {
664
- width: 28px; height: 28px; border-radius: 5px;
665
- border: none; background: transparent;
666
- color: var(--text-muted);
667
- display: flex; align-items: center; justify-content: center;
668
- cursor: pointer; transition: all 0.12s;
669
- }
670
- .panel-icon-btn:hover:not(:disabled) {
671
- background: var(--border-subtle);
672
- color: var(--text-primary);
673
- }
674
- .panel-icon-btn.active { color: var(--brand-400); }
675
- .panel-icon-btn:disabled { opacity: 0.3; cursor: not-allowed; }
676
-
677
- /* ── 内容区 ── */
678
- .panel-content {
679
- flex: 1;
680
- overflow: hidden;
681
- display: flex;
682
- flex-direction: column;
683
- position: relative;
684
- }
685
- .panel-view { flex: 1; overflow: auto; }
686
-
687
- /* 代码视图 */
688
- .panel-view--code { background: var(--bg-primary); }
689
-
690
- .streaming-bar {
691
- display: flex; align-items: center; gap: 7px;
692
- padding: 7px 16px;
693
- background: rgba(26,111,196,0.06);
694
- border-bottom: 1px solid rgba(26,111,196,0.15);
695
- font-size: 12px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
696
- color: var(--brand-400);
697
- flex-shrink: 0;
698
- }
699
- .streaming-bar__dot {
700
- width: 6px; height: 6px; border-radius: 50%;
701
- background: #4ade80; animation: pulseSlow 1.2s ease infinite;
702
- }
703
-
704
- /* 面板等待中(内容尚未到达时的呼吸动画)*/
705
- .panel-waiting {
706
- display: flex;
707
- align-items: center;
708
- justify-content: center;
709
- gap: 6px;
710
- height: 120px;
711
- flex-shrink: 0;
712
- }
713
- .panel-waiting__dot {
714
- width: 8px; height: 8px; border-radius: 50%;
715
- background: var(--brand-400);
716
- animation: ai-bounce 1.2s ease-in-out infinite;
717
- opacity: 0.5;
718
- }
719
- .panel-waiting__dot:nth-child(1) { animation-delay: 0s; }
720
- .panel-waiting__dot:nth-child(2) { animation-delay: 0.2s; }
721
- .panel-waiting__dot:nth-child(3) { animation-delay: 0.4s; }
722
-
723
- .shiki-wrap {
724
- height: 100%;
725
- overflow: auto;
726
- }
727
-
728
- /* ── Shiki 输出:紧凑风格 + 行号 ── */
729
- :deep(.shiki-wrap .shiki) {
730
- min-height: 100%;
731
- margin: 0 !important;
732
- border-radius: 0 !important;
733
- font-size: 12px !important;
734
- line-height: 1.5 !important;
735
- font-family: 'Cascadia Code', 'Consolas', monospace !important;
736
- padding: 0 !important;
737
- tab-size: 2;
738
- }
739
- :deep(.shiki-wrap pre) {
740
- min-height: 100%;
741
- margin: 0 !important;
742
- padding: 0 !important;
743
- counter-reset: line;
744
- }
745
- :deep(.shiki-wrap .shiki code) {
746
- display: block;
747
- padding: 14px 0;
748
- font-family: 'Cascadia Code', 'Consolas', monospace !important;
749
- /* 消灭 span.line 之间换行符产生的匿名行盒(ghost whitespace)
750
- 换行符文本节点继承 line-height 后会撑出额外行高,font-size:0 让其高度归零 */
751
- font-size: 0;
752
- }
753
- /* 每行:data-line 注入后用伪元素显示行号 */
754
- :deep(.shiki-wrap .shiki code .line) {
755
- display: block;
756
- padding: 0 16px 0 0;
757
- line-height: 20px;
758
- /* 在 code font-size:0 的环境下,.line 必须显式恢复字体大小 */
759
- font-size: 12px;
760
- }
761
- :deep(.shiki-wrap .shiki code .line::before) {
762
- counter-increment: line;
763
- content: counter(line);
764
- display: inline-block;
765
- width: 2.8em;
766
- padding-right: 1em;
767
- margin-right: 0.8em;
768
- text-align: right;
769
- color: var(--text-faint);
770
- border-right: 1px solid var(--border-subtle);
771
- user-select: none;
772
- font-size: 10px;
773
- line-height: 20px;
774
- vertical-align: top;
775
- }
776
-
777
- /* ── Fallback 行号表格 ── */
778
- :deep(.fallback-table) {
779
- width: 100%;
780
- border-collapse: collapse;
781
- font-size: 11.5px;
782
- line-height: 1.55;
783
- font-family: 'Cascadia Code', 'Consolas', monospace;
784
- padding: 14px 0;
785
- }
786
- :deep(.fallback-table .ln) {
787
- width: 2.8em;
788
- padding: 0 0.8em 0 0;
789
- text-align: right;
790
- color: var(--text-faint);
791
- border-right: 1px solid var(--border-subtle);
792
- user-select: none;
793
- font-size: 10px;
794
- vertical-align: top;
795
- white-space: nowrap;
796
- }
797
- :deep(.fallback-table .lc) {
798
- padding: 0 16px 0 0.8em;
799
- color: var(--code-color);
800
- white-space: pre;
801
- word-break: break-all;
802
- }
803
-
804
- /* 预览视图 */
805
- .panel-view--preview { background: #ffffff; }
806
- .preview-iframe { width: 100%; height: 100%; border: none; display: block; }
807
- .preview-mermaid {
808
- padding: 32px;
809
- display: flex; justify-content: center; align-items: flex-start;
810
- background: var(--bg-secondary);
811
- min-height: 100%;
812
- }
813
- .preview-svg {
814
- padding: 32px;
815
- display: flex; justify-content: center; align-items: center;
816
- min-height: 100%;
817
- background: var(--bg-secondary);
818
- }
819
- :deep(.preview-svg svg) { max-width: 100%; height: auto; }
820
- .preview-math {
821
- padding: 40px;
822
- display: flex; justify-content: center; align-items: center;
823
- background: var(--bg-secondary); min-height: 100%;
824
- }
825
- .preview-markdown {
826
- padding: 28px 36px;
827
- background: var(--bg-primary);
828
- color: var(--text-primary);
829
- font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
830
- font-size: 14px;
831
- line-height: 1.75;
832
- overflow: auto;
833
- min-height: 100%;
834
- }
835
- :deep(.preview-markdown h1) { font-size: 1.5em; font-weight: 700; margin: 0 0 12px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; border-bottom: 1px solid var(--border-subtle); padding-bottom: 8px; }
836
- :deep(.preview-markdown h2) { font-size: 1.25em; font-weight: 700; margin: 20px 0 8px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; }
837
- :deep(.preview-markdown h3) { font-size: 1.1em; font-weight: 600; margin: 16px 0 6px; }
838
- :deep(.preview-markdown h4),:deep(.preview-markdown h5),:deep(.preview-markdown h6) { font-size: 1em; font-weight: 600; margin: 12px 0 4px; }
839
- :deep(.preview-markdown p) { margin: 0 0 10px; }
840
- :deep(.preview-markdown strong) { font-weight: 600; }
841
- :deep(.preview-markdown em) { font-style: italic; }
842
- :deep(.preview-markdown code) { font-family: 'Cascadia Code', 'Consolas', monospace; font-size: 12px; background: var(--bg-tertiary); padding: 1px 5px; border-radius: 3px; }
843
- :deep(.preview-markdown pre) { margin: 10px 0; border-radius: 8px; overflow: hidden; }
844
- :deep(.preview-markdown pre code) { background: none; padding: 0; border-radius: 0; font-size: 13px; }
845
- /* Shiki 输出在 markdown 预览里的适配 */
846
- :deep(.preview-markdown .shiki) { margin: 10px 0 !important; border-radius: 8px !important; font-size: 13px !important; line-height: 1.6 !important; overflow-x: auto; padding: 14px 16px !important; }
847
- :deep(.preview-markdown .shiki code) { font-family: 'Cascadia Code', 'Consolas', monospace !important; font-size: 0; }
848
- :deep(.preview-markdown .shiki code .line) { display: block; font-size: 13px; line-height: 20px; }
849
- /* plain fallback */
850
- :deep(.preview-markdown .md-code-block) { margin: 10px 0; padding: 14px 16px; border-radius: 8px; background: var(--bg-tertiary); overflow-x: auto; }
851
- :deep(.preview-markdown .md-code-block code) { background: none; padding: 0; font-size: 13px; line-height: 1.6; color: var(--code-color); white-space: pre; }
852
- :deep(.preview-markdown blockquote) { border-left: 3px solid var(--brand-500); margin: 10px 0; padding: 6px 14px; color: var(--text-secondary); background: var(--bg-secondary); border-radius: 0 6px 6px 0; }
853
- :deep(.preview-markdown hr) { border: none; border-top: 1px solid var(--border-subtle); margin: 16px 0; }
854
- :deep(.preview-markdown li) { margin: 3px 0; }
855
- :deep(.preview-markdown a) { color: var(--brand-400); text-decoration: underline; }
856
- /* renderMarkdown 输出的代码块(.code-wrapper)在 preview-markdown 区域的样式 */
857
- :deep(.preview-markdown .code-wrapper) {
858
- margin: 10px 0;
859
- border-radius: 10px;
860
- border: 1px solid var(--border-subtle);
861
- overflow: hidden;
862
- background: var(--bg-tertiary);
863
- }
864
- :deep(.preview-markdown .cb-actions) {
865
- display: flex;
866
- align-items: center;
867
- justify-content: space-between;
868
- padding: 6px 12px;
869
- background: var(--bg-secondary);
870
- border-bottom: 1px solid var(--border-subtle);
871
- }
872
- :deep(.preview-markdown .cb-lang) {
873
- font-family: 'Cascadia Code', 'Consolas', monospace;
874
- font-size: 11px;
875
- color: var(--brand-400);
876
- letter-spacing: 0.04em;
877
- }
878
- :deep(.preview-markdown .cb-btn) {
879
- display: inline-flex;
880
- align-items: center;
881
- gap: 4px;
882
- padding: 3px 8px;
883
- font-size: 11px;
884
- font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
885
- border: 1px solid var(--border-default);
886
- border-radius: 5px;
887
- background: transparent;
888
- color: var(--text-muted);
889
- cursor: pointer;
890
- transition: all 0.15s;
891
- }
892
- :deep(.preview-markdown .cb-btn:hover) {
893
- border-color: rgba(26,111,196,0.4);
894
- color: var(--brand-400);
895
- background: var(--tag-bg);
896
- }
897
- :deep(.preview-markdown .shiki) {
898
- margin: 0 !important;
899
- border-radius: 0 0 8px 8px !important;
900
- font-size: 13px !important;
901
- line-height: 1.6 !important;
902
- overflow-x: auto !important;
903
- padding: 14px 16px !important;
904
- }
905
- :deep(.preview-markdown .shiki code) {
906
- font-family: 'Cascadia Code', 'Consolas', monospace !important;
907
- font-size: 0;
908
- }
909
- :deep(.preview-markdown .shiki code .line) {
910
- display: block;
911
- font-size: 13px;
912
- line-height: 20px;
913
- }
914
- .preview-unsupported {
915
- display: flex; flex-direction: column;
916
- align-items: center; justify-content: center;
917
- height: 100%;
918
- color: var(--text-muted);
919
- font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
920
- font-size: 14px;
921
- }
922
- .preview-error { color: #f87171; padding: 16px; font-size: 13px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; }
923
-
924
- /* ── 状态栏 ── */
925
- .panel-statusbar {
926
- display: flex;
927
- align-items: center;
928
- gap: 6px;
929
- padding: 5px 14px;
930
- border-top: 1px solid var(--border-subtle);
931
- background: var(--bg-secondary);
932
- flex-shrink: 0;
933
- font-size: 11px;
934
- font-family: 'Cascadia Code', 'Consolas', monospace;
935
- color: var(--text-muted);
936
- overflow: hidden;
937
- }
938
- .status-lang { color: var(--brand-400); }
939
- .status-sep { opacity: 0.4; }
940
- .status-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
941
- .status-streaming {
942
- display: inline-flex; align-items: center; gap: 4px;
943
- color: #4ade80;
944
- }
945
- .status-dot {
946
- width: 5px; height: 5px; border-radius: 50%;
947
- background: #4ade80; animation: pulseSlow 1.2s ease infinite;
948
- }
949
- .status-cursor { color: var(--text-muted); white-space: nowrap; }
950
-
951
- /* ── Vue SFC 分屏(@vue/repl)── */
952
- .panel-vue-repl {
953
- flex: 1;
954
- width: 100%;
955
- height: 100%;
956
- overflow: hidden;
957
- }
958
- :deep(.vue-repl) {
959
- border: none !important;
960
- border-radius: 0 !important;
961
- height: 100% !important;
962
- }
963
- </style>