add message history
This commit is contained in:
@@ -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[]>(
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
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 />
|
<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 {
|
||||||
// 静默失败,不打扰用户
|
// 静默失败,不打扰用户
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user