feat: 一些优化,以及token消耗展示
This commit is contained in:
@@ -6,8 +6,8 @@ const { avatar } = defineProps<{
|
||||
|
||||
<template>
|
||||
<NImage
|
||||
:src="avatar" object-fit="cover" :preview-disabled="true" width="60" height="60" class="!block" :img-props="{
|
||||
class: 'rounded-lg !block',
|
||||
:src="avatar" object-fit="cover" :preview-disabled="true" width="64" height="64" class="!block !w-16 !min-w-16 !h-16" :img-props="{
|
||||
class: 'rounded-lg !block !w-16 !min-w-16 !h-16',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import hljs from "highlight.js"
|
||||
import markdownit from "markdown-it"
|
||||
import hljs from "highlight.js";
|
||||
import markdownit from "markdown-it";
|
||||
import { useWindowWidth } from "@/utils";
|
||||
|
||||
const { content } = defineProps<{
|
||||
content: string
|
||||
}>()
|
||||
content: string;
|
||||
}>();
|
||||
|
||||
const windowWidth = useWindowWidth();
|
||||
|
||||
const md = markdownit({
|
||||
html: true,
|
||||
@@ -13,24 +16,34 @@ const md = markdownit({
|
||||
highlight(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(str, { language: lang }).value
|
||||
return hljs.highlight(str, { language: lang }).value;
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (__) { }
|
||||
}
|
||||
|
||||
return "" // use external default escaping
|
||||
return ""; // use external default escaping
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// // 计算代码块宽度
|
||||
const codeWidth = computed(() => {
|
||||
return windowWidth.value - 160 - 200; // 减去左右边距和其他元素的宽度
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markdown-body w-full text-base break-words whitespace-normal" v-html="md.render(content)">
|
||||
<div
|
||||
class="markdown-body w-full text-base break-words whitespace-normal"
|
||||
:style="{ '--code-width': `${codeWidth}px` }" v-html="md.render(content)"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.markdown-body :deep(pre) {
|
||||
width: var(--code-width, 100%);
|
||||
max-width: 100%;
|
||||
background: #fafafacc;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
@@ -38,4 +51,20 @@ const md = markdownit({
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre)::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre)::-webkit-scrollbar-thumb {
|
||||
background: #bfbfbf;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre)::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
export interface IChatWithLLMRequest {
|
||||
messages: Message[]
|
||||
messages: Message[];
|
||||
/**
|
||||
* 要使用的模型的 ID
|
||||
*/
|
||||
model: string
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
content?: string
|
||||
role?: string
|
||||
[property: string]: any
|
||||
content?: string;
|
||||
role?: string;
|
||||
usage?: UsageInfo;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
model_id: string
|
||||
model_name: string
|
||||
model_type: string
|
||||
model_id: string;
|
||||
model_name: string;
|
||||
model_type: string;
|
||||
}
|
||||
|
||||
export interface ModelListInfo {
|
||||
vendor: string
|
||||
models: ModelInfo[]
|
||||
vendor: string;
|
||||
models: ModelInfo[];
|
||||
}
|
||||
|
||||
export interface UsageInfo {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import type { AxiosRequestConfig } from "axios"
|
||||
import type { AxiosRequestConfig } from "axios";
|
||||
|
||||
import type { IChatWithLLMRequest } from "@/interfaces"
|
||||
import BaseClientService, { BaseUrl } from "./base_service.ts"
|
||||
import type { IChatWithLLMRequest, UsageInfo } from "@/interfaces";
|
||||
import BaseClientService, { BaseUrl } from "./base_service.ts";
|
||||
|
||||
export class ChatService {
|
||||
public static basePath = BaseUrl
|
||||
public static basePath = BaseUrl;
|
||||
|
||||
/** Chat with LLM */
|
||||
public static async ChatWithLLM(
|
||||
accessToken: string,
|
||||
request: IChatWithLLMRequest,
|
||||
onProgress: (content: string) => void,
|
||||
getUsageInfo: (object: UsageInfo) => void = () => { },
|
||||
) {
|
||||
let response
|
||||
let buffer = ""
|
||||
let accumulatedContent = ""
|
||||
let response;
|
||||
let buffer = "";
|
||||
let accumulatedContent = "";
|
||||
try {
|
||||
response = await fetch("/v1/chat/completions", {
|
||||
method: "POST",
|
||||
@@ -23,69 +24,77 @@ export class ChatService {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// eslint-disable-next-line unicorn/error-message
|
||||
throw new Error()
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader!.read()
|
||||
const { done, value } = await reader!.read();
|
||||
|
||||
if (done)
|
||||
break
|
||||
break;
|
||||
|
||||
// 将二进制数据转为字符串并存入缓冲区
|
||||
buffer += decoder.decode(value)
|
||||
buffer += decoder.decode(value);
|
||||
|
||||
// 查找换行符分割数据
|
||||
const lines = buffer.split("\n")
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
// 保留未处理完的部分
|
||||
buffer = lines.pop() || ""
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
// 处理每一行
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine)
|
||||
continue
|
||||
continue;
|
||||
|
||||
if (trimmedLine.startsWith("data: ")) {
|
||||
const jsonStr = trimmedLine.slice(6)
|
||||
const jsonStr = trimmedLine.slice(6);
|
||||
|
||||
// 处理结束标记
|
||||
if (jsonStr === "[DONE]") {
|
||||
onProgress(accumulatedContent) // 最终更新
|
||||
return
|
||||
onProgress(accumulatedContent); // 最终更新
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonStr)
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
// 处理内容
|
||||
if (data.choices?.[0]?.delta?.content) {
|
||||
// 累积内容
|
||||
accumulatedContent += data.choices[0].delta.content
|
||||
accumulatedContent += data.choices[0].delta.content;
|
||||
// 触发回调
|
||||
onProgress(accumulatedContent)
|
||||
onProgress(accumulatedContent);
|
||||
}
|
||||
|
||||
// 处理使用信息
|
||||
if (data.usage) {
|
||||
const { prompt_tokens, completion_tokens, total_tokens } = data.usage;
|
||||
getUsageInfo({ prompt_tokens, completion_tokens, total_tokens });
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error("JSON解析失败:", err)
|
||||
console.error("JSON解析失败:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Error:", err)
|
||||
console.error("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模型列表
|
||||
public static GetModelList(config?: AxiosRequestConfig<any>) {
|
||||
return BaseClientService.get(`${this.basePath}/model/list`, config)
|
||||
return BaseClientService.get(`${this.basePath}/model/list`, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IChatWithLLMRequest, ModelInfo, ModelListInfo } from "@/interfaces";
|
||||
import type { IChatWithLLMRequest, ModelInfo, ModelListInfo, UsageInfo } from "@/interfaces";
|
||||
import { ChatService } from "@/services";
|
||||
|
||||
export const useChatStore = defineStore("chat", () => {
|
||||
@@ -16,6 +16,7 @@ export const useChatStore = defineStore("chat", () => {
|
||||
const chatWithLLM = async (
|
||||
request: IChatWithLLMRequest,
|
||||
onProgress: (content: string) => void, // 接收进度回调
|
||||
getUsageInfo: (object: UsageInfo) => void = () => { },
|
||||
) => {
|
||||
if (completing.value)
|
||||
throw new Error("正在响应中");
|
||||
@@ -24,6 +25,8 @@ export const useChatStore = defineStore("chat", () => {
|
||||
try {
|
||||
await ChatService.ChatWithLLM(token, request, (content) => {
|
||||
onProgress(content);
|
||||
}, (object: UsageInfo) => {
|
||||
getUsageInfo(object);
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
@@ -71,6 +74,15 @@ export const useChatStore = defineStore("chat", () => {
|
||||
});
|
||||
}
|
||||
historyMessages.value[historyMessages.value.length - 1].content = content;
|
||||
}, (usageInfo: UsageInfo) => {
|
||||
// 处理使用usage信息回调
|
||||
// 如果最后一条消息是助手的回复,则更新使用信息
|
||||
if (
|
||||
historyMessages.value.length > 0
|
||||
&& historyMessages.value[historyMessages.value.length - 1].role === "assistant"
|
||||
) {
|
||||
historyMessages.value[historyMessages.value.length - 1].usage = usageInfo;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./asr_store"
|
||||
export * from "./chat_store"
|
||||
export * from "./asr_store";
|
||||
export * from "./chat_store";
|
||||
|
||||
@@ -7,37 +7,55 @@ export const matchMedia = (
|
||||
if (type === "sm") {
|
||||
if (window.matchMedia("(max-width: 767.98px)").matches) {
|
||||
/* 窗口小于或等于 */
|
||||
matchFunc?.()
|
||||
matchFunc?.();
|
||||
}
|
||||
else {
|
||||
mismatchFunc?.()
|
||||
mismatchFunc?.();
|
||||
}
|
||||
}
|
||||
else if (type === "md") {
|
||||
if (window.matchMedia("(max-width: 992px)").matches) {
|
||||
/* 窗口小于或等于 */
|
||||
matchFunc?.()
|
||||
matchFunc?.();
|
||||
}
|
||||
else {
|
||||
mismatchFunc?.()
|
||||
mismatchFunc?.();
|
||||
}
|
||||
}
|
||||
else if (type === "lg") {
|
||||
if (window.matchMedia("(max-width: 1200px)").matches) {
|
||||
/* 窗口小于或等于 */
|
||||
matchFunc?.()
|
||||
matchFunc?.();
|
||||
}
|
||||
else {
|
||||
mismatchFunc?.()
|
||||
mismatchFunc?.();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (window.matchMedia(`(max-width: ${type}px)`).matches) {
|
||||
/* 窗口小于或等于 */
|
||||
matchFunc?.()
|
||||
matchFunc?.();
|
||||
}
|
||||
else {
|
||||
mismatchFunc?.()
|
||||
}
|
||||
mismatchFunc?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const useWindowWidth = () => {
|
||||
const width = ref(window.innerWidth);
|
||||
|
||||
const updateWidth = () => {
|
||||
width.value = window.innerWidth;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", updateWidth);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", updateWidth);
|
||||
});
|
||||
|
||||
return width;
|
||||
};
|
||||
|
||||
@@ -49,7 +49,6 @@ watch(() => modelList.value, (newVal) => {
|
||||
if (newVal.length > 0 && newVal[0].models.length > 0) {
|
||||
modelInfo.value = newVal[0].models[0];
|
||||
}
|
||||
console.log("Options updated:", options.value);
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
@@ -89,17 +88,17 @@ onMounted(() => {
|
||||
<!-- 历史消息区 -->
|
||||
<NScrollbar ref="scrollbarRef" class="flex-1 pr-4 relative">
|
||||
<div class="flex items-start mb-4">
|
||||
<span class="rounded-lg overflow-hidden">
|
||||
<span class="rounded-lg overflow-hidden !w-16 !min-w-16 !h-16">
|
||||
<avatar :avatar="AIAvatar" />
|
||||
</span>
|
||||
<div class="text-base w-full max-w-full ml-2 flex flex-col items-start">
|
||||
<span class="text-base font-bold">助手:</span>
|
||||
<span class="text-base font-bold mb-4">助手:</span>
|
||||
<span class="text-base">你好,我是你的智能助手,请问有什么可以帮助你的吗?</span>
|
||||
<NDivider />
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(msg, idx) in historyMessages" :key="idx" class="flex items-start mb-4">
|
||||
<span v-if="msg.role === 'user'" class="rounded-lg overflow-hidden">
|
||||
<span v-if="msg.role === 'user'" class="rounded-lg overflow-hidden !w-16 !min-w-16 !h-16">
|
||||
<avatar :avatar="UserAvatar" />
|
||||
</span>
|
||||
<span v-else class="rounded-lg overflow-hidden">
|
||||
@@ -107,6 +106,9 @@ onMounted(() => {
|
||||
</span>
|
||||
<div class="text-base w-full max-w-full ml-2 flex flex-col items-start">
|
||||
<span class="text-base font-bold">{{ msg.role === 'user' ? '你:' : '助手:' }}</span>
|
||||
<div v-if="msg.role !== 'user'" class="text-[12px] text-[#7A7A7A] mb-[2px]">
|
||||
Tokens: <span class="mr-1">{{ msg.usage?.total_tokens }}</span>
|
||||
</div>
|
||||
<div class="w-full max-w-full">
|
||||
<markdown :content="msg.content || ''" />
|
||||
<NDivider />
|
||||
@@ -121,7 +123,12 @@ onMounted(() => {
|
||||
</div>
|
||||
</NScrollbar>
|
||||
<!-- 输入框 -->
|
||||
<NInput v-model:value="inputData" type="textarea" placeholder="在这里输入消息" @keyup.enter="handleSendMessage" />
|
||||
<NInput
|
||||
v-model:value="inputData" type="textarea" placeholder="输入内容,Enter发送,Shift+Enter换行" :autosize="{
|
||||
minRows: 3,
|
||||
maxRows: 15,
|
||||
}" @keyup.enter="handleSendMessage"
|
||||
/>
|
||||
<!-- 操作区 -->
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user