fix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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%,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
5
src/utils/markdown.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { marked } from "marked"
|
||||
|
||||
export function renderMarkdown(text: string): string {
|
||||
return marked.parse(text) as string
|
||||
}
|
||||
Reference in New Issue
Block a user