@templmf/temp-solf-lmf 0.0.54 → 0.0.55

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 (87) hide show
  1. package/guanwang/README.md +95 -0
  2. package/guanwang/docs/changelog.md +145 -0
  3. package/guanwang/docs/doc-maintenance.md +229 -0
  4. package/guanwang/docs/product.md +181 -0
  5. package/guanwang/docs/test-cases.md +395 -0
  6. package/guanwang/docs/usage.md +291 -0
  7. package/guanwang/env.example +27 -0
  8. package/guanwang/index.html +13 -0
  9. package/guanwang/package-lock.json +3825 -0
  10. package/guanwang/package.json +32 -0
  11. package/guanwang/public/favicon.svg +4 -0
  12. package/guanwang/public/react-runtime/babel.min.js +4 -0
  13. package/guanwang/public/react-runtime/react-dom.min.js +267 -0
  14. package/guanwang/public/react-runtime/react.min.js +31 -0
  15. package/guanwang/public/vue-repl-assets/compiler-sfc.esm-browser.js +50795 -0
  16. package/guanwang/public/vue-repl-assets/runtime-dom.esm-browser.js +12758 -0
  17. package/guanwang/public/vue-repl-assets/server-renderer.esm-browser.js +8600 -0
  18. package/guanwang/public/vue-repl-assets/vue.esm-browser.js +18672 -0
  19. package/guanwang/src/App.vue +61 -0
  20. package/guanwang/src/chat-sdk/core/components/ChatBox.vue +305 -0
  21. package/guanwang/src/chat-sdk/core/components/ChatSidebar.vue +84 -0
  22. package/guanwang/src/chat-sdk/core/components/InputBar.vue +354 -0
  23. package/guanwang/src/chat-sdk/core/components/MessageBubble.vue +703 -0
  24. package/guanwang/src/chat-sdk/core/useTheme.js +31 -0
  25. package/guanwang/src/chat-sdk/features/artifact/ArtifactCard.vue +172 -0
  26. package/guanwang/src/chat-sdk/features/artifact/ArtifactPanel.vue +963 -0
  27. package/guanwang/src/chat-sdk/features/artifact/index.js +13 -0
  28. package/guanwang/src/chat-sdk/features/artifact/useArtifactStore.js +275 -0
  29. package/guanwang/src/chat-sdk/features/codepreview/CodePreview.vue +523 -0
  30. package/guanwang/src/chat-sdk/features/codepreview/index.js +7 -0
  31. package/guanwang/src/chat-sdk/features/markdown/index.js +13 -0
  32. package/guanwang/src/chat-sdk/features/markdown/useMarkdown.js +724 -0
  33. package/guanwang/src/chat-sdk/features/mermaid/MermaidZoom.vue +254 -0
  34. package/guanwang/src/chat-sdk/features/upload/FileAttachment.vue +142 -0
  35. package/guanwang/src/chat-sdk/features/upload/index.js +17 -0
  36. package/guanwang/src/chat-sdk/features/upload/useFileHandler.js +336 -0
  37. package/guanwang/src/chat-sdk/headless/api/adapters/openai.js +76 -0
  38. package/guanwang/src/chat-sdk/headless/api/chatApi.js +126 -0
  39. package/guanwang/src/chat-sdk/headless/buildSystemPrompt.js +351 -0
  40. package/guanwang/src/chat-sdk/headless/index.js +15 -0
  41. package/guanwang/src/chat-sdk/headless/useChat.js +77 -0
  42. package/guanwang/src/chat-sdk/headless/useChatDB.js +147 -0
  43. package/guanwang/src/chat-sdk/headless/useChatStore.js +529 -0
  44. package/guanwang/src/chat-sdk/index.js +79 -0
  45. package/guanwang/src/chat-sdk/modes/architect.js +27 -0
  46. package/guanwang/src/chat-sdk/modes/ask.js +26 -0
  47. package/guanwang/src/chat-sdk/modes/code.js +25 -0
  48. package/guanwang/src/chat-sdk/modes/index.js +36 -0
  49. package/guanwang/src/chat-sdk/modes/requirements.js +175 -0
  50. package/guanwang/src/chat-sdk/settings/SettingsPanel.vue +170 -0
  51. package/guanwang/src/chat-sdk/settings/index.js +9 -0
  52. package/guanwang/src/chat-sdk/settings/useSettings.js +122 -0
  53. package/guanwang/src/chat-sdk/tools/defaults.js +89 -0
  54. package/guanwang/src/chat-sdk/tools/index.js +16 -0
  55. package/guanwang/src/chat-sdk/tools/parser.js +116 -0
  56. package/guanwang/src/components/CustomCursor.vue +69 -0
  57. package/guanwang/src/components/Footer.vue +24 -0
  58. package/guanwang/src/components/LoginModal.vue +109 -0
  59. package/guanwang/src/components/Navbar.vue +193 -0
  60. package/guanwang/src/components/ThemeToggle.vue +25 -0
  61. package/guanwang/src/composables/useArtifactStore.js +253 -0
  62. package/guanwang/src/composables/useAuth.js +88 -0
  63. package/guanwang/src/composables/useChatDB.js +147 -0
  64. package/guanwang/src/composables/useCountUp.js +24 -0
  65. package/guanwang/src/composables/useFileHandler.js +345 -0
  66. package/guanwang/src/composables/useTheme.js +31 -0
  67. package/guanwang/src/config/api.js +71 -0
  68. package/guanwang/src/main.js +23 -0
  69. package/guanwang/src/router/index.js +23 -0
  70. package/guanwang/src/services/authApi.js +27 -0
  71. package/guanwang/src/services/chatApi.js +66 -0
  72. package/guanwang/src/styles/global.css +478 -0
  73. package/guanwang/src/tracker/analyze.js +73 -0
  74. package/guanwang/src/tracker/config.js +82 -0
  75. package/guanwang/src/tracker/index.js +18 -0
  76. package/guanwang/src/tracker/service.js +102 -0
  77. package/guanwang/src/tracker/useChatTracker.js +179 -0
  78. package/guanwang/src/tracker/useTracker.js +45 -0
  79. package/guanwang/src/views/ChatView.vue +65 -0
  80. package/guanwang/src/views/HomeView.vue +156 -0
  81. package/guanwang/src/views/MarketView.vue +143 -0
  82. package/guanwang/src/views/PracticesView.vue +190 -0
  83. package/guanwang/src/views/SkillsView.vue +129 -0
  84. package/guanwang/temp +19 -0
  85. package/guanwang/vite.config.js +6 -0
  86. package/package.json +1 -1
  87. package/guanwang copy.zip +0 -0
