add message history
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-05-06 07:12:52 -06:00
parent 77aca640ac
commit 2b216878ca
5 changed files with 247 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ import type {
FlagType, FlagType,
SubmissionOut, SubmissionOut,
PromptMessage, PromptMessage,
PromptHistoryItem,
TaskStatsOut, TaskStatsOut,
TaskAsset, TaskAsset,
AwardSection, AwardSection,
@@ -306,6 +307,11 @@ export const Prompt = {
return (await http.get("/prompt/conversations/", { params })).data return (await http.get("/prompt/conversations/", { params })).data
}, },
async listHistory(taskId: number): Promise<PromptHistoryItem[]> {
const res = await http.get(`/prompt/history/${taskId}`)
return res.data
},
async getMessages(conversationId: string): Promise<PromptMessage[]> { async getMessages(conversationId: string): Promise<PromptMessage[]> {
return ( return (
await http.get<PromptMessage[]>( await http.get<PromptMessage[]>(

View File

@@ -52,6 +52,9 @@ import { html, css, js } from "../../store/editors"
import { Submission } from "../../api" import { Submission } from "../../api"
const props = defineProps<{ taskId: number }>() const props = defineProps<{ taskId: number }>()
const emit = defineEmits<{
submitted: []
}>()
const message = useMessage() const message = useMessage()
const promptText = ref("") const promptText = ref("")
@@ -105,6 +108,7 @@ async function submit() {
js: splitResult.value.js, js: splitResult.value.js,
prompt: promptText.value.trim(), prompt: promptText.value.trim(),
}) })
emit("submitted")
message.success("提交成功") message.success("提交成功")
promptText.value = "" promptText.value = ""
rawCode.value = "" rawCode.value = ""

View File

@@ -0,0 +1,212 @@
<template>
<div class="history-panel">
<n-flex
class="history-toolbar"
align="center"
justify="space-between"
:wrap="false"
>
<n-text depth="3"> {{ items.length }} 条历史对话</n-text>
<n-tooltip>
<template #trigger>
<n-button
quaternary
circle
size="small"
:loading="loading"
aria-label="刷新历史对话"
@click="load(true)"
>
<template #icon>
<Icon icon="lucide:refresh-cw" />
</template>
</n-button>
</template>
刷新历史对话
</n-tooltip>
</n-flex>
<n-spin v-if="loading" class="state" />
<n-empty
v-else-if="!items.length"
class="state"
description="暂无历史对话"
/>
<n-scrollbar v-else class="history-scrollbar">
<n-flex vertical :size="12" class="history-list">
<n-card
v-for="item in items"
:key="item.assistant_message_id"
size="small"
:bordered="true"
:content-style="{ padding: 0 }"
>
<n-flex
class="history-main"
align="center"
justify="space-between"
:wrap="false"
>
<n-tag
size="small"
:type="item.source === 'manual' ? 'info' : 'success'"
>
{{ item.source === "manual" ? "手动提交" : "AI 对话" }}
</n-tag>
<n-text depth="3">
{{ parseTime(item.created, "YYYY-MM-DD HH:mm") }}
</n-text>
</n-flex>
<n-p class="prompt-text">
{{ item.prompt }}
</n-p>
<div class="thumbnail" aria-label="页面缩略图">
<iframe
v-if="item.hasPage"
title="页面缩略图"
loading="lazy"
sandbox="allow-scripts"
referrerpolicy="no-referrer"
tabindex="-1"
:srcdoc="item.previewDoc"
/>
<n-empty v-else size="small" description="未生成页面" />
</div>
</n-card>
</n-flex>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue"
import { Icon } from "@iconify/vue"
import { Prompt } from "../../api"
import type { PromptHistoryItem } from "../../utils/type"
import { parseTime } from "../../utils/helper"
import { buildPreviewDocument } from "../../utils/previewDocument"
const props = defineProps<{
taskId: number
active: boolean
assetBaseUrl?: string
refreshKey?: number
}>()
type HistoryViewItem = PromptHistoryItem & {
hasPage: boolean
previewDoc: string
}
const items = ref<HistoryViewItem[]>([])
const loading = ref(false)
let loadedTaskId = 0
function toViewItem(item: PromptHistoryItem): HistoryViewItem {
const html = item.code_html ?? ""
const css = item.code_css ?? ""
const js = item.code_js ?? ""
return {
...item,
hasPage: !!(html.trim() || css.trim() || js.trim()),
previewDoc: buildPreviewDocument({
html,
css,
js,
assetBaseUrl: props.assetBaseUrl,
}),
}
}
async function load(force = true) {
if (!props.taskId || loading.value) return
if (!force && loadedTaskId === props.taskId) return
loading.value = true
try {
const data = await Prompt.listHistory(props.taskId)
items.value = data.map(toViewItem)
loadedTaskId = props.taskId
} finally {
loading.value = false
}
}
watch(
() => [props.active, props.taskId] as const,
([active]) => {
if (active) load(false)
},
)
watch(
() => props.refreshKey,
() => {
if (props.active) load(true)
},
)
onMounted(() => {
if (props.active) load(false)
})
</script>
<style scoped>
.history-panel {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.history-toolbar {
height: 42px;
padding: 0 12px;
border-bottom: 1px solid var(--n-border-color, #efeff5);
flex-shrink: 0;
}
.state {
margin: auto;
}
.history-scrollbar {
flex: 1;
min-height: 0;
}
.history-list {
padding: 12px;
}
.history-main {
padding: 10px 12px;
}
.prompt-text {
margin: 0;
padding: 0 12px 12px;
color: #333;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.thumbnail {
height: 160px;
border-top: 1px solid var(--n-border-color, #efeff5);
background: #f7f7f9;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.thumbnail iframe {
width: 100%;
height: 100%;
border: 0;
background: #fff;
pointer-events: none;
}
</style>

View File

@@ -52,7 +52,15 @@
<PromptPanel /> <PromptPanel />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="external" tab="手动提交" display-directive="show"> <n-tab-pane name="external" tab="手动提交" display-directive="show">
<ExternalAIPanel :task-id="taskId" /> <ExternalAIPanel :task-id="taskId" @submitted="historyRefreshKey++" />
</n-tab-pane>
<n-tab-pane name="history" tab="历史对话" display-directive="show">
<PromptHistoryPanel
:task-id="taskId"
:active="activeTab === 'history'"
:asset-base-url="assetBaseUrl"
:refresh-key="historyRefreshKey"
/>
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
</div> </div>
@@ -116,6 +124,7 @@ import { marked, type MarkedOptions } from "marked"
import copyFn from "copy-text-to-clipboard" import copyFn from "copy-text-to-clipboard"
import PromptPanel from "../components/ai/PromptPanel.vue" import PromptPanel from "../components/ai/PromptPanel.vue"
import ExternalAIPanel from "../components/ai/ExternalAIPanel.vue" import ExternalAIPanel from "../components/ai/ExternalAIPanel.vue"
import PromptHistoryPanel from "../components/ai/PromptHistoryPanel.vue"
import Preview from "../components/editor/Preview.vue" import Preview from "../components/editor/Preview.vue"
import TaskStatsModal from "../components/task/TaskStatsModal.vue" import TaskStatsModal from "../components/task/TaskStatsModal.vue"
import { Challenge, Submission, TaskAssets } from "../api" import { Challenge, Submission, TaskAssets } from "../api"
@@ -169,6 +178,7 @@ const showCode = ref(false)
const showStats = ref(false) const showStats = ref(false)
const showAssets = ref(false) const showAssets = ref(false)
const assets = ref<TaskAsset[]>([]) const assets = ref<TaskAsset[]>([])
const historyRefreshKey = ref(0)
const assetBaseUrl = computed( const assetBaseUrl = computed(
() => `/media/tasks/challenge/${challengeDisplay.value}/`, () => `/media/tasks/challenge/${challengeDisplay.value}/`,
@@ -203,6 +213,7 @@ async function loadChallenge() {
}, },
messageId, messageId,
) )
historyRefreshKey.value++
message.success("已自动提交本次对话生成的代码") message.success("已自动提交本次对话生成的代码")
} catch { } catch {
// 静默失败,不打扰用户 // 静默失败,不打扰用户

View File

@@ -12,6 +12,19 @@ export interface PromptMessage {
created: string created: string
} }
export interface PromptHistoryItem {
user_message_id: number
assistant_message_id: number
submission_id: string | null
source: string
prompt: string
prompt_level: number | null
code_html: string | null
code_css: string | null
code_js: string | null
created: string
}
export enum Role { export enum Role {
Super = "super", Super = "super",
Admin = "admin", Admin = "admin",