feat: AI 对话等待时显示构思了多久,代码不在对话里展开
Deploy / deploy (build, debian, 22) (push) Canceled after 0s

deepseek-flash 生成代码前要思考,复杂的题第一个字要等一两分钟,之前只有三个跳动的点;
出字后又把几万字的代码一段段渲染在对话里,学生并不看代码,机房的旧机器也吃力。

- 出字前显示“AI 正在构思… 已 N 秒”,超过 20 秒提示复杂的网页要想一两分钟
- 生成中只显示文字说明和“正在写网页… 已写 N 行”;写完的回复里代码块换成
  “网页代码 N 行,在右边‘代码’里看”

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 21:09:47 +08:00
1 parent 6b2ffbb3ec
commit 9f3f7187e1
2 files changed
+78 -12

No files matched your search

+75 -12
View File
@@ -52,15 +52,26 @@
<!-- Streaming indicator --> <!-- Streaming indicator -->
<div v-if="streaming" class="message assistant"> <div v-if="streaming" class="message assistant">
<div class="message-role">AI</div> <div class="message-role">AI</div>
<div v-if="!streamingContent" class="typing-indicator"> <template v-if="!streamingContent">
<span></span><span></span><span></span> <div class="typing-indicator">
</div> <span></span><span></span><span></span>
<div </div>
v-else <div class="streaming-hint">
class="message-content" AI 正在构思… 已 {{ elapsedText }}<template v-if="elapsed >= 20">,复杂的网页要想一两分钟</template>
v-html="renderMarkdown(streamingContent)" </div>
></div> </template>
<div class="streaming-hint">AI 正在生成中…</div> <template v-else>
<div
v-if="streamParts.prose"
class="message-content"
v-html="renderMarkdown(streamParts.prose)"
></div>
<div v-if="streamParts.lines >= 0" class="code-progress">
<Icon icon="lucide:sparkles" :width="14" />
正在写网页… 已写 {{ streamParts.lines }} 行
</div>
<div v-else class="streaming-hint">AI 正在回复…</div>
</template>
</div> </div>
<RatingCard v-if="rating" :active="streaming" /> <RatingCard v-if="rating" :active="streaming" />
</div> </div>
@@ -122,7 +133,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick, computed } from "vue" import { ref, watch, nextTick, computed, onUnmounted } from "vue"
import { useStorage } from "@vueuse/core" import { useStorage } from "@vueuse/core"
import { useMessage } from "naive-ui" import { useMessage } from "naive-ui"
import { Icon } from "../../icons" import { Icon } from "../../icons"
@@ -133,6 +144,7 @@ import {
messages, messages,
streaming, streaming,
streamingContent, streamingContent,
streamStartedAt,
connected, connected,
sendPrompt, sendPrompt,
stopPrompt, stopPrompt,
@@ -238,10 +250,47 @@ async function onGuidanceGenerate(finalPrompt: string) {
sendPrompt(finalPrompt, selectedModel.value) sendPrompt(finalPrompt, selectedModel.value)
} }
function renderContent(msg: { role: string; content: string }): string { // 学生只关心成品:AI 回复里的代码块不在对话里展开(右边“代码”里能看),只说写了多少行。
return renderMarkdown(msg.content) // 按 ``` 切开,奇数段是代码;段数是偶数说明最后一个代码块还没写完
function splitCode(text: string) {
const parts = text.split("```")
const prose = parts.filter((_, i) => i % 2 === 0).join("\n\n").trim()
const code = parts.filter((_, i) => i % 2 === 1)
const lines = code.reduce((n, c) => n + c.split("\n").length - 1, 0)
return { prose, lines: code.length ? lines : -1 }
} }
const streamParts = computed(() => splitCode(streamingContent.value))
function renderContent(msg: { role: string; content: string }): string {
if (msg.role !== "assistant") return renderMarkdown(msg.content)
const { prose, lines } = splitCode(msg.content)
const note = lines >= 0 ? `<p class="code-note">网页代码 ${lines} 行,在右边“代码”里看</p>` : ""
return renderMarkdown(prose) + note
}
// 等 AI 的计时:每秒刷新一次
const now = ref(Date.now())
let ticker: ReturnType<typeof setInterval> | undefined
watch(
streaming,
(on) => {
clearInterval(ticker)
now.value = Date.now()
if (on) ticker = setInterval(() => (now.value = Date.now()), 1000)
},
{ immediate: true },
)
onUnmounted(() => clearInterval(ticker))
const elapsed = computed(() =>
streamStartedAt.value ? Math.max(0, Math.floor((now.value - streamStartedAt.value) / 1000)) : 0,
)
const elapsedText = computed(() => {
const s = elapsed.value
return s < 60 ? `${s} 秒` : `${Math.floor(s / 60)} 分 ${s % 60} 秒`
})
watch([() => messages.value.length, streamingContent], () => { watch([() => messages.value.length, streamingContent], () => {
nextTick(() => { nextTick(() => {
if (messagesRef.value) { if (messagesRef.value) {
@@ -377,6 +426,20 @@ watch([() => messages.value.length, streamingContent], () => {
} }
} }
.code-progress {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 12px;
color: var(--wp-muted);
}
.message-content :deep(.code-note) {
font-size: 12px;
color: var(--wp-muted);
}
.streaming-hint { .streaming-hint {
font-size: 11px; font-size: 11px;
color: var(--wp-faint); color: var(--wp-faint);
+3
View File
@@ -19,6 +19,8 @@ export const conversationId = ref<string>("")
export const connected = ref(false) export const connected = ref(false)
export const streaming = ref(false) export const streaming = ref(false)
export const streamingContent = ref("") export const streamingContent = ref("")
// 这次提问发出的时间:AI 先思考,第一个字可能一两分钟后才出来,界面据此显示等了多久
export const streamStartedAt = ref(0)
export const currentTaskId = ref(0) export const currentTaskId = ref(0)
// 新代码交给工作台处理:init 是连上时对话里最新的那一版,complete 是 AI 刚生成的 // 新代码交给工作台处理:init 是连上时对话里最新的那一版,complete 是 AI 刚生成的
let _codeSink: let _codeSink:
@@ -106,6 +108,7 @@ export function disconnectPrompt() {
export function sendPrompt(content: string, model: string = "") { export function sendPrompt(content: string, model: string = "") {
if (!socket.send({ type: "message", content, model })) return if (!socket.send({ type: "message", content, model })) return
streaming.value = true streaming.value = true
streamStartedAt.value = Date.now()
messages.value.push({ role: "user", content }) messages.value.push({ role: "user", content })
} }