feat: 改版阶段 2:挑战和自由创作共用工作台,学生自己选版本提交

- 新的 Workbench:左边题目(可收起)+ AI 对话,右边预览 / 代码(可编辑),底部版本条;
  挑战页和自由创作页都用它,原来的“手动提交”“历史对话”标签换成“粘贴外部代码”和“全部历史”
- 不再每轮 AI 回复自动提交:学生点“交这一版”,可交多次,成绩仍取最高分;已交的版本不能重复点
- 手改代码、粘贴外部 AI 代码都存成版本(POST /prompt/versions,source 为 edit / manual),
  在版本条和提示词链里标“手改”“外部 AI”;它们进入 AI 上下文,AI 在手改后的代码上继续改;
  以前手动提交的回复正文为空时用代码补上;edit 的占位提问不参与提示词层级分类
- 互评从全屏弹窗改成对话里的卡片,打分前不显示作者,每个分数写明含义
- 编辑器代码按任务各存一份(useTaskCode),挑战页“清空”不再清掉教程的代码;
  提交列表“复制到编辑器”对挑战写入该任务自己的代码
- 抽出 utils/code.ts(整理代码、拆分整页 HTML);删除 RandomRatingModal、ExternalAIPanel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 23:59:24 +08:00
1 parent ff1eb2982e
commit 9c6c01eba1
29 files changed
+1748 -1069

No files matched your search

+2 -2
View File
@@ -39,7 +39,7 @@ async function callLlm(content: string): Promise<number | null> {
export async function classifyMessage(messageId: number): Promise<number | null> {
const msg = get<{ content: string }>(
`SELECT content FROM prompt_message WHERE id = $id AND role = 'user'`,
`SELECT content FROM prompt_message WHERE id = $id AND role = 'user' AND source != 'edit'`,
{ id: messageId },
)
if (!msg) return null
@@ -56,7 +56,7 @@ export async function classifyMessage(messageId: number): Promise<number | null>
export async function classifyConversationMessages(conversationId: string, force = false) {
const rows = all<{ id: number }>(
`SELECT id FROM prompt_message
WHERE conversation_id = $id AND role = 'user' ${force ? "" : "AND prompt_level IS NULL"}
WHERE conversation_id = $id AND role = 'user' AND source != 'edit' ${force ? "" : "AND prompt_level IS NULL"}
ORDER BY created, id`,
{ id: conversationId },
)
+17
View File
@@ -81,6 +81,23 @@ export function insertMessage(m: {
return Number(res.lastInsertRowid)
}
// 不是 AI 生成的版本(手改 / 粘贴外部 AI 的代码),AI 回复那一条的正文写成和 AI 输出一样的代码块,
// 后面接着和 AI 对话时,它在上下文里看到的就是这份代码
export const EDIT_PROMPT = "(手动修改了代码)"
export function codeAsMarkdown(code: {
code_html: string | null
code_css: string | null
code_js: string | null
}) {
const parts = [
code.code_css ? `<style>\n${code.code_css}\n</style>` : "",
code.code_html ?? "",
code.code_js ? `<script>\n${code.code_js}\n</script>` : "",
].filter(Boolean)
return "```html\n" + parts.join("\n\n") + "\n```"
}
export function getPrecedingUserMessage(asst: Pick<MessageRow, "id" | "conversation_id" | "created">) {
return get<MessageRow>(
`SELECT * FROM prompt_message