feat: 一些优化,以及token消耗展示

This commit is contained in:
2025-06-29 01:25:42 +08:00
parent 2da3fe3b47
commit dfc817e3e3
8 changed files with 146 additions and 64 deletions

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
});
}
}

View File

@@ -1,2 +1,2 @@
export * from "./asr_store"
export * from "./chat_store"
export * from "./asr_store";
export * from "./chat_store";

View File

@@ -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;
};

View File

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