feat: 提示词链显示层级走势、每轮改了多少行;课堂总结加“怎么问的 AI”
- 提示词弹框:顶部每轮一根层级柱子,点了跳到那一轮;每轮标出代码比上一版 +/− 多少行 (没改标“代码没变”),和前面某轮一字不差的提问标“重复提问” - 课堂总结:每人最新一版平均几轮、一轮就交、最后一版用外部 AI、问过引导的比例, 以及各层级提问次数 - prompt_message 加两个覆盖索引,统计只读小列(像素画板全部班级 197 份约 0.3 ms) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f41fda3c19
commit
e06930c8f7
7 files changed
+299
-29
No files matched your search
@@ -47,6 +47,9 @@ CREATE INDEX IF NOT EXISTS "prompt_conversation_user_id_ecc418b3" ON "prompt_con
|
||||
CREATE TABLE IF NOT EXISTS "prompt_message" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "role" varchar(10) NOT NULL, "content" text NOT NULL, "code_html" text NULL, "code_css" text NULL, "code_js" text NULL, "created" datetime NOT NULL, "conversation_id" char(32) NOT NULL REFERENCES "prompt_conversation" ("id") DEFERRABLE INITIALLY DEFERRED, "prompt_level" integer NULL, "source" varchar(12) NOT NULL, "submission_id" char(32) NULL UNIQUE REFERENCES "submission_submission" ("id") DEFERRABLE INITIALLY DEFERRED);
|
||||
CREATE INDEX IF NOT EXISTS "prompt_message_conversation_id_af18dd4b" ON "prompt_message" ("conversation_id");
|
||||
CREATE INDEX IF NOT EXISTS "prompt_message_prompt_level_e461b596" ON "prompt_message" ("prompt_level");
|
||||
-- 课堂总结统计提示词链用的覆盖索引:只读小列,不碰 content / code_*
|
||||
CREATE INDEX IF NOT EXISTS "prompt_message_chain" ON "prompt_message" ("conversation_id", "created", "id", "role", "source", "prompt_level");
|
||||
CREATE INDEX IF NOT EXISTS "prompt_message_submission" ON "prompt_message" ("submission_id", "conversation_id", "created", "source") WHERE "submission_id" IS NOT NULL;
|
||||
|
||||
-- 登录用 JWT:access token 不落库,refresh token 只存 sha256
|
||||
DROP TABLE IF EXISTS "auth_session";
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getActiveConversation,
|
||||
getPrecedingUserMessage,
|
||||
insertMessage,
|
||||
promptChainStats,
|
||||
type MessageRow,
|
||||
} from "../services/prompt"
|
||||
import { pyRound } from "../services/pyround"
|
||||
@@ -473,6 +474,8 @@ router.get("/stats/:task_id", (c) => {
|
||||
present_count: studentSubs.filter((s) => s.flag === "present").length,
|
||||
classes,
|
||||
top_scored: topScored,
|
||||
// 每人最新一版是怎么问出来的
|
||||
prompts: promptChainStats([...latest.values()].map((s) => s.id)),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { all, get, run, now, newUuidHex } from "../db"
|
||||
import { Role, type User } from "../auth"
|
||||
import { pyRound } from "./pyround"
|
||||
|
||||
export interface MessageRow {
|
||||
id: number
|
||||
@@ -141,3 +142,38 @@ export function buildPromptRounds(source: MessageRow) {
|
||||
}
|
||||
return rounds
|
||||
}
|
||||
|
||||
// 课堂总结:这些提交各自是怎么问出来的(只走覆盖索引,不读提问和代码)
|
||||
export function promptChainStats(submissionIds: string[]) {
|
||||
type Slim = { conversation_id: string; created: string; id: number; role: string; source: string; prompt_level: number | null }
|
||||
const finals = all<Omit<Slim, "role" | "prompt_level">>(
|
||||
`SELECT conversation_id, created, id, source FROM prompt_message
|
||||
WHERE submission_id IN (SELECT value FROM json_each($ids))`,
|
||||
{ ids: JSON.stringify(submissionIds) },
|
||||
)
|
||||
const byConv = new Map<string, Slim[]>()
|
||||
const msgs = all<Slim>(
|
||||
`SELECT conversation_id, created, id, role, source, prompt_level FROM prompt_message
|
||||
WHERE conversation_id IN (SELECT value FROM json_each($convs))`,
|
||||
{ convs: JSON.stringify([...new Set(finals.map((f) => f.conversation_id))]) },
|
||||
)
|
||||
for (const m of msgs) byConv.set(m.conversation_id, [...(byConv.get(m.conversation_id) ?? []), m])
|
||||
|
||||
const out = { with_chain: finals.length, one_round: 0, external: 0, used_guidance: 0, avg_rounds: 0 }
|
||||
const levels = [0, 0, 0, 0, 0, 0]
|
||||
let rounds = 0
|
||||
for (const f of finals) {
|
||||
// 和 buildPromptRounds 一样:只算交的那一版之前的提问
|
||||
const asked = (byConv.get(f.conversation_id) ?? []).filter(
|
||||
(m) => m.role === "user" && (m.created < f.created || (m.created === f.created && m.id < f.id)),
|
||||
)
|
||||
const code = asked.filter((m) => m.source !== "guidance")
|
||||
rounds += code.length
|
||||
if (code.length === 1) out.one_round++
|
||||
if (f.source === "manual") out.external++
|
||||
if (code.length < asked.length) out.used_guidance++
|
||||
for (const m of code) if (m.prompt_level) levels[m.prompt_level - 1]!++
|
||||
}
|
||||
out.avg_rounds = finals.length ? pyRound(rounds / finals.length, 1) : 0
|
||||
return { ...out, levels }
|
||||
}
|
||||
@@ -645,6 +645,39 @@ describe("课堂总结", () => {
|
||||
const all = await adm("GET", `/api/submission/stats/${task}`)
|
||||
expect(all.json.top_scored.map((t: any) => t.username)).toEqual(["weba2小李", "weba1小明"])
|
||||
})
|
||||
|
||||
test("统计每人最新一版的提示词链", async () => {
|
||||
const { createConversation, insertMessage } = await import("../src/services/prompt")
|
||||
makeUser("adm", "admin")
|
||||
const task = makeTask("挑战")
|
||||
const say = (conv: string, source: string, level: number | null, submission_id?: string) => {
|
||||
const id = insertMessage({ conversation_id: conv, role: "user", source, content: "提问" })
|
||||
run(`UPDATE prompt_message SET prompt_level = $level WHERE id = $id`, { level, id })
|
||||
insertMessage({ conversation_id: conv, role: "assistant", source, content: "回答", submission_id })
|
||||
}
|
||||
// 小明:AI 对话 → 引导 → 外部 AI,交最后这版;交了之后的提问不算
|
||||
const a = makeUser("weba1小明", "normal", "a1")
|
||||
const convA = createConversation(a, task)
|
||||
say(convA, "conversation", 3)
|
||||
say(convA, "guidance", 2)
|
||||
say(convA, "manual", 6, makeSubmission(a, task))
|
||||
say(convA, "conversation", 1)
|
||||
// 小红:一轮就交
|
||||
const b = makeUser("weba1小红", "normal", "a1")
|
||||
say(createConversation(b, task), "conversation", 1, makeSubmission(b, task))
|
||||
// 小刚:手动交的,没有提示词
|
||||
makeSubmission(makeUser("weba1小刚", "normal", "a1"), task)
|
||||
|
||||
const res = await (await client("adm"))("GET", `/api/submission/stats/${task}`)
|
||||
expect(res.json.prompts).toEqual({
|
||||
with_chain: 2,
|
||||
one_round: 1,
|
||||
external: 1,
|
||||
used_guidance: 1,
|
||||
avg_rounds: 1.5,
|
||||
levels: [1, 0, 1, 0, 0, 1],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("提示词分类", () => {
|
||||
|
||||
Reference in new issue
Block a user