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-05-09 01:30:36 -06:00
parent 53a0759507
commit f7817d8c26
6 changed files with 95 additions and 124 deletions

View File

@@ -84,8 +84,8 @@
<script setup lang="ts">
import { ref, computed, watch, nextTick } from "vue"
import { marked } from "marked"
import { Icon } from "@iconify/vue"
import { renderMarkdown } from "../../utils/markdown"
import {
messages,
streaming,
@@ -110,10 +110,6 @@ const displayStreamingContent = computed(() =>
streamingContent.value.replace(/^\[READY\]\n?/, "")
)
function renderMarkdown(text: string): string {
return marked.parse(text) as string
}
function send() {
const text = draftPrompt.value.trim()
if (!text || streaming.value) return

View File

@@ -75,17 +75,36 @@
{{ item.source === "manual" ? "手动提交" : "AI 对话" }}
</n-tag>
</n-flex>
<n-tag
v-if="selectedAssistantMessageId === item.assistant_message_id"
size="small"
type="success"
:bordered="false"
>
正在预览
</n-tag>
<n-text depth="3">
{{ parseTime(item.created, "YYYY-MM-DD HH:mm") }}
</n-text>
<n-flex align="center" :wrap="false" :size="4">
<n-tag
v-if="selectedAssistantMessageId === item.assistant_message_id"
size="small"
type="success"
:bordered="false"
>
正在预览
</n-tag>
<n-text depth="3">
{{ parseTime(item.created, "YYYY-MM-DD HH:mm") }}
</n-text>
<n-tooltip placement="top">
<template #trigger>
<n-button
quaternary
circle
size="small"
:loading="deletingId === item.assistant_message_id"
:disabled="deletingId !== null"
@click="deleteItem(item, $event)"
>
<template #icon>
<Icon icon="lucide:trash-2" />
</template>
</n-button>
</template>
删除这条历史对话
</n-tooltip>
</n-flex>
</n-flex>
<div
class="prompt-markdown markdown-body"
@@ -112,7 +131,8 @@
<script setup lang="ts">
import { onMounted, ref, watch } from "vue"
import { Icon } from "@iconify/vue"
import { marked } from "marked"
import { useMessage } from "naive-ui"
import { renderMarkdown } from "../../utils/markdown"
import { Prompt } from "../../api"
import type { PromptHistoryItem } from "../../utils/type"
import { parseTime } from "../../utils/helper"
@@ -127,6 +147,7 @@ const props = defineProps<{
const emit = defineEmits<{
select: [code: { html: string; css: string; js: string }]
deleted: [assistantMessageId: number]
}>()
type HistoryViewItem = PromptHistoryItem & {
@@ -137,7 +158,11 @@ type HistoryViewItem = PromptHistoryItem & {
const items = ref<HistoryViewItem[]>([])
const loading = ref(false)
const selectedAssistantMessageId = ref<number | null>(null)
const deletingId = ref<number | null>(null)
let loadedTaskId = 0
let pendingRefresh = false
const naiveMessage = useMessage()
function toViewItem(item: PromptHistoryItem): HistoryViewItem {
const html = item.code_html ?? ""
@@ -155,8 +180,25 @@ function toViewItem(item: PromptHistoryItem): HistoryViewItem {
}
}
function renderMarkdown(text: string): string {
return marked.parse(text) as string
async function deleteItem(item: HistoryViewItem, e: Event) {
e.stopPropagation()
if (deletingId.value !== null) return
deletingId.value = item.assistant_message_id
try {
await Prompt.deleteMessagePair(item.assistant_message_id)
items.value = items.value.filter(
(i) => i.assistant_message_id !== item.assistant_message_id,
)
if (selectedAssistantMessageId.value === item.assistant_message_id) {
selectedAssistantMessageId.value = null
}
emit("deleted", item.assistant_message_id)
naiveMessage.success("已删除")
} catch {
naiveMessage.error("删除失败,请重试")
} finally {
deletingId.value = null
}
}
function selectItem(item: HistoryViewItem) {
@@ -194,7 +236,14 @@ async function load(force = true) {
watch(
() => [props.active, props.taskId] as const,
([active]) => {
if (active) load(false)
if (active) {
if (pendingRefresh) {
pendingRefresh = false
load(true)
} else {
load(false)
}
}
},
)
@@ -202,6 +251,7 @@ watch(
() => props.refreshKey,
() => {
if (props.active) load(true)
else pendingRefresh = true
},
)
@@ -243,6 +293,7 @@ onMounted(() => {
overflow: hidden;
}
.history-card.is-selected {
--n-color: #f7fffa;
box-shadow: 0 10px 24px rgba(24, 160, 88, 0.14);

View File

@@ -100,7 +100,6 @@
<script setup lang="ts">
import { ref, watch, nextTick, computed } from "vue"
import { useStorage } from "@vueuse/core"
import { marked, Renderer } from "marked"
import { useMessage } from "naive-ui"
import { Icon } from "@iconify/vue"
import GuidancePanel from "./GuidancePanel.vue"
@@ -113,8 +112,12 @@ import {
sendPrompt,
stopPrompt,
currentTaskId,
removeMessagePair,
} from "../../store/prompt"
import { Prompt } from "../../api"
import { renderMarkdown } from "../../utils/markdown"
const emit = defineEmits<{ deleted: [] }>()
const input = ref("")
const messagesRef = ref<HTMLElement>()
@@ -151,11 +154,9 @@ const pairs = computed(() => {
async function deletePair(assistantMsgId: number) {
try {
await Prompt.deleteMessagePair(assistantMsgId)
const msgIdx = messages.value.findIndex((m) => m.id === assistantMsgId)
if (msgIdx >= 1) {
messages.value.splice(msgIdx - 1, 2)
}
removeMessagePair(assistantMsgId)
naiveMessage.success("已删除")
emit("deleted")
} catch {
naiveMessage.error("删除失败,请重试")
}
@@ -179,56 +180,6 @@ function onGuidanceGenerate(finalPrompt: string) {
input.value = ""
}
const renderer = new Renderer()
renderer.code = function ({ lang }: { text: string; lang?: string }) {
const label = lang ? lang.toUpperCase() : "CODE"
const colors: Record<
string,
{ bg: string; fg: string; dot: string; border: string; shimmer: string }
> = {
html: {
bg: "#f0fff4",
fg: "#18a058",
dot: "#18a058",
border: "#b8e8cc",
shimmer: "#f0fff4, #e0f7ea, #f0fff4",
},
css: {
bg: "#f0f0ff",
fg: "#6060d0",
dot: "#6060d0",
border: "#d0d0f0",
shimmer: "#f0f0ff, #e8e8fa, #f0f0ff",
},
js: {
bg: "#fffbf0",
fg: "#c0960a",
dot: "#c0960a",
border: "#f0e0b0",
shimmer: "#fffbf0, #fff5e0, #fffbf0",
},
javascript: {
bg: "#fffbf0",
fg: "#c0960a",
dot: "#c0960a",
border: "#f0e0b0",
shimmer: "#fffbf0, #fff5e0, #fffbf0",
},
}
const c = colors[(lang ?? "").toLowerCase()] ?? {
bg: "#f0f7ff",
fg: "#2080f0",
dot: "#2080f0",
border: "#e0eaf5",
shimmer: "#f0f7ff, #e8f4f8, #f0f7ff",
}
return `<div class="code-placeholder" style="background: linear-gradient(90deg, ${c.shimmer}); background-size: 200% 100%; border-color: ${c.border}"><span class="code-placeholder-dot" style="background: ${c.dot}"></span><span class="code-placeholder-label" style="color: ${c.fg}; background: ${c.fg}18">${label}</span><span class="code-placeholder-text">代码正在生成中,结束后会自动应用到预览区</span></div>`
}
function renderMarkdown(text: string): string {
return marked.parse(text, { renderer }) as string
}
function renderContent(msg: { role: string; content: string }): string {
return renderMarkdown(msg.content)
}
@@ -289,47 +240,6 @@ watch([() => messages.value.length, streamingContent], () => {
font-size: 13px;
}
.message-content :deep(.code-placeholder) {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
margin: 8px 0;
background: linear-gradient(90deg, #f0f7ff, #e8f4f8, #f0f7ff);
background-size: 200% 100%;
animation: shimmer 2s ease-in-out infinite;
border-radius: 6px;
border: 1px solid #e0eaf5;
}
.message-content :deep(.code-placeholder-dot) {
width: 8px;
height: 8px;
border-radius: 50%;
background: #2080f0;
animation: pulse 1.5s ease-in-out infinite;
}
.message-content :deep(.code-placeholder-label) {
font-size: 11px;
font-weight: 600;
padding: 1px 6px;
border-radius: 3px;
}
.message-content :deep(.code-placeholder-text) {
font-size: 12px;
color: #888;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes pulse {
0%,

View File

@@ -49,7 +49,7 @@
</div>
</n-tab-pane>
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
<PromptPanel />
<PromptPanel @deleted="historyRefreshKey++" />
</n-tab-pane>
<n-tab-pane name="external" tab="手动提交" display-directive="show">
<ExternalAIPanel :task-id="taskId" @submitted="historyRefreshKey++" />
@@ -61,6 +61,7 @@
:asset-base-url="assetBaseUrl"
:refresh-key="historyRefreshKey"
@select="previewHistoryItem"
@deleted="removeMessagePair"
/>
</n-tab-pane>
</n-tabs>
@@ -139,6 +140,7 @@ import {
disconnectPrompt,
streaming,
setOnCodeComplete,
removeMessagePair,
} from "../store/prompt"
const route = useRoute()
@@ -193,14 +195,17 @@ async function loadChallenge() {
const display = Number(route.params.display)
taskTab.value = TASK_TYPE.Challenge
challengeDisplay.value = display
const data = await Challenge.get(display)
const [data, fetchedAssets] = await Promise.all([
Challenge.get(display),
TaskAssets.listChallenge(display),
])
taskId.value = data.task_ptr
challengeAuthor.value = data.author_name ?? ""
challengeContent.value = await marked.parse(data.content, {
async: true,
renderer: challengeRenderer,
} as MarkedOptions)
assets.value = await TaskAssets.listChallenge(display)
assets.value = fetchedAssets
if (!authed.value) return
connectPrompt(data.task_ptr)
setOnCodeComplete(async (code, messageId) => {

View File

@@ -28,10 +28,8 @@ export function setOnCodeComplete(fn: typeof _onCodeComplete) {
}
let ws: WebSocket | null = null
let _currentTaskId = 0
export function connectPrompt(taskId: number) {
_currentTaskId = taskId
currentTaskId.value = taskId
if (ws) ws.close()
@@ -102,7 +100,6 @@ export function disconnectPrompt() {
streaming.value = false
streamingContent.value = ""
currentTaskId.value = 0
_currentTaskId = 0
_onCodeComplete = null
}
@@ -122,8 +119,15 @@ export function stopPrompt() {
}
streaming.value = false
streamingContent.value = ""
if (_currentTaskId) {
connectPrompt(_currentTaskId)
if (currentTaskId.value) {
connectPrompt(currentTaskId.value)
}
}
export function removeMessagePair(assistantMsgId: number) {
const idx = messages.value.findIndex((m) => m.id === assistantMsgId)
if (idx >= 1) {
messages.value.splice(idx - 1, 2)
}
}

5
src/utils/markdown.ts Normal file
View File

@@ -0,0 +1,5 @@
import { marked } from "marked"
export function renderMarkdown(text: string): string {
return marked.parse(text) as string
}