You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

290 lines
11 KiB

<script setup lang="ts">
import Msg from "./_/Msg.vue";
import PromptText from "./prompt.txt?raw";
const chatboxContainerEl = useTemplateRef<HTMLDivElement>("chatboxContainer");
const chatboxContentEl = useTemplateRef<HTMLDivElement>("chatboxContent");
const { scrollToBottom } = useScroll({
containerEl: chatboxContainerEl,
contentEl: chatboxContentEl,
firstToBottom: true,
});
// import sseDataModule from "./_/sseData.ts";
interface IMsg <T = "user">{
role: "user" | "assistant" | "system",
content: T extends "user" ? any : string,
reasoning_content?: string,
isHidden?: boolean
}
const msgList = ref<IMsg[]>([
{
role: "system",
content: PromptText
}
]);
const inputMsg = ref("");
enum STATUS {
WAITING = "WAITING",
SENDING = "SENDING",
}
const status = ref(STATUS.WAITING);
const { sendStream, getConfig, updateConfig } = useChat(Chat.ModelProvider.OpenAI, {
// DeepSeek
// model: "deepseek-chat",
// apiKey: process.env.AI_APIKEY,
// baseUrl: "https://api.deepseek.com",
// temperature: 0.8,
// siliconflow
model: "Qwen/Qwen3-8B", // 免费文本模型,可tools
// model: "deepseek-ai/deepseek-vl2",
apiKey: process.env.AI_APIKEY,
baseUrl: "https://api.siliconflow.cn/v1",
temperature: 0.8,
});
const openaiConfig = reactive({
model: getConfig().model,
apiKey: getConfig().apiKey,
baseUrl: getConfig().baseUrl,
temperature: getConfig().temperature,
});
watch(openaiConfig, () => {
updateConfig(toRaw(openaiConfig) as OpenAIModelConfig);
console.log(getConfig());
}, { deep: true })
const inputEl = useTemplateRef("inputEl")
async function sendMsg(msg: any, isHidden?: boolean) {
const newMsg = `检查回答是否符合要求!!!
---
${msg}`
msgList.value.push({
role: "user",
content: newMsg,
isHidden: isHidden,
});
status.value = STATUS.SENDING;
let contents = JSON.parse(JSON.stringify(unref(msgList))).map((v: any) => {
return {
role: v.role,
content: v.content,
}
})
msgList.value.push({
role: "assistant",
content: "",
reasoning_content: "",
});
try {
await sendStream(contents as any, (msg: any) => {
msgList.value[msgList.value.length - 1].reasoning_content = msg.reasoning_content;
msgList.value[msgList.value.length - 1].content = msg.content;
if (msg.isComplete) {
status.value = STATUS.WAITING;
}
});
} catch (error: any) {
try {
const text = await error.response.text()
msgList.value[msgList.value.length - 1].content = text;
} catch (err) {
msgList.value[msgList.value.length - 1].content = error.message;
}
status.value = STATUS.WAITING;
}
}
onMounted(() => {
sendMsg("列举你能的事情,标记顺序,简洁回答。", true)
inputEl.value?.focus()
})
function handleSubmit() {
if (status.value === STATUS.SENDING) return
sendMsg(inputMsg.value)
inputMsg.value = "";
nextTick(() => {
scrollToBottom();
});
}
function handleDelete(item: { role: string, content: string, reasoning_content?: string }, index: number) {
if (item.role === 'system') {
msgList.value[0].content = "";
} else {
msgList.value.splice(index, 1);
}
}
//https://www.codecopy.cn/post/t3clc5
// https://zhuanlan.zhihu.com/p/1948421667379483653#:~:text=Cursor%20%E6%9C%89%E5%BE%88%E5%A4%9A%E5%A5%97%E6%8F%90%E7%A4%BA%E8%AF%8D%EF%BC%8C%E6%AF%8F%E5%A5%97%E6%8F%90%E7%A4%BA%E8%AF%8D%E9%80%82%E7%94%A8%E4%BA%8E%E4%B8%8D%E5%90%8C%E7%9A%84%E5%9C%BA%E6%99%AF%E3%80%82%20%E6%AF%94%E5%A6%82%EF%BC%9A%20Agent%20%E6%A8%A1%E5%BC%8F%E6%8F%90%E7%A4%BA%E8%AF%8D%EF%BC%9A%E8%AE%A9%20AI%20%E8%83%BD%E5%A4%9F%E8%87%AA%E4%B8%BB%E5%9C%B0%E5%88%86%E6%9E%90%E3%80%81%E8%A7%84%E5%88%92%E5%B9%B6%E6%89%A7%E8%A1%8C%E7%BC%96%E7%A0%81%E4%BB%BB%E5%8A%A1%EF%BC%8C%E7%9B%B4%E5%88%B0%E9%97%AE%E9%A2%98%E8%A2%AB%E5%BD%BB%E5%BA%95%E8%A7%A3%E5%86%B3%E3%80%82,Chat%20%E5%AF%B9%E8%AF%9D%E6%8F%90%E7%A4%BA%E8%AF%8D%EF%BC%9A%E9%80%82%E7%94%A8%E4%BA%8E%E4%BB%A5%E5%AF%B9%E8%AF%9D%E9%97%AE%E7%AD%94%E4%B8%BA%E4%B8%BB%E7%9A%84%E5%9C%BA%E6%99%AF%EF%BC%8C%E8%83%BD%E5%BF%AB%E9%80%9F%E5%93%8D%E5%BA%94%E7%94%A8%E6%88%B7%E7%9A%84%E9%97%AE%E9%A2%98%E3%80%82%20Memory%20%E5%AF%B9%E8%AF%9D%E8%AE%B0%E5%BF%86%E6%8F%90%E7%A4%BA%E8%AF%8D%EF%BC%9A%E8%AF%84%E4%BC%B0%20AI%20%E7%9A%84%E9%95%BF%E6%9C%9F%E8%AE%B0%E5%BF%86%EF%BC%8C%E4%BF%9D%E8%AF%81%20AI%20%E8%83%BD%E5%A4%9F%E4%BB%8E%E5%8E%86%E5%8F%B2%E4%BA%A4%E4%BA%92%E4%B8%AD%E5%AD%A6%E4%B9%A0%E5%B9%B6%E6%B2%89%E6%B7%80%E9%AB%98%E8%B4%A8%E9%87%8F%E7%9A%84%E9%80%9A%E7%94%A8%E5%81%8F%E5%A5%BD%E8%AE%B0%E5%BF%86%E3%80%82
// https://mcpcn.com/docs/tutorials/building-a-client-node/#%e4%ba%a4%e4%ba%92%e5%bc%8f%e8%81%8a%e5%a4%a9%e7%95%8c%e9%9d%a2
</script>
<template>
<div class="chat-wrapper">
<div style="display: flex;gap: 20px;">
<input type="text" v-model="openaiConfig.model" placeholder="模型">
<input type="text" v-model="openaiConfig.apiKey" placeholder="apiKey">
<input type="text" v-model="openaiConfig.baseUrl" placeholder="baseUrl">
<input type="text" v-model="openaiConfig.temperature" placeholder="temperature">
</div>
<div class="chatbox-container" ref="chatboxContainer">
<div class="chatbox-content" ref="chatboxContent">
<template v-for="(data, index) in msgList" :key="index">
<div class="system-msg" v-if="data.role === 'system' && !data.isHidden">
<textarea rows="10" cols="50" v-model="data.content"></textarea>
</div>
<div v-else-if="!data.isHidden" class="chat-item"
:class="{ left: data.role === 'assistant', right: data.role === 'user' }">
<div v-if="data.role === 'assistant'" style="display: flex; flex-direction: column; gap: 2px;">
<div style="display: flex; gap: 10px;">
<div style="width: 50px; height: 50px;flex-shrink: 0;">
<img style="width: 100%; height: 100%;" src="/deepseek.svg" alt="avatar"></img>
</div>
<div style="padding-top: 5px;">
<div v-if="data.reasoning_content"
style="color: var(--color-fg-muted);padding: 20px;">
<h2 style="font-size: 20px;margin-bottom: 10px;">推理中</h2>
<Msg :msg="data.reasoning_content"></Msg>
</div>
<div>
<Msg :msg="data.content"></Msg>
</div>
</div>
</div>
<div style="display: flex; gap: 10px;margin-left: 60px;">
<div @click="handleDelete(data, index)">删除</div>
</div>
</div>
<div v-else style="display: flex; flex-direction: column; gap: 2px;">
<div style="display: flex; gap: 10px;flex-direction: row-reverse;">
<div style="width: 50px; height: 50px;flex-shrink: 0;">
<img style="width: 100%; height: 100%;" src="/vite.svg" alt="avatar"></img>
</div>
<div style="padding-top: 5px;">
{{ data.content }}
</div>
</div>
<div style="display: flex; gap: 10px;justify-content: flex-end;margin-right: 60px;">
<div @click="handleDelete(data, index)">删除</div>
</div>
</div>
</div>
</template>
</div>
</div>
<form class="chat-input" @submit.native.prevent="handleSubmit">
<input ref="inputEl" type="text" v-model="inputMsg" placeholder="请输入内容" class="chat-input-input">
</form>
</div>
</template>
<style lang="scss" scoped>
.chat-wrapper {
padding: 20px;
box-sizing: border-box;
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
}
.chat-input {
.chat-input-input {
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
background: css-var("color-canvas-subtle");
color: css-var("color-fg-default");
line-height: 1.5;
word-break: break-all;
border-radius: 10px;
padding: 10px;
box-sizing: border-box;
outline: none;
}
}
.chatbox-container {
background: css-var("color-canvas-default");
color: css-var("color-fg-default");
height: 0;
flex: 1;
overflow: auto;
padding: 10px;
box-sizing: border-box;
border-radius: 10px;
background: css-var("color-canvas-subtle");
color: css-var("color-fg-default");
line-height: 1.2;
.chatbox-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.system-msg {
align-self: center;
font-size: 12px;
color: css-var("color-fg-muted");
line-height: 1.5;
word-break: break-all;
padding: 10px;
width: 50%;
box-sizing: border-box;
border-radius: 10px;
background: css-var("color-canvas-subtle");
color: css-var("color-fg-default");
word-break: break-all;
text-align: center;
}
.chat-item {
max-width: 100%;
padding: 10px;
box-sizing: border-box;
border-radius: 10px;
background: css-var("color-canvas-subtle");
color: css-var("color-fg-default");
line-height: 1.5;
word-break: break-all;
position: relative;
&.left {
align-self: flex-start;
}
&.right {
// border: css-var("color-border-default") 1px solid;
align-self: flex-end;
margin-left: 10%;
}
.close-btn {
position: absolute;
right: 0;
top: 0;
cursor: pointer;
font-size: 12px;
color: css-var("color-fg-muted");
line-height: 1.5;
padding: 5px;
box-sizing: border-box;
border-radius: 5px;
background: css-var("color-canvas-subtle");
color: css-var("color-fg-default");
line-height: 1.5;
word-break: break-all;
border: css-var("color-border-default") 1px solid;
align-self: flex-end;
margin-left: 10%;
}
}
}
</style>