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