feat: 重新设计“帮我写清楚”弹框

- 标题从“调教提示词”改成“帮我把要求写清楚”,开头说明怎么用
- 学生每次发的标成“你的要求 · 第 N 版”,AI 的回复标成“AI 的建议”
- 输入框明确是“你的要求”;按钮改成“让 AI 再看看”和“用这段生成网页”,
  没改过的要求不会重复发;AI 回复时不能直接生成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 12:22:53 +08:00
1 parent 51ee246a6f
commit 1ba31844a9
1 file changed
+109 -81
+109 -81
View File
@@ -2,83 +2,75 @@
<n-modal <n-modal
:show="isOpen" :show="isOpen"
preset="card" preset="card"
title="调教提示词" title="帮我把要求写清楚"
style="width: min(560px, 92vw)" style="width: min(640px, 92vw)"
:mask-closable="!streaming" :mask-closable="!streaming"
@update:show="onModalClose" @update:show="onModalClose"
> >
<div class="guidance-body"> <p class="intro">
<div class="guidance-messages" ref="messagesRef"> AI 会看看你的要求哪里还不够清楚,给你提建议。照着建议改好下面的要求,再让它看看;
<div 觉得可以了就点“用这段生成网页”。
v-for="(msg, i) in messages" </p>
:key="msg.id ?? i"
class="guidance-msg" <div ref="messagesRef" class="messages">
:class="msg.role" <div v-for="(msg, i) in messages" :key="msg.id ?? i" :class="['msg', msg.role]">
> <div class="who">
<div class="msg-role">{{ msg.role === "user" ? "你" : "AI" }}</div> {{ msg.role === "user" ? `你的要求 · 第 ${versionOf(i)} 版` : "AI 的建议" }}
<div class="msg-content" v-html="renderMarkdown(msg.content)"></div> </div>
<div class="content markdown-body" v-html="renderMarkdown(msg.content)"></div>
</div> </div>
<div v-if="streaming" class="guidance-msg assistant"> <div v-if="streaming" class="msg assistant">
<div class="msg-role">AI</div> <div class="who">AI 的建议</div>
<div v-if="!displayStreamingContent" class="typing-indicator"> <div v-if="!displayStreamingContent" class="typing">
<span></span><span></span><span></span> <span></span><span></span><span></span>
</div> </div>
<div <div
v-else v-else
class="msg-content" class="content markdown-body"
v-html="renderMarkdown(displayStreamingContent)" v-html="renderMarkdown(displayStreamingContent)"
></div> ></div>
</div> </div>
</div> </div>
<div v-if="isReady" class="ready-hint"> <div v-if="isReady" class="ready">
<Icon icon="lucide:check-circle" :width="16" /> <Icon icon="lucide:check-circle" :width="16" />
提示词已优化,可以生成代码了! AI 觉得要求已经写清楚了,可以生成网页了
</div> </div>
<div class="guidance-input"> <div class="draft">
<div class="draft-label">你的要求</div>
<n-input <n-input
v-model:value="draftPrompt" v-model:value="draftPrompt"
type="textarea" type="textarea"
:autosize="{ minRows: 2, maxRows: 5 }" :autosize="{ minRows: 3, maxRows: 6 }"
placeholder="修改你的提示词后发送..." placeholder="按 AI 的建议把要求改得更具体:要什么内容、什么颜色和布局、点了以后会怎样……"
:disabled="streaming" :disabled="streaming"
@keydown.enter.exact.prevent="send" @keydown.enter.exact.prevent="send"
/> />
<n-flex justify="space-between" align="center" style="margin-top: 8px"> <n-flex justify="space-between" align="center" class="draft-actions">
<div> <n-flex align="center" :size="8">
<n-button <n-button v-if="streaming" size="small" @click="stopGuidance">停止</n-button>
v-if="streaming"
type="error"
size="small"
@click="stopGuidance"
>
停止
</n-button>
<n-button <n-button
v-else v-else
size="small" size="small"
:disabled="!draftPrompt.trim() || !connected" :disabled="!draftPrompt.trim() || !connected || draftPrompt.trim() === lastSent"
@click="send" @click="send"
> >
发送 让 AI 再看看
</n-button> </n-button>
</div> <span class="muted">Enter 发送,Shift + Enter 换行</span>
</n-flex>
<n-button <n-button
:type="isReady ? 'primary' : 'default'" :type="isReady ? 'primary' : 'default'"
size="small" :disabled="!draftPrompt.trim() || streaming"
:disabled="!draftPrompt.trim()"
@click="generate" @click="generate"
> >
<template #icon> <template #icon><Icon icon="lucide:play" :width="14" /></template>
<Icon icon="lucide:play" :width="14" /> 用这段生成网页
</template>
生成代码
</n-button> </n-button>
</n-flex> </n-flex>
</div> </div>
</div>
</n-modal> </n-modal>
</template> </template>
@@ -106,13 +98,23 @@ const emit = defineEmits<{
const draftPrompt = ref("") const draftPrompt = ref("")
const messagesRef = ref<HTMLElement>() const messagesRef = ref<HTMLElement>()
// 第几版要求:只数学生发的
function versionOf(index: number) {
return messages.value.slice(0, index + 1).filter((m) => m.role === "user").length
}
// 没改过就不用再发一遍
const lastSent = computed(
() => [...messages.value].reverse().find((m) => m.role === "user")?.content.trim() ?? "",
)
const displayStreamingContent = computed(() => const displayStreamingContent = computed(() =>
streamingContent.value.replace(/^\[READY\]\n?/, "") streamingContent.value.replace(/^\[READY\]\n?/, "")
) )
function send() { function send() {
const text = draftPrompt.value.trim() const text = draftPrompt.value.trim()
if (!text || streaming.value) return if (!text || streaming.value || text === lastSent.value) return
sendGuidance(text) sendGuidance(text)
} }
@@ -141,87 +143,113 @@ watch([() => messages.value.length, streamingContent], () => {
</script> </script>
<style scoped> <style scoped>
.guidance-body { .intro {
display: flex; margin: 0 0 12px;
flex-direction: column; font-size: 13px;
gap: 12px; line-height: 1.7;
color: var(--wp-muted);
} }
.guidance-messages { .messages {
min-height: 160px; min-height: 120px;
max-height: 360px; max-height: 45vh;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
padding-right: 4px;
} }
.guidance-msg .msg-role { .who {
font-size: 12px;
font-weight: bold;
margin-bottom: 4px; margin-bottom: 4px;
font-size: 12px;
font-weight: 600;
color: var(--wp-muted); color: var(--wp-muted);
} }
.guidance-msg.user .msg-role { .msg.assistant .who {
color: var(--wp-css);
}
.guidance-msg.assistant .msg-role {
color: var(--wp-brand); color: var(--wp-brand);
} }
.guidance-msg.assistant .msg-content { .content {
background: var(--wp-css-soft);
border-left: 3px solid var(--wp-css);
padding: 8px 12px; padding: 8px 12px;
border-radius: 0 6px 6px 0; border-radius: 8px;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.7;
}
.guidance-msg.user .msg-content {
font-size: 14px;
line-height: 1.6;
color: var(--wp-ink); color: var(--wp-ink);
} }
.msg-content :deep(p) { .msg.user .content {
margin: 0; border: 1px solid var(--wp-line);
background: var(--wp-paper);
} }
.ready-hint { .msg.assistant .content {
border-left: 3px solid var(--wp-brand);
border-radius: 0 8px 8px 0;
background: var(--wp-brand-soft);
}
.content :deep(p) {
margin: 0 0 6px;
}
.content :deep(:last-child) {
margin-bottom: 0;
}
.ready {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
color: var(--wp-brand); margin-top: 12px;
font-size: 13px;
font-weight: 500;
padding: 6px 10px; padding: 6px 10px;
background: var(--wp-brand-soft);
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--wp-brand-soft); font-size: 13px;
color: var(--wp-brand);
background: var(--wp-brand-soft);
} }
.typing-indicator { .draft {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--wp-line);
}
.draft-label {
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
}
.draft-actions {
margin-top: 8px;
}
.muted {
font-size: 12px;
color: var(--wp-faint);
}
.typing {
display: flex; display: flex;
gap: 5px; gap: 5px;
padding: 8px 0; padding: 10px 12px;
} }
.typing-indicator span { .typing span {
width: 7px; width: 7px;
height: 7px; height: 7px;
border-radius: 50%; border-radius: 50%;
background: var(--wp-css); background: var(--wp-brand);
animation: bounce 1.4s ease-in-out infinite; animation: bounce 1.4s ease-in-out infinite;
} }
.typing-indicator span:nth-child(2) { .typing span:nth-child(2) {
animation-delay: 0.2s; animation-delay: 0.2s;
} }
.typing-indicator span:nth-child(3) { .typing span:nth-child(3) {
animation-delay: 0.4s; animation-delay: 0.4s;
} }