fix
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled

This commit is contained in:
2026-03-31 02:21:40 -06:00
parent 3c721224c8
commit 85e1681017
9 changed files with 46 additions and 141 deletions

View File

@@ -157,14 +157,14 @@ export const Submission = {
html?: string html?: string
css?: string css?: string
js?: string js?: string
conversationId?: string prompt?: string
}, },
) { ) {
const { conversationId, ...rest } = code const { prompt, ...rest } = code
const data = { const data = {
task_id: taskId, task_id: taskId,
...rest, ...rest,
conversation_id: conversationId || null, prompt: prompt || null,
} }
const res = await http.post("/submission/", data) const res = await http.post("/submission/", data)
return res.data return res.data
@@ -250,6 +250,15 @@ export const Prompt = {
) )
).data ).data
}, },
async getMessagesByUserTask(
taskId: number,
userId: number,
): Promise<PromptMessage[]> {
const convs = await this.listConversations(taskId, userId)
if (!convs.length) return []
return this.getMessages(convs[0].id)
},
} }
export const Helper = { export const Helper = {

View File

@@ -101,6 +101,7 @@ async function submit() {
html: splitResult.value.html, html: splitResult.value.html,
css: splitResult.value.css, css: splitResult.value.css,
js: splitResult.value.js, js: splitResult.value.js,
prompt: promptText.value.trim() || undefined,
}) })
message.success("提交成功") message.success("提交成功")
promptText.value = "" promptText.value = ""

View File

@@ -1,10 +1,6 @@
<template> <template>
<div class="prompt-panel"> <div class="prompt-panel">
<div class="messages" ref="messagesRef"> <div class="messages" ref="messagesRef">
<div v-if="historyLoading" class="history-loading">
<n-spin size="small" />
<span>加载历史记录</span>
</div>
<div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]"> <div v-for="(msg, i) in messages" :key="i" :class="['message', msg.role]">
<div class="message-role">{{ msg.role === "user" ? "我" : "AI" }}</div> <div class="message-role">{{ msg.role === "user" ? "我" : "AI" }}</div>
<div class="message-content" v-html="renderContent(msg)"></div> <div class="message-content" v-html="renderContent(msg)"></div>
@@ -31,15 +27,7 @@
: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="flex-end" align="center" style="margin-top: 8px">
<n-button
text
size="small"
@click="newConversation"
:disabled="streaming"
>
新对话
</n-button>
<n-button <n-button
type="primary" type="primary"
:loading="streaming" :loading="streaming"
@@ -61,8 +49,6 @@ import {
streaming, streaming,
streamingContent, streamingContent,
sendPrompt, sendPrompt,
newConversation,
historyLoading,
} from "../store/prompt" } from "../store/prompt"
const input = ref("") const input = ref("")
@@ -285,13 +271,4 @@ watch([() => messages.value.length, streamingContent], () => {
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
} }
.history-loading {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
color: #aaa;
font-size: 13px;
justify-content: center;
}
</style> </style>

View File

@@ -129,7 +129,8 @@ import type { PromptMessage } from "../../utils/type"
const props = defineProps<{ const props = defineProps<{
show: boolean show: boolean
conversationId?: string userId: number
taskId: number
}>() }>()
defineEmits<{ "update:show": [value: boolean] }>() defineEmits<{ "update:show": [value: boolean] }>()
@@ -188,37 +189,22 @@ const selectedPageHtml = computed(() => {
return `<!DOCTYPE html><html><head><meta charset="utf-8">${style}</head><body>${round.html}${script}</body></html>` return `<!DOCTYPE html><html><head><meta charset="utf-8">${style}</head><body>${round.html}${script}</body></html>`
}) })
watch( async function loadMessages() {
() => props.conversationId, if (!props.userId || !props.taskId) return
async (id) => { loading.value = true
if (!id || !props.show) return messages.value = []
loading.value = true selectedRound.value = 0
messages.value = [] try {
selectedRound.value = 0 messages.value = await Prompt.getMessagesByUserTask(props.taskId, props.userId)
try { const last = rounds.value.length - 1
messages.value = await Prompt.getMessages(id) if (last >= 0) selectedRound.value = last
const last = rounds.value.length - 1 } finally {
if (last >= 0) selectedRound.value = last loading.value = false
} finally { }
loading.value = false }
}
},
)
watch( watch(
() => props.show, () => [props.show, props.userId, props.taskId] as const,
async (visible) => { ([visible]) => { if (visible) loadMessages() },
if (!visible || !props.conversationId) return
loading.value = true
messages.value = []
selectedRound.value = 0
try {
messages.value = await Prompt.getMessages(props.conversationId)
const last = rounds.value.length - 1
if (last >= 0) selectedRound.value = last
} finally {
loading.value = false
}
},
) )
</script> </script>

