feat: “出问题了”三栏说清问题,交作品时写一句反思,外部提示词至少 8 个字
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
- 对话框加“出问题了”:做了什么 / 想看到什么 / 实际看到了什么拼成一句发给 AI; 只说“还是不行”这类的提醒一次,可以直接发 - “交这一版”先弹框问最满意的改动和理由(可以不写),存在 submission_reflection, 批改面板显示为“学生说”,只有老师和作者本人看得到 - 粘贴外部 AI 代码时提示词至少 8 个字,可以把几轮都贴进来 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5bdacc438d
commit
e9127318cb
12 files changed
+351
-63
No files matched your search
@@ -264,6 +264,7 @@ export const Submission = {
|
||||
prompt?: string
|
||||
},
|
||||
messageId?: number,
|
||||
reflection?: string,
|
||||
) {
|
||||
const { prompt, ...rest } = code
|
||||
const data = {
|
||||
@@ -271,6 +272,7 @@ export const Submission = {
|
||||
...rest,
|
||||
prompt: prompt || null,
|
||||
message_id: messageId ?? null,
|
||||
reflection: reflection || null,
|
||||
}
|
||||
const res = await http.post("/submission/", data)
|
||||
return res.data
|
||||
|
||||
@@ -77,63 +77,105 @@
|
||||
</div>
|
||||
<GuidancePanel @generate="onGuidanceGenerate" />
|
||||
<div class="input-area">
|
||||
<n-flex
|
||||
class="prompt-input-row"
|
||||
align="flex-start"
|
||||
:wrap="false"
|
||||
:size="8"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="input"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }"
|
||||
placeholder="用你自己的话说说想做成什么样…"
|
||||
:disabled="streaming"
|
||||
style="flex: 1; min-width: 0"
|
||||
@keydown.enter.exact.prevent="send"
|
||||
/>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button
|
||||
secondary
|
||||
:disabled="!input.trim() || !connected || !currentTaskId || streaming"
|
||||
style="flex-shrink: 0"
|
||||
@click="startGuidance"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="lucide:lightbulb" :width="16" />
|
||||
</template>
|
||||
帮我写清楚
|
||||
</n-button>
|
||||
</template>
|
||||
AI 教练会问你几个问题,帮你把想要的效果描述清楚
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
<n-flex justify="flex-end" align="center" style="margin-top: 8px">
|
||||
<n-select
|
||||
v-model:value="selectedModel"
|
||||
:options="modelOptions"
|
||||
style="width: 150px"
|
||||
:disabled="streaming"
|
||||
/>
|
||||
<n-button v-if="streaming" type="error" @click="stopPrompt">
|
||||
停止
|
||||
</n-button>
|
||||
<n-button
|
||||
v-else
|
||||
type="primary"
|
||||
:disabled="!input.trim() || !connected"
|
||||
@click="send"
|
||||
<!-- 只说“不行”“没反应”时提醒一下,可以直接发 -->
|
||||
<div v-if="vagueHint" class="vague-hint">
|
||||
<span>说清楚哪里不对,AI 更容易一次修好</span>
|
||||
<n-flex :size="4" :wrap="false">
|
||||
<n-button size="tiny" type="primary" secondary @click="openProblem">说清楚</n-button>
|
||||
<n-button size="tiny" quaternary @click="sendAnyway">直接发</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
<div v-if="problemOpen" class="problem">
|
||||
<div class="problem-title">出问题了?说清楚这三件事</div>
|
||||
<label>我做了什么</label>
|
||||
<n-input v-model:value="problem.did" placeholder="比如:点了「开始」按钮" />
|
||||
<label>想看到什么</label>
|
||||
<n-input v-model:value="problem.want" placeholder="比如:倒计时开始走" />
|
||||
<label>实际看到了什么</label>
|
||||
<n-input v-model:value="problem.saw" placeholder="比如:数字一直停在 60 不动" />
|
||||
<n-flex justify="end" :size="8" style="margin-top: 10px">
|
||||
<n-button @click="problemOpen = false">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!problem.did.trim() || !problem.saw.trim() || !connected || streaming"
|
||||
@click="sendProblem"
|
||||
>
|
||||
发给 AI
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
<template v-else>
|
||||
<n-flex
|
||||
class="prompt-input-row"
|
||||
align="flex-start"
|
||||
:wrap="false"
|
||||
:size="8"
|
||||
>
|
||||
发送
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-input
|
||||
v-model:value="input"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }"
|
||||
placeholder="用你自己的话说说想做成什么样…"
|
||||
:disabled="streaming"
|
||||
style="flex: 1; min-width: 0"
|
||||
@keydown.enter.exact.prevent="send"
|
||||
/>
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button
|
||||
secondary
|
||||
:disabled="!input.trim() || !connected || !currentTaskId || streaming"
|
||||
style="flex-shrink: 0"
|
||||
@click="startGuidance"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="lucide:lightbulb" :width="16" />
|
||||
</template>
|
||||
帮我写清楚
|
||||
</n-button>
|
||||
</template>
|
||||
AI 教练会问你几个问题,帮你把想要的效果描述清楚
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" align="center" style="margin-top: 8px">
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button quaternary :disabled="!hasVersion || streaming" @click="openProblem">
|
||||
<template #icon>
|
||||
<Icon icon="lucide:bug" :width="16" />
|
||||
</template>
|
||||
出问题了
|
||||
</n-button>
|
||||
</template>
|
||||
网页哪里不对?说清楚做了什么、想看到什么、实际看到了什么
|
||||
</n-tooltip>
|
||||
<n-flex align="center" :size="8">
|
||||
<n-select
|
||||
v-model:value="selectedModel"
|
||||
:options="modelOptions"
|
||||
style="width: 150px"
|
||||
:disabled="streaming"
|
||||
/>
|
||||
<n-button v-if="streaming" type="error" @click="stopPrompt">
|
||||
停止
|
||||
</n-button>
|
||||
<n-button
|
||||
v-else
|
||||
type="primary"
|
||||
:disabled="!input.trim() || !connected"
|
||||
@click="send"
|
||||
>
|
||||
发送
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed, onUnmounted } from "vue"
|
||||
import { ref, reactive, watch, nextTick, computed, onUnmounted } from "vue"
|
||||
import { useStorage } from "@vueuse/core"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "../../icons"
|
||||
@@ -233,6 +275,46 @@ async function deletePair(assistantMsgId: number) {
|
||||
async function send() {
|
||||
const text = input.value.trim()
|
||||
if (!text || streaming.value) return
|
||||
// 只说“不行”“没反应”,AI 只能瞎猜;提醒一次,学生选“直接发”以后这次就不再拦
|
||||
if (!skipVagueCheck && hasVersion.value && [...text].length <= 15 && VAGUE.test(text)) {
|
||||
vagueHint.value = true
|
||||
return
|
||||
}
|
||||
input.value = ""
|
||||
await props.beforeSend?.()
|
||||
sendPrompt(text, selectedModel.value)
|
||||
}
|
||||
|
||||
// “出问题了”:做了什么、想看到什么、实际看到了什么,拼成一句发给 AI
|
||||
const VAGUE =
|
||||
/不行|不对|没反应|没用|没效果|用不了|不能用|不能玩|动不了|进不去|打不开|没显示|不显示|显示不出|bug|出错|报错|错了|坏了|白屏|空白|有问题/i
|
||||
const vagueHint = ref(false)
|
||||
let skipVagueCheck = false
|
||||
const problemOpen = ref(false)
|
||||
const problem = reactive({ did: "", want: "", saw: "" })
|
||||
|
||||
watch(input, () => (vagueHint.value = false))
|
||||
|
||||
const hasVersion = computed(() => messages.value.some((m) => m.role === "assistant" && hasCode(m)))
|
||||
|
||||
function openProblem() {
|
||||
vagueHint.value = false
|
||||
problemOpen.value = true
|
||||
}
|
||||
|
||||
function sendAnyway() {
|
||||
skipVagueCheck = true
|
||||
vagueHint.value = false
|
||||
send()
|
||||
}
|
||||
|
||||
async function sendProblem() {
|
||||
const part = (s: string) => s.trim().replace(/[。,,.!!;;]+$/, "")
|
||||
const did = part(problem.did).replace(/^我/, "")
|
||||
const want = part(problem.want)
|
||||
const text = `我${did},${want ? `想看到${want},` : ""}但实际${part(problem.saw)}。`
|
||||
Object.assign(problem, { did: "", want: "", saw: "" })
|
||||
problemOpen.value = false
|
||||
input.value = ""
|
||||
await props.beforeSend?.()
|
||||
sendPrompt(text, selectedModel.value)
|
||||
@@ -447,6 +529,31 @@ watch([() => messages.value.length, streamingContent], () => {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.vague-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--wp-brand-soft);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.problem-title {
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.problem label {
|
||||
display: block;
|
||||
margin: 8px 0 4px;
|
||||
font-size: 12px;
|
||||
color: var(--wp-muted);
|
||||
}
|
||||
|
||||
.input-area {
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
|
||||
@@ -119,6 +119,11 @@
|
||||
</n-popconfirm>
|
||||
</section>
|
||||
|
||||
<section v-if="sub.reflection">
|
||||
<h4>学生说</h4>
|
||||
<div class="reflection">{{ sub.reflection }}</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<n-flex justify="space-between" align="center" class="chain-head">
|
||||
<h4>提示词 <span class="muted" v-if="chainSteps">· {{ chainSteps }} 轮</span></h4>
|
||||
@@ -479,6 +484,17 @@ h4 {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 交的时候写的反思 */
|
||||
.reflection {
|
||||
padding: 6px 10px;
|
||||
border-left: 3px solid var(--wp-brand);
|
||||
background: var(--wp-brand-soft);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chain {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
title="粘贴外部 AI 生成的代码"
|
||||
style="width: 640px; max-width: 94vw"
|
||||
>
|
||||
<n-form-item label="你在外部 AI 里用的提示词" :show-feedback="false">
|
||||
<n-form-item
|
||||
label="你在外部 AI 里说了什么"
|
||||
:validation-status="promptTooShort ? 'warning' : undefined"
|
||||
:feedback="promptTooShort ? `再写具体一点:你让 AI 做了什么、改了什么(至少 ${MIN_PROMPT} 个字)` : ''"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
placeholder="把你发给豆包、DeepSeek 等 AI 的话粘贴到这里"
|
||||
placeholder="把你发给豆包、DeepSeek 等 AI 的话粘贴到这里;说了好几轮的,每一轮都贴进来,一轮一行"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="完整的网页代码" :show-feedback="false" style="margin-top: 12px">
|
||||
@@ -30,7 +34,7 @@
|
||||
<n-button @click="show = false">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!prompt.trim() || !raw.trim()"
|
||||
:disabled="[...prompt.trim()].length < MIN_PROMPT || !raw.trim()"
|
||||
:loading="saving"
|
||||
@click="save"
|
||||
>
|
||||
@@ -41,7 +45,7 @@
|
||||
</n-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { computed, ref } from "vue"
|
||||
import { splitHtml, type Code } from "../../utils/code"
|
||||
|
||||
const show = defineModel<boolean>("show", { required: true })
|
||||
@@ -50,6 +54,12 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const prompt = ref("")
|
||||
// 以前四分之一的外部提示词只有“1”“倒计时”这样几个字,看不出学生想了什么
|
||||
const MIN_PROMPT = 8
|
||||
const promptTooShort = computed(() => {
|
||||
const n = [...prompt.value.trim()].length
|
||||
return n > 0 && n < MIN_PROMPT
|
||||
})
|
||||
const raw = ref("")
|
||||
const saving = ref(false)
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="title"
|
||||
style="width: 520px; max-width: 94vw"
|
||||
:mask-closable="!saving"
|
||||
>
|
||||
<div class="question">
|
||||
{{ hasEarlier ? "比起第一版,这一版你最满意的改动是什么?为什么这样改?" : "这一版你最满意的地方是什么?为什么?" }}
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="reflection"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
:maxlength="500"
|
||||
placeholder="比如:我把按钮从五个减到三个,因为按钮太多了看不出先点哪个"
|
||||
/>
|
||||
<n-text depth="3" class="hint">可以不写,直接交。写了的话,老师批改时能看到。</n-text>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button :disabled="saving" @click="show = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="save">交</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
|
||||
// 交作品前问一句反思:最满意的改动和理由。不拦着交,只是让学生回头看一眼自己改了什么
|
||||
const show = defineModel<boolean>("show", { required: true })
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
// 之前还有别的版本,就问“比起第一版”
|
||||
hasEarlier: boolean
|
||||
onSubmit: (reflection: string) => Promise<void>
|
||||
}>()
|
||||
|
||||
const reflection = ref("")
|
||||
const saving = ref(false)
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await props.onSubmit(reflection.value.trim())
|
||||
reflection.value = ""
|
||||
show.value = false
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.question {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--wp-ink);
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -103,6 +103,12 @@
|
||||
</template>
|
||||
</n-split>
|
||||
<PasteCodeModal v-model:show="showPaste" :on-save="savePasted" />
|
||||
<SubmitModal
|
||||
v-model:show="showSubmit"
|
||||
:title="submitTitle"
|
||||
:has-earlier="submitHasEarlier"
|
||||
:on-submit="submitChallenge"
|
||||
/>
|
||||
<n-drawer v-model:show="showHistory" :width="420" placement="right">
|
||||
<n-drawer-content title="全部历史" :native-scrollbar="false" body-content-style="padding: 0">
|
||||
<PromptHistoryPanel
|
||||
@@ -128,6 +134,7 @@ import Preview from "../editor/Preview.vue"
|
||||
import CodeTabs from "./CodeTabs.vue"
|
||||
import VersionBar, { type VersionItem } from "./VersionBar.vue"
|
||||
import PasteCodeModal from "./PasteCodeModal.vue"
|
||||
import SubmitModal from "./SubmitModal.vue"
|
||||
import { Prompt, Submission } from "../../api"
|
||||
import { useTaskCode } from "../../composables/useTaskCode"
|
||||
import {
|
||||
@@ -174,6 +181,7 @@ const selectedId = ref<number | null>(null)
|
||||
const busy = ref(false)
|
||||
const briefOpen = ref(true)
|
||||
const showPaste = ref(false)
|
||||
const showSubmit = ref(false)
|
||||
const showHistory = ref(false)
|
||||
const historyKey = ref(0)
|
||||
|
||||
@@ -308,21 +316,46 @@ function openHistory() {
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (props.kind === "challenge") {
|
||||
showSubmit.value = true
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
try {
|
||||
await props.saveWork?.({ ...code })
|
||||
message.success("作品已保存")
|
||||
} catch (err: any) {
|
||||
message.error(err.response?.data?.detail ?? "保存失败,请重试")
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitTitle = computed(() => {
|
||||
if (modified.value || !selectedId.value) return "交当前修改"
|
||||
const n = versions.value.find((v) => v.id === selectedId.value)?.n
|
||||
return n ? `交 v${n}` : "交这一版"
|
||||
})
|
||||
|
||||
// 要交的这一版之前还有版本(当前修改会存成新的一版)
|
||||
const submitHasEarlier = computed(() => {
|
||||
if (modified.value || !selectedId.value) return versions.value.length > 0
|
||||
return (versions.value.find((v) => v.id === selectedId.value)?.n ?? 1) > 1
|
||||
})
|
||||
|
||||
// 挑战:先在弹框里写一句反思(可以不写)再交;出错时抛出去,弹框不关
|
||||
async function submitChallenge(reflection: string) {
|
||||
busy.value = true
|
||||
try {
|
||||
if (props.kind === "creation") {
|
||||
await props.saveWork?.({ ...code })
|
||||
message.success("作品已保存")
|
||||
return
|
||||
}
|
||||
const id =
|
||||
modified.value || !selectedId.value ? await saveAsVersion() : selectedId.value
|
||||
await Submission.create(props.taskId, { ...code }, id)
|
||||
await Submission.create(props.taskId, { ...code }, id, reflection)
|
||||
markMessageSubmitted(id)
|
||||
const n = versions.value.find((v) => v.id === id)?.n
|
||||
message.success(n ? `已提交 v${n}` : "已提交")
|
||||
} catch (err: any) {
|
||||
message.error(err.response?.data?.detail ?? "提交失败,请重试")
|
||||
throw err
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ArrowRight,
|
||||
Award,
|
||||
BookOpen,
|
||||
Bug,
|
||||
ChartNoAxesColumn,
|
||||
Check,
|
||||
ChevronDown,
|
||||
@@ -49,6 +50,7 @@ const registry: Record<string, Component> = {
|
||||
"arrow-right": ArrowRight,
|
||||
award: Award,
|
||||
"book-open": BookOpen,
|
||||
bug: Bug,
|
||||
"bar-chart-2": ChartNoAxesColumn,
|
||||
check: Check,
|
||||
"chevron-down": ChevronDown,
|
||||
|
||||
@@ -167,6 +167,8 @@ export interface SubmissionAll {
|
||||
view_count: number
|
||||
created: Date
|
||||
modified: Date
|
||||
// 交的时候写的反思,只有老师和作者本人拿得到
|
||||
reflection?: string | null
|
||||
}
|
||||
|
||||
export interface UserTag {
|
||||
|
||||
Reference in new issue
Block a user