webgl(ui): 生成等待期加活体指示 — spinner + 计时 + 状态文案
deploy webgl / build-and-deploy (push) Successful in 1m24s
deploy webgl / build-and-deploy (push) Successful in 1m24s
gemma 首字延迟那十几秒里代码 tab 是空的,看着像坏了。现在 thinking/ writing 两个阶段都有 spinner + 计时(Ns) + 行数 + 提示,一眼看出在干活。
This commit is contained in:
@@ -30,6 +30,23 @@ const input = ref('')
|
|||||||
const busy = ref(false)
|
const busy = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 生成计时器(让等待期一眼看出在工作,尤其 gemma 首字延迟那十几秒)
|
||||||
|
const genSeconds = ref(0)
|
||||||
|
let genTimer: number | undefined
|
||||||
|
function startTimer() {
|
||||||
|
genSeconds.value = 0
|
||||||
|
const t0 = Date.now()
|
||||||
|
genTimer = window.setInterval(() => {
|
||||||
|
genSeconds.value = Math.floor((Date.now() - t0) / 1000)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
function stopTimer() {
|
||||||
|
if (genTimer !== undefined) {
|
||||||
|
clearInterval(genTimer)
|
||||||
|
genTimer = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 流式累积的原始文本(代码 tab 实时显示),以及结束后提取的可运行 HTML
|
// 流式累积的原始文本(代码 tab 实时显示),以及结束后提取的可运行 HTML
|
||||||
const code = ref('')
|
const code = ref('')
|
||||||
const runningHtml = ref('')
|
const runningHtml = ref('')
|
||||||
@@ -43,6 +60,11 @@ const iframeEl = ref<HTMLIFrameElement | null>(null)
|
|||||||
const canSend = computed(() => !busy.value && input.value.trim().length > 0)
|
const canSend = computed(() => !busy.value && input.value.trim().length > 0)
|
||||||
const hasProgram = computed(() => runningHtml.value.length > 0)
|
const hasProgram = computed(() => runningHtml.value.length > 0)
|
||||||
const lineCount = computed(() => (code.value ? code.value.split('\n').length : 0))
|
const lineCount = computed(() => (code.value ? code.value.split('\n').length : 0))
|
||||||
|
// 还没吐出第一个字符(gemma 首字延迟 / thinking 阶段)—— 最容易被误以为坏了
|
||||||
|
const thinking = computed(() => busy.value && code.value.length === 0)
|
||||||
|
const genStatus = computed(() =>
|
||||||
|
thinking.value ? `正在思考…(${genSeconds.value}s)` : `正在写代码…(${genSeconds.value}s · ${lineCount.value} 行)`,
|
||||||
|
)
|
||||||
|
|
||||||
watch([messages, busy], async () => {
|
watch([messages, busy], async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -69,6 +91,7 @@ async function send() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
messages.value.push({ role: 'user', content: prompt })
|
messages.value.push({ role: 'user', content: prompt })
|
||||||
busy.value = true
|
busy.value = true
|
||||||
|
startTimer()
|
||||||
code.value = ''
|
code.value = ''
|
||||||
tab.value = 'code' // 切到代码 tab 看它一行行生成
|
tab.value = 'code' // 切到代码 tab 看它一行行生成
|
||||||
|
|
||||||
@@ -91,6 +114,7 @@ async function send() {
|
|||||||
messages.value.push({ role: 'assistant', content: `出错了:${msg}` })
|
messages.value.push({ role: 'assistant', content: `出错了:${msg}` })
|
||||||
} finally {
|
} finally {
|
||||||
busy.value = false
|
busy.value = false
|
||||||
|
stopTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +241,7 @@ function onKeydown(e: KeyboardEvent) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="busy" class="bubble assistant typing">
|
<div v-if="busy" class="bubble assistant typing">
|
||||||
<span /><span /><span /> 正在生成…
|
<span /><span /><span /> {{ genStatus }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -264,9 +288,15 @@ function onKeydown(e: KeyboardEvent) {
|
|||||||
title="生成的 WebGL 程序"
|
title="生成的 WebGL 程序"
|
||||||
/>
|
/>
|
||||||
<div v-else class="placeholder">
|
<div v-else class="placeholder">
|
||||||
|
<template v-if="busy">
|
||||||
|
<div class="spinner" />
|
||||||
|
<p class="status">{{ genStatus }}</p>
|
||||||
|
<p class="sub">写完会自动在这里跑起来 · 切到 <b></> 代码</b> 看 AI 实时写 →</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<div class="big">▶</div>
|
<div class="big">▶</div>
|
||||||
<p v-if="busy">正在生成,写完自动在这里跑起来…</p>
|
<p>左边说个需求,生成好的程序会在这里运行。</p>
|
||||||
<p v-else>左边说个需求,生成好的程序会在这里运行。</p>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -274,7 +304,11 @@ function onKeydown(e: KeyboardEvent) {
|
|||||||
<div v-show="tab === 'code'" class="codewrap" ref="codeEl">
|
<div v-show="tab === 'code'" class="codewrap" ref="codeEl">
|
||||||
<pre v-if="code" class="code"><code>{{ code }}</code><span v-if="busy" class="caret" /></pre>
|
<pre v-if="code" class="code"><code>{{ code }}</code><span v-if="busy" class="caret" /></pre>
|
||||||
<div v-else class="placeholder small">
|
<div v-else class="placeholder small">
|
||||||
<p>这里会实时显示 AI 写的代码。</p>
|
<template v-if="busy">
|
||||||
|
<div class="spinner" />
|
||||||
|
<p class="status">{{ genStatus }}</p>
|
||||||
|
</template>
|
||||||
|
<p v-else>这里会实时显示 AI 写的代码。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -469,6 +503,19 @@ function onKeydown(e: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
.placeholder .big { font-size: 3.2rem; opacity: 0.35; }
|
.placeholder .big { font-size: 3.2rem; opacity: 0.35; }
|
||||||
.placeholder.small { position: static; height: 100%; }
|
.placeholder.small { position: static; height: 100%; }
|
||||||
|
.placeholder .status { color: var(--fg); font-size: 1rem; font-weight: 600; }
|
||||||
|
.placeholder .sub { font-size: 0.82rem; opacity: 0.8; }
|
||||||
|
.placeholder .sub b { color: var(--fg); }
|
||||||
|
.spinner {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid var(--border);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-right-color: var(--accent-2);
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
.errorbar {
|
.errorbar {
|
||||||
padding: 9px 14px;
|
padding: 9px 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user