add prompt assistant
This commit is contained in:
244
src/components/ai/GuidancePanel.vue
Normal file
244
src/components/ai/GuidancePanel.vue
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
:show="isOpen"
|
||||||
|
preset="card"
|
||||||
|
title="调教提示词"
|
||||||
|
style="width: min(560px, 92vw)"
|
||||||
|
:mask-closable="!streaming"
|
||||||
|
@update:show="onModalClose"
|
||||||
|
>
|
||||||
|
<div class="guidance-body">
|
||||||
|
<div class="guidance-messages" ref="messagesRef">
|
||||||
|
<div
|
||||||
|
v-for="(msg, i) in messages"
|
||||||
|
:key="msg.id ?? i"
|
||||||
|
class="guidance-msg"
|
||||||
|
:class="msg.role"
|
||||||
|
>
|
||||||
|
<div class="msg-role">{{ msg.role === "user" ? "你" : "AI" }}</div>
|
||||||
|
<div class="msg-content" v-html="renderMarkdown(msg.content)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="streaming" class="guidance-msg assistant">
|
||||||
|
<div class="msg-role">AI 教练</div>
|
||||||
|
<div v-if="!displayStreamingContent" class="typing-indicator">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="msg-content"
|
||||||
|
v-html="renderMarkdown(displayStreamingContent)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isReady" class="ready-hint">
|
||||||
|
<Icon icon="lucide:check-circle" :width="16" />
|
||||||
|
提示词已优化,可以生成代码了!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guidance-input">
|
||||||
|
<n-input
|
||||||
|
v-model:value="draftPrompt"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 5 }"
|
||||||
|
placeholder="修改你的提示词后发送..."
|
||||||
|
:disabled="streaming"
|
||||||
|
@keydown.enter.exact.prevent="send"
|
||||||
|
/>
|
||||||
|
<n-flex justify="space-between" align="center" style="margin-top: 8px">
|
||||||
|
<div>
|
||||||
|
<n-button
|
||||||
|
v-if="streaming"
|
||||||
|
type="error"
|
||||||
|
size="small"
|
||||||
|
@click="stopGuidance"
|
||||||
|
>
|
||||||
|
停止
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
v-else
|
||||||
|
size="small"
|
||||||
|
:disabled="!draftPrompt.trim() || !connected"
|
||||||
|
@click="send"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<n-button
|
||||||
|
:type="isReady ? 'primary' : 'default'"
|
||||||
|
size="small"
|
||||||
|
:disabled="!draftPrompt.trim()"
|
||||||
|
@click="generate"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<Icon icon="lucide:play" :width="14" />
|
||||||
|
</template>
|
||||||
|
生成代码
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick } from "vue"
|
||||||
|
import { marked } from "marked"
|
||||||
|
import { Icon } from "@iconify/vue"
|
||||||
|
import {
|
||||||
|
messages,
|
||||||
|
streaming,
|
||||||
|
streamingContent,
|
||||||
|
isReady,
|
||||||
|
isOpen,
|
||||||
|
connected,
|
||||||
|
initialPrompt,
|
||||||
|
sendGuidance,
|
||||||
|
stopGuidance,
|
||||||
|
closeGuidance,
|
||||||
|
} from "../../store/guidance"
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
generate: [prompt: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const draftPrompt = ref("")
|
||||||
|
const messagesRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
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
|
||||||
|
sendGuidance(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate() {
|
||||||
|
const text = draftPrompt.value.trim()
|
||||||
|
if (!text) return
|
||||||
|
emit("generate", text)
|
||||||
|
closeGuidance()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onModalClose(show: boolean) {
|
||||||
|
if (!show && !streaming.value) closeGuidance()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(isOpen, (val) => {
|
||||||
|
if (val) draftPrompt.value = initialPrompt.value
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([() => messages.value.length, streamingContent], () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (messagesRef.value) {
|
||||||
|
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.guidance-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidance-messages {
|
||||||
|
min-height: 160px;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidance-msg .msg-role {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidance-msg.user .msg-role {
|
||||||
|
color: #2080f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidance-msg.assistant .msg-role {
|
||||||
|
color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidance-msg.assistant .msg-content {
|
||||||
|
background: #f0f7ff;
|
||||||
|
border-left: 3px solid #2080f0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidance-msg.user .msg-content {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-content :deep(p) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ready-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #18a058;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #f0fff4;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #b8e8cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2080f0;
|
||||||
|
animation: bounce 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%,
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
<div class="streaming-hint">AI 正在思考中…</div>
|
<div class="streaming-hint">AI 正在思考中…</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<GuidancePanel @generate="onGuidanceGenerate" />
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="input"
|
v-model:value="input"
|
||||||
@@ -54,6 +55,16 @@
|
|||||||
@keydown.enter.exact.prevent="send"
|
@keydown.enter.exact.prevent="send"
|
||||||
/>
|
/>
|
||||||
<n-flex justify="flex-end" align="center" style="margin-top: 8px">
|
<n-flex justify="flex-end" align="center" style="margin-top: 8px">
|
||||||
|
<n-button
|
||||||
|
secondary
|
||||||
|
:disabled="!input.trim() || !connected || !currentTaskId || streaming"
|
||||||
|
title="调教提示词"
|
||||||
|
@click="startGuidance"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<Icon icon="lucide:lightbulb" :width="16" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="selectedModel"
|
v-model:value="selectedModel"
|
||||||
:options="modelOptions"
|
:options="modelOptions"
|
||||||
@@ -82,6 +93,8 @@ import { useStorage } from "@vueuse/core"
|
|||||||
import { marked, Renderer } from "marked"
|
import { marked, Renderer } from "marked"
|
||||||
import { useMessage } from "naive-ui"
|
import { useMessage } from "naive-ui"
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
|
import GuidancePanel from "./GuidancePanel.vue"
|
||||||
|
import { openGuidance } from "../../store/guidance"
|
||||||
import {
|
import {
|
||||||
messages,
|
messages,
|
||||||
streaming,
|
streaming,
|
||||||
@@ -89,6 +102,7 @@ import {
|
|||||||
connected,
|
connected,
|
||||||
sendPrompt,
|
sendPrompt,
|
||||||
stopPrompt,
|
stopPrompt,
|
||||||
|
currentTaskId,
|
||||||
} from "../../store/prompt"
|
} from "../../store/prompt"
|
||||||
import { Prompt } from "../../api"
|
import { Prompt } from "../../api"
|
||||||
|
|
||||||
@@ -143,6 +157,17 @@ function send() {
|
|||||||
input.value = ""
|
input.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startGuidance() {
|
||||||
|
const text = input.value.trim()
|
||||||
|
if (!text || !currentTaskId.value) return
|
||||||
|
openGuidance(currentTaskId.value, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGuidanceGenerate(finalPrompt: string) {
|
||||||
|
sendPrompt(finalPrompt, selectedModel.value)
|
||||||
|
input.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
const renderer = new Renderer()
|
const renderer = new Renderer()
|
||||||
renderer.code = function ({ lang }: { text: string; lang?: string }) {
|
renderer.code = function ({ lang }: { text: string; lang?: string }) {
|
||||||
const label = lang ? lang.toUpperCase() : "CODE"
|
const label = lang ? lang.toUpperCase() : "CODE"
|
||||||
|
|||||||
@@ -11,21 +11,7 @@
|
|||||||
vertical
|
vertical
|
||||||
style="height: 100%; padding-right: 10px; overflow: hidden"
|
style="height: 100%; padding-right: 10px; overflow: hidden"
|
||||||
>
|
>
|
||||||
<n-flex justify="space-between" style="flex-shrink: 0">
|
<n-flex align="center" justify="end">
|
||||||
<n-button
|
|
||||||
secondary
|
|
||||||
@click="
|
|
||||||
() =>
|
|
||||||
goHome(
|
|
||||||
$router,
|
|
||||||
taskTab,
|
|
||||||
taskTab === TASK_TYPE.Challenge ? challengeDisplay : step,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
首页
|
|
||||||
</n-button>
|
|
||||||
<n-flex align="center">
|
|
||||||
<n-select
|
<n-select
|
||||||
:value="query.flag"
|
:value="query.flag"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
@@ -64,7 +50,6 @@
|
|||||||
>
|
>
|
||||||
<Icon :width="16" icon="lucide:refresh-cw" />
|
<Icon :width="16" icon="lucide:refresh-cw" />
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
flex-height
|
flex-height
|
||||||
|
|||||||
111
src/store/guidance.ts
Normal file
111
src/store/guidance.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { ref } from "vue"
|
||||||
|
import { WS_BASE_URL } from "../utils/const"
|
||||||
|
|
||||||
|
export interface GuidanceMessage {
|
||||||
|
role: "user" | "assistant"
|
||||||
|
content: string
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messages = ref<GuidanceMessage[]>([])
|
||||||
|
export const connected = ref(false)
|
||||||
|
export const streaming = ref(false)
|
||||||
|
export const streamingContent = ref("")
|
||||||
|
export const isReady = ref(false)
|
||||||
|
export const isOpen = ref(false)
|
||||||
|
export const initialPrompt = ref("")
|
||||||
|
|
||||||
|
let ws: WebSocket | null = null
|
||||||
|
let _taskId = 0
|
||||||
|
|
||||||
|
function setupHandlers(socket: WebSocket) {
|
||||||
|
socket.onopen = () => {
|
||||||
|
connected.value = true
|
||||||
|
if (initialPrompt.value) {
|
||||||
|
sendToSocket(initialPrompt.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
if (data.type === "init") {
|
||||||
|
streaming.value = false
|
||||||
|
streamingContent.value = ""
|
||||||
|
} else if (data.type === "stream") {
|
||||||
|
streaming.value = true
|
||||||
|
streamingContent.value += data.content
|
||||||
|
} else if (data.type === "complete") {
|
||||||
|
streaming.value = false
|
||||||
|
const content = streamingContent.value.replace(/^\[READY\]\n?/, "")
|
||||||
|
messages.value.push({ role: "assistant", content, id: data.message_id })
|
||||||
|
streamingContent.value = ""
|
||||||
|
if (data.is_ready) isReady.value = true
|
||||||
|
} else if (data.type === "error") {
|
||||||
|
streaming.value = false
|
||||||
|
streamingContent.value = ""
|
||||||
|
messages.value.push({ role: "assistant", content: data.content })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
connected.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToSocket(content: string) {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||||
|
streaming.value = true
|
||||||
|
messages.value.push({ role: "user", content })
|
||||||
|
ws.send(JSON.stringify({ type: "message", content }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openGuidance(taskId: number, prompt: string) {
|
||||||
|
if (!taskId) return
|
||||||
|
|
||||||
|
_taskId = taskId
|
||||||
|
initialPrompt.value = prompt
|
||||||
|
messages.value = []
|
||||||
|
isReady.value = false
|
||||||
|
streamingContent.value = ""
|
||||||
|
isOpen.value = true
|
||||||
|
|
||||||
|
if (ws) ws.close()
|
||||||
|
ws = new WebSocket(`${WS_BASE_URL}/ws/guidance/${taskId}/`)
|
||||||
|
setupHandlers(ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendGuidance(content: string) {
|
||||||
|
sendToSocket(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopGuidance() {
|
||||||
|
if (
|
||||||
|
messages.value.length > 0 &&
|
||||||
|
messages.value[messages.value.length - 1].role === "user"
|
||||||
|
) {
|
||||||
|
messages.value.pop()
|
||||||
|
}
|
||||||
|
streaming.value = false
|
||||||
|
streamingContent.value = ""
|
||||||
|
if (_taskId) {
|
||||||
|
if (ws) ws.close()
|
||||||
|
ws = new WebSocket(`${WS_BASE_URL}/ws/guidance/${_taskId}/`)
|
||||||
|
initialPrompt.value = ""
|
||||||
|
setupHandlers(ws)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeGuidance() {
|
||||||
|
if (ws) {
|
||||||
|
ws.close()
|
||||||
|
ws = null
|
||||||
|
}
|
||||||
|
isOpen.value = false
|
||||||
|
messages.value = []
|
||||||
|
isReady.value = false
|
||||||
|
connected.value = false
|
||||||
|
streaming.value = false
|
||||||
|
streamingContent.value = ""
|
||||||
|
initialPrompt.value = ""
|
||||||
|
_taskId = 0
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export const conversationId = ref<string>("")
|
|||||||
export const connected = ref(false)
|
export const connected = ref(false)
|
||||||
export const streaming = ref(false)
|
export const streaming = ref(false)
|
||||||
export const streamingContent = ref("")
|
export const streamingContent = ref("")
|
||||||
|
export const currentTaskId = ref(0)
|
||||||
let _onCodeComplete:
|
let _onCodeComplete:
|
||||||
| ((
|
| ((
|
||||||
code: { html: string | null; css: string | null; js: string | null },
|
code: { html: string | null; css: string | null; js: string | null },
|
||||||
@@ -31,6 +32,7 @@ let _currentTaskId = 0
|
|||||||
|
|
||||||
export function connectPrompt(taskId: number) {
|
export function connectPrompt(taskId: number) {
|
||||||
_currentTaskId = taskId
|
_currentTaskId = taskId
|
||||||
|
currentTaskId.value = taskId
|
||||||
if (ws) ws.close()
|
if (ws) ws.close()
|
||||||
|
|
||||||
ws = new WebSocket(`${WS_BASE_URL}/ws/prompt/${taskId}/`)
|
ws = new WebSocket(`${WS_BASE_URL}/ws/prompt/${taskId}/`)
|
||||||
@@ -99,6 +101,8 @@ export function disconnectPrompt() {
|
|||||||
connected.value = false
|
connected.value = false
|
||||||
streaming.value = false
|
streaming.value = false
|
||||||
streamingContent.value = ""
|
streamingContent.value = ""
|
||||||
|
currentTaskId.value = 0
|
||||||
|
_currentTaskId = 0
|
||||||
_onCodeComplete = null
|
_onCodeComplete = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user