View File

@@ -36,7 +36,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
select: [id: string] select: [id: string]
delete: [row: SubmissionOut, parentId: string] delete: [row: SubmissionOut, parentId: string]
"show-chain": [conversationId: string] "show-chain": [userId: number, taskId: number]
}>() }>()
const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge) const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge)
@@ -80,23 +80,21 @@ const subColumns = computed((): DataTableColumn<SubmissionOut>[] => [
? [ ? [
{ {
title: "提示词", title: "提示词",
key: "conversation_id", key: "prompt",
width: 70, width: 70,
render: (r: SubmissionOut) => { render: (r: SubmissionOut) =>
if (!r.conversation_id) return "-" h(
return h(
NButton, NButton,
{ {
text: true, text: true,
type: "primary", type: "primary",
onClick: (e: Event) => { onClick: (e: Event) => {
e.stopPropagation() e.stopPropagation()
emit("show-chain", r.conversation_id!) emit("show-chain", r.userid, r.task_id)
}, },
}, },
() => "查看", () => "查看",
) ),
},
} as DataTableColumn<SubmissionOut>, } as DataTableColumn<SubmissionOut>,
] ]
: []), : []),

View File

@@ -82,10 +82,8 @@ import { authed, roleSuper } from "../store/user"
import { import {
connectPrompt, connectPrompt,
disconnectPrompt, disconnectPrompt,
conversationId,
streaming, streaming,
setOnCodeComplete, setOnCodeComplete,
loadHistory,
} from "../store/prompt" } from "../store/prompt"
const route = useRoute() const route = useRoute()
@@ -106,16 +104,13 @@ async function loadChallenge() {
taskId.value = data.task_ptr taskId.value = data.task_ptr
challengeContent.value = await marked.parse(data.content, { async: true }) challengeContent.value = await marked.parse(data.content, { async: true })
if (!authed.value) return if (!authed.value) return
loadHistory(data.task_ptr) // HTTP preload — async, non-blocking connectPrompt(data.task_ptr)
connectPrompt(data.task_ptr) // WebSocket — synchronous open
setOnCodeComplete(async (code) => { setOnCodeComplete(async (code) => {
if (!conversationId.value) return
try { try {
await Submission.create(taskId.value, { await Submission.create(taskId.value, {
html: code.html ?? "", html: code.html ?? "",
css: code.css ?? "", css: code.css ?? "",
js: code.js ?? "", js: code.js ?? "",
conversationId: conversationId.value,
}) })
message.success("已自动提交本次对话生成的代码") message.success("已自动提交本次对话生成的代码")
} catch { } catch {

View File

@@ -95,7 +95,8 @@
<ChainModal <ChainModal
v-model:show="chainModal" v-model:show="chainModal"
:conversation-id="chainConversationId" :user-id="chainUserId"
:task-id="chainTaskId"
/> />
</template> </template>
@@ -147,7 +148,8 @@ const js = computed(() => submission.value.js)
// Modal 状态 // Modal 状态
const codeModal = ref(false) const codeModal = ref(false)
const chainModal = ref(false) const chainModal = ref(false)
const chainConversationId = ref<string | undefined>() const chainUserId = ref<number>(0)
const chainTaskId = ref<number>(0)
// 展开行 // 展开行
const expandedKeys = ref<string[]>([]) const expandedKeys = ref<string[]>([])
@@ -189,8 +191,9 @@ async function clearAllFlags() {
query.flag = null query.flag = null
} }
function showChain(conversationId: string) { function showChain(userId: number, taskId: number) {
chainConversationId.value = conversationId chainUserId.value = userId
chainTaskId.value = taskId
chainModal.value = true chainModal.value = true
} }
@@ -206,7 +209,7 @@ const columns: DataTableColumn<SubmissionOut>[] = [
loading: expandedLoading.has(row.id), loading: expandedLoading.has(row.id),
onSelect: (id) => getSubmissionByID(id), onSelect: (id) => getSubmissionByID(id),
onDelete: (r, parentId) => handleDelete(r, parentId), onDelete: (r, parentId) => handleDelete(r, parentId),
"onShow-chain": (id) => showChain(id), "onShow-chain": (userId, taskId) => showChain(userId, taskId),
}), }),
}, },
{ {

View File

@@ -1,9 +1,6 @@
import { ref } from "vue" import { ref } from "vue"
import { WS_BASE_URL } from "../utils/const" import { WS_BASE_URL } from "../utils/const"
import { html, css, js } from "./editors" import { html, css, js } from "./editors"
import { Prompt } from "../api"
import type { PromptMessage as RawMessage } from "../utils/type"
import { user } from "./user"
export interface PromptMessage { export interface PromptMessage {
role: "user" | "assistant" role: "user" | "assistant"
@@ -16,8 +13,6 @@ export const messages = ref<PromptMessage[]>([])
export const conversationId = ref<string>("") export const conversationId = ref<string>("")
export const connected = ref(false) export const connected = ref(false)
export const streaming = ref(false) export const streaming = ref(false)
export const historyLoading = ref(false)
let _historyLoadId = 0
export const streamingContent = ref("") export const streamingContent = ref("")
let _onCodeComplete: let _onCodeComplete:
| ((code: { | ((code: {
@@ -96,8 +91,6 @@ export function connectPrompt(taskId: number) {
} }
export function disconnectPrompt() { export function disconnectPrompt() {
_historyLoadId++ // cancel any in-flight loadHistory
historyLoading.value = false // reset here; finally block won't (loadId mismatch)
if (ws) { if (ws) {
ws.close() ws.close()
ws = null ws = null
@@ -110,68 +103,12 @@ export function disconnectPrompt() {
_onCodeComplete = null _onCodeComplete = null
} }
export async function loadHistory(taskId: number) {
const loadId = ++_historyLoadId
historyLoading.value = true
try {
const convs = await Prompt.listConversations(taskId)
console.log(
"[loadHistory] convs:",
convs.map((c: any) => ({
id: c.id,
is_active: c.is_active,
message_count: c.message_count,
username: c.username,
})),
"user.username:",
user.username,
)
if (loadId !== _historyLoadId) return // navigated away, abort
const active = convs.find(
(c: { is_active: boolean; message_count: number; username: string }) =>
c.is_active && c.message_count > 0 && c.username === user.username,
)
console.log("[loadHistory] active:", active)
if (!active) return
const raw: RawMessage[] = await Prompt.getMessages(active.id)
console.log("[loadHistory] raw messages:", raw.length)
if (loadId !== _historyLoadId) return // navigated away, abort
// Only apply if nothing has arrived via WebSocket yet
if (messages.value.length > 0) return
conversationId.value = active.id
messages.value = raw.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
code:
m.role === "assistant"
? { html: m.code_html, css: m.code_css, js: m.code_js }
: undefined,
created: m.created,
}))
// Apply code from last assistant message to editors
const lastAssistant = [...messages.value]
.reverse()
.find((m) => m.role === "assistant" && m.code)
if (lastAssistant?.code) {
applyCode(lastAssistant.code)
}
} catch {
// 静默失败,不影响 WebSocket 正常流程
} finally {
if (loadId === _historyLoadId) historyLoading.value = false
}
}
export function sendPrompt(content: string) { export function sendPrompt(content: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return if (!ws || ws.readyState !== WebSocket.OPEN) return
messages.value.push({ role: "user", content }) messages.value.push({ role: "user", content })
ws.send(JSON.stringify({ type: "message", content })) ws.send(JSON.stringify({ type: "message", content }))
} }
export function newConversation() {
if (!ws || ws.readyState !== WebSocket.OPEN) return
ws.send(JSON.stringify({ type: "new_conversation" }))
}
function applyCode(code: { function applyCode(code: {
html: string | null html: string | null

View File

@@ -77,7 +77,6 @@ export interface SubmissionOut {
task_title: string task_title: string
score: number score: number
my_score: number my_score: number
conversation_id?: string
flag?: FlagType flag?: FlagType
zone?: "featured" | "low" | "pending" | null zone?: "featured" | "low" | "pending" | null
submit_count: number submit_count: number