@@ -0,0 +1,703 @@
1
+ <template>
2
+ <div class="msg-row" :class="isUser ? 'msg-user' : 'msg-ai'" @click="onBubbleClick">
3
+ <!-- Avatar -->
4
+ <div v-if="isUser" class="msg-avatar" :style="{ background: avatarBg }">{{ userInitial }}</div>
5
+ <div v-else class="msg-avatar msg-avatar--ai">
6
+ <Bot :size="14" color="var(--brand-400)" :stroke-width="1.75" />
7
+ </div>
8
+
9
+ <!-- Bubble -->
10
+ <div class="msg-bubble" :class="{
11
+ 'msg-bubble--user': isUser,
12
+ 'msg-bubble--ai': !isUser,
13
+ 'msg-bubble--error': msg.error,
14
+ }">
15
+ <div v-if="msg.error" class="msg-error-header">
16
+ <AlertCircle :size="13" /> 请求出错
17
+ </div>
18
+
19
+ <!-- 用户消息:文字 + 附件缩略图 -->
20
+ <template v-if="isUser">
21
+ <!-- 附件缩略图行 -->
22
+ <div v-if="msg.attachments && msg.attachments.length > 0" class="msg-attachments">
23
+ <div
24
+ v-for="att in msg.attachments"
25
+ :key="att.id"
26
+ class="msg-att-item"
27
+ :title="att.fileName"
28
+ >
29
+ <!-- 图片:显示缩略图 -->
30
+ <img
31
+ v-if="att.preview"
32
+ :src="att.preview"
33
+ class="msg-att-thumb"
34
+ :alt="att.fileName"
35
+ />
36
+ <!-- 非图片:图标 + 文件名 -->
37
+ <div v-else class="msg-att-file">
38
+ <FileText :size="14" />
39
+ <span>{{ att.fileName }}</span>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <!-- 文字内容 -->
44
+ <div v-if="msg.content" class="msg-text">{{ msg.content }}</div>
45
+ </template>
46
+
47
+ <!-- AI 消息:双区渲染 -->
48
+ <template v-else>
49
+ <!-- 等待阶段:streaming=true 且 content 为空时,显示呼吸动画 -->
50
+ <div v-if="msg.streaming && !msg.content" class="ai-waiting">
51
+ <span class="ai-waiting__dot" />
52
+ <span class="ai-waiting__dot" />
53
+ <span class="ai-waiting__dot" />
54
+ </div>
55
+
56
+ <div ref="closedRef" class="msg-md msg-md-closed" />
57
+ <!-- ArtifactCard 通过 createApp 原地挂载到 closedRef 内的占位符节点 -->
58
+ <div ref="tailRef" class="msg-md msg-md-tail" />
59
+
60
+ <!-- 原始文本调试面板(VITE_SHOW_RAW_TEXT=true 时显示)-->
61
+ <div v-if="showRawText && msg.content" class="raw-text-panel">
62
+ <button class="raw-text-toggle" @click="rawExpanded = !rawExpanded">
63
+ <span class="raw-text-toggle__icon">{{ rawExpanded ? '▾' : '▸' }}</span>
64
+ <span>原始文本</span>
65
+ <span class="raw-text-toggle__len">{{ msg.content.length }} 字符</span>
66
+ </button>
67
+ <pre v-if="rawExpanded" class="raw-text-content">{{ msg.content }}</pre>
68
+ </div>
69
+ </template>
70
+
71
+ <!-- 流式光标(附加在 tail 区后面)-->
72
+ <span v-if="msg.streaming && !isUser && msg.content" class="cursor-blink" />
73
+ </div>
74
+ </div>
75
+
76
+ <!-- 代码预览弹框 -->
77
+ <CodePreview
78
+ v-if="previewState.visible"
79
+ :visible="previewState.visible"
80
+ :lang="previewState.lang"
81
+ :code="previewState.code"
82
+ @close="previewState.visible = false"
83
+ />
84
+
85
+ <!-- mermaid 图表全屏放大 -->
86
+ <MermaidZoom
87
+ :visible="mermaidZoom.visible"
88
+ :svg-html="mermaidZoom.svgHtml"
89
+ @close="mermaidZoom.visible = false"
90
+ />
91
+ </template>
92
+
93
+ <script setup>
94
+ import { ref, shallowRef, triggerRef, computed, watch, nextTick, reactive, onUnmounted, createApp, defineComponent, h } from 'vue'
95
+ import { Bot, AlertCircle, FileText } from 'lucide-vue-next'
96
+ import CodePreview from '../../features/codepreview/CodePreview.vue'
97
+ import MermaidZoom from '../../features/mermaid/MermaidZoom.vue'
98
+ import ArtifactCard from '../../features/artifact/ArtifactCard.vue'
99
+ import {
100
+ renderMarkdown,
101
+ createStreamingParser,
102
+ postRender,
103
+ handleCodeAction,
104
+ onThemeChange,
105
+ offThemeChange,
106
+ clearBlocksByMessage,
107
+ } from '../../features/markdown/useMarkdown.js'
108
+ import { useArtifactStore, STORE_KEY } from '../../features/artifact/useArtifactStore.js'
109
+ import { useChatTracker } from '../../../tracker/useChatTracker.js'
110
+
111
+ const props = defineProps({
112
+ msg: { type: Object, required: true },
113
+ user: { type: Object, default: null },
114
+ })
115
+
116
+ // ── 原始文本调试面板(.env: VITE_SHOW_RAW_TEXT=true)────────────
117
+ const showRawText = import.meta.env.VITE_SHOW_RAW_TEXT === 'true'
118
+ const rawExpanded = ref(false)
119
+
120
+ // ── 头像 ──────────────────────────────────────────────────────────
121
+ const AVATAR_COLORS = ['#1A6FC4', '#0891b2', '#059669', '#7c3aed', '#b45309']
122
+ const isUser = computed(() => props.msg.role === 'user')
123
+ const userInitial = computed(() => (props.user?.name || props.user?.username || '?')[0].toUpperCase())
124
+ const avatarBg = computed(() => AVATAR_COLORS[(props.user?.id || 0) % AVATAR_COLORS.length])
125
+
126
+ // ── Artifact Store 注入 ───────────────────────────────────────────
127
+ const artifactStore = useArtifactStore()
128
+ // 当前消息关联的 artifacts
129
+ const msgArtifacts = computed(() =>
130
+ artifactStore.byMessage.value.get(props.msg.id) || []
131
+ )
132
+
133
+ // 每个 artId 对应一个 createApp 实例,组件卸载时统一 unmount,防止内存泄漏
134
+ const mountedApps = new Map()
135
+
136
+ // ── DOM refs ──────────────────────────────────────────────────────
137
+ const closedRef = ref(null)
138
+ const tailRef = ref(null)
139
+ const previewState = reactive({ visible: false, lang: '', code: '', svgHtml: '' })
140
+ const mermaidZoom = reactive({ visible: false, svgHtml: '' })
141
+
142
+ // ── 渲染上下文:messageId + artifactStore,传给 useMarkdown ────────
143
+ const renderCtx = {
144
+ messageId: props.msg.id,
145
+ artifactStore: {
146
+ shouldBeArtifact: artifactStore.shouldBeArtifact,
147
+ preRegister: (opts) => artifactStore.preRegister(opts),
148
+ register: (opts) => artifactStore.register(opts),
149
+ updateCode: (id, code) => artifactStore.updateCode(id, code),
150
+ markDone: (id) => artifactStore.markDone(id),
151
+ },
152
+ }
153
+
154
+ // ── 流式解析器(每条 AI 消息独享)────────────────────────────────
155
+ const parser = createStreamingParser(renderCtx)
156
+
157
+ // 上一次的 closedHtml —— 只有发生变化时才更新 DOM(避免抖动)
158
+ let lastClosedHtml = ''
159
+
160
+ // ── 流式渲染调度 ─────────────────────────────────────────────────
161
+ // useChatStore 层已做自适应节流(弱网16ms/强网80ms),
162
+ // MessageBubble 层只需用 rAF 把渲染对齐到浏览器绘制时机,无需额外节流。
163
+ let rafId = null
164
+ let pendingContent = null
165
+
166
+ function scheduleRender(content) {
167
+ pendingContent = content
168
+ if (rafId) return
169
+ rafId = requestAnimationFrame(() => {
170
+ rafId = null
171
+ const c = pendingContent
172
+ pendingContent = null
173
+ renderStreaming(c)
174
+ })
175
+ }
176
+
177
+ // ── 流式渲染:双区增量 patch ──────────────────────────────────────
178
+ // closed 区:代码块/段落完整关闭后才更新一次(低频,O(新增块))
179
+ // tail 区:正在输入的文字用增量 textContent patch,不重建 DOM(零回流)
180
+ let lastTailText = '' // 上一帧 tail 区的纯文本,用于增量对比
181
+
182
+ function renderStreaming(content) {
183
+ const { closedHtml, tailHtml } = parser.parse(content)
184
+
185
+ // closed 区:只在内容真正变化时才 patch
186
+ if (closedHtml !== lastClosedHtml) {
187
+ lastClosedHtml = closedHtml
188
+ if (closedRef.value) {
189
+ // 记录已渲染的 mermaid 图的 data-mermaid hash,innerHTML 重建后恢复标记,避免重复渲染抖动
190
+ const renderedHashes = new Set(
191
+ [...closedRef.value.querySelectorAll('.mermaid-chart[data-rendered]')]
192
+ .map(el => el.dataset.mermaid)
193
+ )
194
+ closedRef.value.innerHTML = closedHtml
195
+ if (renderedHashes.size > 0) {
196
+ closedRef.value.querySelectorAll('.mermaid-chart[data-mermaid]').forEach(el => {
197
+ if (renderedHashes.has(el.dataset.mermaid)) el.dataset.rendered = '1'
198
+ })
199
+ }
200
+ postRender(closedRef.value).then(replaceArtifactPlaceholders)
201
+ }
202
+ }
203
+
204
+ // tail 区:用 innerHTML 更新,但跳过内容相同的帧(强网下 content 可能多帧不变)
205
+ if (tailRef.value && tailHtml !== lastTailText) {
206
+ lastTailText = tailHtml
207
+ tailRef.value.innerHTML = tailHtml
208
+ scanTailPlaceholders()
209
+ }
210
+ }
211
+
212
+ function scanTailPlaceholders() {
213
+ // 流式期间 tailRef 里的占位符只是 hint,等 closedRef 里出现后再原地挂载
214
+ // 此处无需操作,避免流式期间频繁触发 createApp
215
+ }
216
+
217
+ // ── 全量渲染(流式结束后切换) ────────────────────────────────────
218
+ async function renderFull(content) {
219
+ if (rafId) { cancelAnimationFrame(rafId); rafId = null }
220
+ lastClosedHtml = ''
221
+ lastTailText = ''
222
+ parser.reset()
223
+
224
+ // innerHTML 重写前先 unmount 旧的 ArtifactCard 实例
225
+ // 否则 mountedApps 里的旧 artId 记录会导致新占位符被跳过
226
+ mountedApps.forEach(app => app.unmount())
227
+ mountedApps.clear()
228
+
229
+ const html = renderMarkdown(content, renderCtx)
230
+ if (closedRef.value) closedRef.value.innerHTML = html
231
+ if (tailRef.value) tailRef.value.innerHTML = ''
232
+
233
+ await nextTick()
234
+ if (closedRef.value) await postRender(closedRef.value, { skipMermaid: false })
235
+ // 把 artifact 占位符节点替换为 Vue 挂载点(触发 ArtifactCard 渲染)
236
+ replaceArtifactPlaceholders()
237
+ }
238
+
239
+ // 找到 closedRef 里的 .artifact-placeholder 节点,
240
+ // 用 createApp 原地挂载 ArtifactCard,位置完全对应代码块在文档流中的位置
241
+ // 已挂载的节点(mountedApps 里有记录)跳过,避免重复挂载
242
+ function replaceArtifactPlaceholders() {
243
+ if (!closedRef.value) return
244
+ const placeholders = closedRef.value.querySelectorAll('.artifact-placeholder[data-art-id]')
245
+ placeholders.forEach(el => {
246
+ const artId = el.dataset.artId
247
+ if (!artId || mountedApps.has(artId)) return
248
+
249
+ // 把占位 div 改为挂载容器(保留尺寸占位,避免替换时布局跳变)
250
+ const container = document.createElement('div')
251
+ container.className = 'artifact-card-mount'
252
+ container.style.display = 'contents' // scoped CSS 无法作用于动态插入节点,改为内联
253
+ // 用 replaceWith 保持文档流位置不变
254
+ el.replaceWith(container)
255
+
256
+ // 创建独立 Vue app 挂载 ArtifactCard,传入 artifactStore 作为依赖
257
+ const app = createApp(defineComponent({
258
+ setup() {
259
+ // 响应式追踪:直接从 store 里取 artifact,保证流式更新时卡片数据同步
260
+ const artifact = computed(() =>
261
+ artifactStore.artifactsMap.value.get(artId) || null
262
+ )
263
+ return () => artifact.value
264
+ ? h(ArtifactCard, { artifact: artifact.value })
265
+ : null
266
+ }
267
+ }))
268
+ // 用与父组件相同的 Symbol key provide,让 ArtifactCard 内的 inject 能正常工作
269
+ app.provide(STORE_KEY, artifactStore)
270
+ app.mount(container)
271
+ mountedApps.set(artId, app)
272
+ })
273
+ }
274
+
275
+ // ── 监听消息内容变化 ──────────────────────────────────────────────
276
+ watch(
277
+ () => props.msg.content,
278
+ (val) => {
279
+ if (!val || isUser.value) return
280
+ if (props.msg.streaming) {
281
+ scheduleRender(val)
282
+ }
283
+ // 流式期间不做全量渲染;结束时由 streaming watch 触发
284
+ },
285
+ { immediate: true }
286
+ )
287
+
288
+ // 流式结束 → 全量渲染完成后再标记 artifact 为已完成
289
+ // 必须 await renderFull:内部的 renderMarkdown → buildCodeBlock → register 会写入最终完整代码
290
+ // markDone 必须在 register 完成之后调用,否则 streaming 状态会被提前清除
291
+ watch(
292
+ () => props.msg.streaming,
293
+ async (streaming) => {
294
+ if (!streaming && !isUser.value) {
295
+ await renderFull(props.msg.content)
296
+ // renderFull 完成后,artifact store 已拿到完整代码,此时再 markDone
297
+ const arts = artifactStore.byMessage.value.get(props.msg.id) || []
298
+ arts.forEach(art => artifactStore.markDone(art.id))
299
+ }
300
+ }
301
+ )
302
+
303
+ // 主题切换 → 重新渲染(使用模块级单例 Observer,避免 N 条消息创建 N 个 Observer)
304
+ function onTheme() {
305
+ if (!isUser.value && props.msg.content) renderFull(props.msg.content)
306
+ }
307
+ onThemeChange(onTheme)
308
+
309
+ onUnmounted(() => {
310
+ // 取消主题订阅
311
+ offThemeChange(onTheme)
312
+ // 取消 rAF
313
+ if (rafId) cancelAnimationFrame(rafId)
314
+ // 清理本条消息的代码块缓存
315
+ clearBlocksByMessage(props.msg.id)
316
+ // 卸载所有原地挂载的 ArtifactCard 实例,防止内存泄漏
317
+ mountedApps.forEach(app => app.unmount())
318
+ mountedApps.clear()
319
+ })
320
+
321
+ // ── 代码块点击委托 ────────────────────────────────────────────────
322
+ const tracker = useChatTracker()
323
+
324
+ function onBubbleClick(e) {
325
+ // 拦截复制按钮:记录采纳行为后再执行原有逻辑
326
+ const btn = e.target.closest('[data-action]')
327
+ if (btn?.dataset.action === 'copy') {
328
+ tracker.contentCopy({
329
+ source: 'code_block',
330
+ lang: btn.closest('[data-lang]')?.dataset.lang || '',
331
+ })
332
+ }
333
+
334
+ handleCodeAction(e, ({ lang, code, svgHtml }) => {
335
+ if (lang === 'mermaid-svg' && svgHtml) {
336
+ mermaidZoom.svgHtml = svgHtml
337
+ mermaidZoom.visible = true
338
+ } else {
339
+ previewState.lang = lang
340
+ previewState.code = code
341
+ previewState.visible = true
342
+ }
343
+ })
344
+ }
345
+ </script>
346
+
347
+ <style scoped>
348
+ /* ── 布局 ── */
349
+ .msg-row { display: flex; gap: 10px; animation: fadeUp 0.25s ease; }
350
+ .msg-user { flex-direction: row-reverse; }
351
+ .msg-ai { flex-direction: row; }
352
+
353
+ /* ── 头像 ── */
354
+ .msg-avatar {
355
+ width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
356
+ display: flex; align-items: center; justify-content: center;
357
+ color: white; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; font-weight: 700; font-size: 12.6px;
358
+ }
359
+ .msg-avatar--ai {
360
+ background: var(--bg-tertiary);
361
+ border: 1px solid var(--border-default);
362
+ color: unset;
363
+ }
364
+
365
+ /* ── 气泡 ── */
366
+ .msg-bubble {
367
+ max-width: 80%;
368
+ padding: 10px 14px;
369
+ font-size: 14px;
370
+ line-height: 1.65;
371
+ position: relative;
372
+ }
373
+ .msg-bubble--user {
374
+ background: var(--brand-500); color: white;
375
+ border-radius: 12px 4px 12px 12px;
376
+ font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
377
+ white-space: pre-wrap; word-break: break-word;
378
+ }
379
+ .msg-bubble--ai {
380
+ background: var(--msg-ai-bg); color: var(--msg-ai-color);
381
+ border: 1px solid var(--border-subtle);
382
+ border-radius: 4px 12px 12px 12px;
383
+ min-width: 60px;
384
+ }
385
+ .msg-bubble--error {
386
+ background: rgba(248,113,113,0.08) !important;
387
+ color: #f87171 !important;
388
+ border-color: rgba(248,113,113,0.25) !important;
389
+ }
390
+ .msg-text { word-break: break-word; }
391
+ .msg-error-header { display: flex; align-items: center; gap: 6px; font-size: 12px; margin-bottom: 6px; color: #f87171; }
392
+
393
+ /* ── 流式光标 ── */
394
+ .cursor-blink {
395
+ display: inline-block; width: 2px; height: 14px;
396
+ background: currentColor; margin-left: 2px; vertical-align: middle;
397
+ animation: blink 1s step-end infinite;
398
+ }
399
+
400
+ /* ── 流式尾部段落(无闪烁过渡) ── */
401
+ :deep(.streaming-tail) {
402
+ margin: 0;
403
+ color: var(--msg-ai-color);
404
+ font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
405
+ line-height: 1.65;
406
+ white-space: pre-wrap;
407
+ word-break: break-word;
408
+ }
409
+
410
+ /* ── 流式代码块 badge ── */
411
+ :deep(.cb-streaming-badge) {
412
+ display: inline-flex; align-items: center; gap: 5px;
413
+ font-size: 11px; color: var(--brand-400); font-family: 'Cascadia Code', 'Consolas', monospace;
414
+ }
415
+ :deep(.cb-dot) {
416
+ width: 6px; height: 6px; border-radius: 50%; background: #4ade80;
417
+ animation: pulseSlow 1.5s ease infinite;
418
+ }
419
+
420
+ /* ── Markdown 通用 ── */
421
+ :deep(.msg-md) { font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; word-break: break-word; overflow-wrap: anywhere; }
422
+ :deep(.msg-md-closed + .msg-md-tail) { margin-top: 0; }
423
+
424
+ /* 标题 */
425
+ :deep(.msg-md h1) { font-size: 1.3em; font-weight: 700; margin: 14px 0 8px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; color: var(--text-primary); border-bottom: 1px solid var(--border-subtle); padding-bottom: 6px; }
426
+ :deep(.msg-md h2) { font-size: 1.15em; font-weight: 700; margin: 12px 0 6px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; color: var(--text-primary); }
427
+ :deep(.msg-md h3) { font-size: 1.05em; font-weight: 600; margin: 10px 0 5px; color: var(--text-primary); }
428
+ :deep(.msg-md h1:first-child),:deep(.msg-md h2:first-child),:deep(.msg-md h3:first-child) { margin-top: 0; }
429
+ :deep(.msg-md p) { margin: 0 0 8px; }
430
+ :deep(.msg-md p:last-child) { margin-bottom: 0; }
431
+ :deep(.msg-md ul), :deep(.msg-md ol) { padding-left: 1.4em; margin: 6px 0 8px; }
432
+ :deep(.msg-md li) { margin: 3px 0; line-height: 1.6; }
433
+ :deep(.msg-md li > p) { margin: 0; }
434
+ :deep(.msg-md blockquote) { margin: 8px 0; padding: 8px 14px; border-left: 3px solid var(--brand-500); background: var(--bg-tertiary); border-radius: 0 8px 8px 0; color: var(--text-secondary); font-style: italic; }
435
+ :deep(.msg-md a) { color: var(--brand-400); text-decoration: underline; text-underline-offset: 2px; }
436
+ :deep(.msg-md a:hover) { color: var(--brand-500); }
437
+ :deep(.msg-md hr) { border: none; border-top: 1px solid var(--border-subtle); margin: 12px 0; }
438
+ :deep(.msg-md table) { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px; }
439
+ :deep(.msg-md th) { background: var(--bg-tertiary); padding: 7px 12px; font-weight: 600; color: var(--text-primary); border: 1px solid var(--border-subtle); }
440
+ :deep(.msg-md td) { padding: 6px 12px; border: 1px solid var(--border-subtle); color: var(--text-secondary); }
441
+ :deep(.msg-md tr:nth-child(even) td) { background: var(--bg-secondary); }
442
+ :deep(.msg-md img) { max-width: 100%; border-radius: 8px; margin: 8px 0; display: block; }
443
+
444
+ /* ── 行内代码 ── */
445
+ :deep(.inline-code) {
446
+ font-family: 'Cascadia Code', 'Consolas', monospace; font-size: 12px;
447
+ padding: 2px 6px; border-radius: 4px;
448
+ background: var(--code-bg); color: var(--code-color);
449
+ border: 1px solid var(--border-subtle);
450
+ }
451
+
452
+ /* ── 代码块容器 ── */
453
+ :deep(.code-wrapper) {
454
+ margin: 10px 0; border-radius: 10px;
455
+ border: 1px solid var(--border-subtle); overflow: hidden;
456
+ background: var(--bg-tertiary);
457
+ }
458
+ :deep(.code-streaming) {
459
+ border-color: rgba(26,111,196,0.3);
460
+ box-shadow: 0 0 0 1px rgba(26,111,196,0.1);
461
+ }
462
+
463
+ /* 操作栏 */
464
+ :deep(.cb-actions) {
465
+ display: flex; align-items: center; justify-content: space-between;
466
+ padding: 6px 12px; background: var(--bg-secondary);
467
+ border-bottom: 1px solid var(--border-subtle);
468
+ }
469
+ :deep(.cb-lang) {
470
+ font-family: 'Cascadia Code', 'Consolas', monospace; font-size: 11px;
471
+ color: var(--brand-400); letter-spacing: 0.04em;
472
+ }
473
+ :deep(.cb-btn) {
474
+ display: inline-flex; align-items: center; gap: 4px;
475
+ padding: 3px 8px; font-size: 11px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
476
+ border: 1px solid var(--border-default); border-radius: 5px;
477
+ background: transparent; color: var(--text-muted); cursor: pointer; transition: all 0.15s;
478
+ }
479
+ :deep(.cb-btn:hover) { border-color: rgba(26,111,196,0.4); color: var(--brand-400); background: var(--tag-bg); }
480
+
481
+ /* Shiki 输出 */
482
+ :deep(.shiki) { margin: 0 !important; border-radius: 0 0 8px 8px !important; font-size: 13px !important; line-height: 1.6 !important; overflow-x: auto !important; padding: 14px 16px !important; }
483
+ :deep(.shiki code) { font-family: 'Cascadia Code', 'Consolas', monospace !important; }
484
+ :deep(.shiki-live .shiki) { border-radius: 0 !important; }
485
+
486
+ /* 降级 pre */
487
+ :deep(.shiki-pre) { margin: 0; padding: 14px 16px; font-family: 'Cascadia Code', 'Consolas', monospace; font-size: 13px; line-height: 1.6; overflow-x: auto; color: var(--code-color); }
488
+
489
+ /* Mermaid */
490
+ :deep(.mermaid-container) { padding: 16px; display: flex; justify-content: center; }
491
+ :deep(.mermaid-chart) { width: 100%; display: flex; justify-content: center; align-items: center; }
492
+ :deep(.mermaid-chart svg) { max-width: 100%; height: auto; }
493
+ /* mermaid-error 不再使用,保留空规则兼容旧引用 */
494
+ :deep(.mermaid-error) { display: none; }
495
+ /* 降级:渲染失败时显示原始代码 */
496
+ :deep(.mermaid-raw) { margin: 0; padding: 12px 16px; font-size: 12px; font-family: 'Cascadia Code', 'Consolas', monospace; color: var(--text-secondary); white-space: pre-wrap; word-break: break-all; background: transparent; }
497
+
498
+ /* 渲染前占位动画,避免高度为 0 → SVG 高度的突变 */
499
+ :deep(.mermaid-pending) { min-height: 120px; }
500
+ :deep(.mermaid-placeholder) {
501
+ display: flex; align-items: center; gap: 6px;
502
+ }
503
+ :deep(.mermaid-placeholder__dot) {
504
+ width: 8px; height: 8px; border-radius: 50%;
505
+ background: var(--brand-400); opacity: 0.5;
506
+ animation: ai-bounce 1.2s ease-in-out infinite;
507
+ }
508
+ :deep(.mermaid-placeholder__dot:nth-child(1)) { animation-delay: 0s; }
509
+ :deep(.mermaid-placeholder__dot:nth-child(2)) { animation-delay: 0.2s; }
510
+ :deep(.mermaid-placeholder__dot:nth-child(3)) { animation-delay: 0.4s; }
511
+
512
+ /* Math / KaTeX */
513
+ :deep(.math-display) { padding: 16px; display: flex; justify-content: center; overflow-x: auto; }
514
+ :deep(.katex-display) { margin: 0 !important; }
515
+
516
+ /* ── 用户消息附件 ── */
517
+ .msg-attachments {
518
+ display: flex;
519
+ flex-wrap: wrap;
520
+ gap: 6px;
521
+ margin-bottom: 8px;
522
+ }
523
+ .msg-att-item { border-radius: 8px; overflow: hidden; }
524
+ .msg-att-thumb {
525
+ display: block;
526
+ width: 120px;
527
+ height: 90px;
528
+ object-fit: cover;
529
+ border-radius: 8px;
530
+ border: 1px solid rgba(255,255,255,0.2);
531
+ }
532
+ .msg-att-file {
533
+ display: flex;
534
+ align-items: center;
535
+ gap: 6px;
536
+ padding: 6px 10px;
537
+ background: rgba(255,255,255,0.15);
538
+ border-radius: 8px;
539
+ font-size: 12px;
540
+ font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
541
+ color: rgba(255,255,255,0.9);
542
+ max-width: 180px;
543
+ }
544
+ .msg-att-file span {
545
+ white-space: nowrap;
546
+ overflow: hidden;
547
+ text-overflow: ellipsis;
548
+ }
549
+
550
+ /* ── ArtifactCard 原地挂载容器 ── */
551
+ :deep(.artifact-card-mount) { display: contents; }
552
+
553
+ /* ── 流式 Artifact Loading 卡片 ── */
554
+ :deep(.artifact-streaming-card) {
555
+ display: inline-flex;
556
+ align-items: center;
557
+ gap: 10px;
558
+ padding: 9px 14px 9px 10px;
559
+ margin: 6px 0;
560
+ border-radius: 10px;
561
+ border: 1px solid rgba(26,111,196,0.35);
562
+ background: var(--tag-bg);
563
+ max-width: 300px;
564
+ animation: fadeUp 0.2s ease;
565
+ }
566
+ :deep(.asc-icon) {
567
+ width: 30px; height: 30px; border-radius: 7px; flex-shrink: 0;
568
+ background: rgba(26,111,196,0.15);
569
+ border: 1px solid rgba(26,111,196,0.25);
570
+ display: flex; align-items: center; justify-content: center;
571
+ color: var(--brand-400);
572
+ }
573
+ :deep(.asc-body) {
574
+ display: flex; flex-direction: column; gap: 3px; min-width: 0;
575
+ }
576
+ :deep(.asc-name) {
577
+ font-size: 12px; font-weight: 500;
578
+ font-family: 'Cascadia Code', 'Consolas', monospace;
579
+ color: var(--text-primary);
580
+ white-space: nowrap;
581
+ }
582
+ :deep(.asc-sub) {
583
+ display: flex; align-items: center; gap: 5px;
584
+ font-size: 10px; font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
585
+ }
586
+ :deep(.asc-lang) { color: var(--brand-400); font-family: 'Cascadia Code', 'Consolas', monospace; }
587
+ :deep(.asc-dot) {
588
+ width: 3px; height: 3px; border-radius: 50%;
589
+ background: var(--text-muted); flex-shrink: 0;
590
+ }
591
+ :deep(.asc-gen) {
592
+ color: #4ade80;
593
+ display: flex; align-items: center; gap: 4px;
594
+ }
595
+ :deep(.asc-gen::before) {
596
+ content: '';
597
+ display: inline-block;
598
+ width: 5px; height: 5px; border-radius: 50%;
599
+ background: #4ade80;
600
+ animation: pulseSlow 1.2s ease infinite;
601
+ }
602
+
603
+ /* ── AI 等待中呼吸动画(content 为空时) ── */
604
+ .ai-waiting {
605
+ display: flex;
606
+ align-items: center;
607
+ gap: 4px;
608
+ padding: 4px 2px;
609
+ min-height: 22px;
610
+ }
611
+ .ai-waiting__dot {
612
+ width: 7px;
613
+ height: 7px;
614
+ border-radius: 50%;
615
+ background: var(--brand-400);
616
+ animation: ai-bounce 1.2s ease-in-out infinite;
617
+ opacity: 0.7;
618
+ }
619
+ .ai-waiting__dot:nth-child(1) { animation-delay: 0s; }
620
+ .ai-waiting__dot:nth-child(2) { animation-delay: 0.2s; }
621
+ .ai-waiting__dot:nth-child(3) { animation-delay: 0.4s; }
622
+ @keyframes ai-bounce {
623
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
624
+ 30% { transform: translateY(-5px); opacity: 1; }
625
+ }
626
+
627
+ /* ── 文件生成提示条(流式期间显示在气泡里) ── */
628
+ :deep(.artifact-gen-hint) {
629
+ display: inline-flex;
630
+ align-items: center;
631
+ gap: 7px;
632
+ padding: 5px 10px;
633
+ margin: 4px 0;
634
+ border-radius: 6px;
635
+ background: var(--bg-secondary);
636
+ border: 1px solid var(--border-subtle);
637
+ font-size: 12px;
638
+ font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
639
+ color: var(--text-muted);
640
+ animation: hint-breathe 1.8s ease-in-out infinite;
641
+ }
642
+ :deep(.artifact-gen-hint__dot) {
643
+ width: 6px;
644
+ height: 6px;
645
+ border-radius: 50%;
646
+ background: #4ade80;
647
+ flex-shrink: 0;
648
+ animation: pulseSlow 1.2s ease infinite;
649
+ }
650
+ :deep(.artifact-gen-hint__text) {
651
+ white-space: nowrap;
652
+ }
653
+ @keyframes hint-breathe {
654
+ 0%, 100% { opacity: 0.6; }
655
+ 50% { opacity: 1; }
656
+ }
657
+
658
+ /* ── 原始文本调试面板 ── */
659
+ .raw-text-panel {
660
+ margin-top: 10px;
661
+ border: 1px dashed rgba(250,204,21,0.35);
662
+ border-radius: 7px;
663
+ overflow: hidden;
664
+ background: rgba(250,204,21,0.04);
665
+ }
666
+ .raw-text-toggle {
667
+ display: flex;
668
+ align-items: center;
669
+ gap: 6px;
670
+ width: 100%;
671
+ padding: 5px 10px;
672
+ background: transparent;
673
+ border: none;
674
+ cursor: pointer;
675
+ color: #fbbf24;
676
+ font-size: 11px;
677
+ font-family: 'Cascadia Code', 'Consolas', monospace;
678
+ text-align: left;
679
+ }
680
+ .raw-text-toggle:hover { background: rgba(250,204,21,0.06); }
681
+ .raw-text-toggle__icon { font-size: 10px; flex-shrink: 0; }
682
+ .raw-text-toggle__len {
683
+ margin-left: auto;
684
+ opacity: 0.5;
685
+ font-size: 10px;
686
+ }
687
+ .raw-text-content {
688
+ margin: 0;
689
+ padding: 10px 12px;
690
+ border-top: 1px dashed rgba(250,204,21,0.2);
691
+ font-family: 'Cascadia Code', 'Consolas', monospace;
692
+ font-size: 11.5px;
693
+ line-height: 1.55;
694
+ color: #fde68a;
695
+ white-space: pre-wrap;
696
+ word-break: break-all;
697
+ max-height: 320px;
698
+ overflow-y: auto;
699
+ }
700
+ /* ── Mermaid 全屏放大 ── */
701
+ /* mermaid zoom 样式已移至 MermaidZoom.vue */
702
+
703
+ </style>