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
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>
|
|
|