add message history
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
FlagType,
|
||||
SubmissionOut,
|
||||
PromptMessage,
|
||||
PromptHistoryItem,
|
||||
TaskStatsOut,
|
||||
TaskAsset,
|
||||
AwardSection,
|
||||
@@ -306,6 +307,11 @@ export const Prompt = {
|
||||
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[]> {
|
||||
return (
|
||||
await http.get<PromptMessage[]>(
|
||||
|
||||
@@ -52,6 +52,9 @@ import { html, css, js } from "../../store/editors"
|
||||
import { Submission } from "../../api"
|
||||
|
||||
const props = defineProps<{ taskId: number }>()
|
||||
const emit = defineEmits<{
|
||||
submitted: []
|
||||
}>()
|
||||
const message = useMessage()
|
||||
|
||||
const promptText = ref("")
|
||||
@@ -105,6 +108,7 @@ async function submit() {
|
||||
js: splitResult.value.js,
|
||||
prompt: promptText.value.trim(),
|
||||
})
|
||||
emit("submitted")
|
||||
message.success("提交成功")
|
||||
promptText.value = ""
|
||||
rawCode.value = ""
|
||||
|
||||
212
src/components/ai/PromptHistoryPanel.vue
Normal file
212
src/components/ai/PromptHistoryPanel.vue
Normal 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>
|
||||
@@ -52,7 +52,15 @@
|
||||
<PromptPanel />
|
||||
</n-tab-pane>
|
||||
<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-tabs>
|
||||
</div>
|
||||
@@ -116,6 +124,7 @@ import { marked, type MarkedOptions } from "marked"
|
||||
import copyFn from "copy-text-to-clipboard"
|
||||
import PromptPanel from "../components/ai/PromptPanel.vue"
|
||||
import ExternalAIPanel from "../components/ai/ExternalAIPanel.vue"
|
||||
import PromptHistoryPanel from "../components/ai/PromptHistoryPanel.vue"
|
||||
import Preview from "../components/editor/Preview.vue"
|
||||
import TaskStatsModal from "../components/task/TaskStatsModal.vue"
|
||||
import { Challenge, Submission, TaskAssets } from "../api"
|
||||
@@ -169,6 +178,7 @@ const showCode = ref(false)
|
||||
const showStats = ref(false)
|
||||
const showAssets = ref(false)
|
||||
const assets = ref<TaskAsset[]>([])
|
||||
const historyRefreshKey = ref(0)
|
||||
|
||||
const assetBaseUrl = computed(
|
||||
() => `/media/tasks/challenge/${challengeDisplay.value}/`,
|
||||
@@ -203,6 +213,7 @@ async function loadChallenge() {
|
||||
},
|
||||
messageId,
|
||||
)
|
||||
historyRefreshKey.value++
|
||||
message.success("已自动提交本次对话生成的代码")
|
||||
} catch {
|
||||
// 静默失败,不打扰用户
|
||||
|
||||
@@ -12,6 +12,19 @@ export interface PromptMessage {
|
||||
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 {
|
||||
Super = "super",
|
||||
Admin = "admin",
|
||||
|
||||
Reference in New Issue
Block a user