feat: 新增未登录时候的消息

This commit is contained in:
何嘉悦 2025-05-27 02:08:04 +08:00
parent d15519e18f
commit 3ab287536c
6 changed files with 310 additions and 18 deletions

View File

@ -257,11 +257,13 @@ function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo
padding: 0 12px; padding: 0 12px;
.creat-chat-btn { .creat-chat-btn {
user-select: none;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 6px; padding: 8px 6px;
margin-top: 16px; margin-top: 16px;
margin-bottom: 6px; margin-bottom: 6px;
color: #0057ff;
background-color: rgba(0, 87, 255, 0.06); background-color: rgba(0, 87, 255, 0.06);
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 102, 255, 0.15); border: 1px solid rgba(0, 102, 255, 0.15);

View File

@ -6,6 +6,8 @@ const sessionStore = useSessionStore();
/* 创建会话 开始 */ /* 创建会话 开始 */
function handleCreatChat() { function handleCreatChat() {
if (!sessionStore.currentSession)
return;
// , // ,
sessionStore.createSessionBtn(); sessionStore.createSessionBtn();
} }
@ -15,6 +17,9 @@ function handleCreatChat() {
<template> <template>
<div <div
class="create-chat-container flex-center flex-none p-6px pl-8px pr-8px c-#0057ff b-#0057ff b-rounded-12px border-1px hover:bg-#0057ff hover:c-#fff hover:b-#fff hover:cursor-pointer border-solid select-none" class="create-chat-container flex-center flex-none p-6px pl-8px pr-8px c-#0057ff b-#0057ff b-rounded-12px border-1px hover:bg-#0057ff hover:c-#fff hover:b-#fff hover:cursor-pointer border-solid select-none"
:class="{
'is-disabled': !sessionStore.currentSession,
}"
@click="handleCreatChat" @click="handleCreatChat"
> >
<el-icon size="12" class="flex-center flex-none w-14px h-14px"> <el-icon size="12" class="flex-center flex-none w-14px h-14px">
@ -24,4 +29,18 @@ function handleCreatChat() {
</div> </div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
.is-disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background-color: transparent;
color: #0057ff;
border-color: #0057ff;
border-style: solid;
cursor: not-allowed;
transition: none;
}
}
</style>

View File

@ -24,7 +24,10 @@ provide('refresh', refreshMainPage);
</script> </script>
<template> <template>
<el-main class="layout-main"> <el-main
class="layout-main"
:class="{ 'layout-main-overfow-hidden': useroute.meta.isDefaultChat }"
>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition :name="transitionName" mode="out-in" appear> <transition :name="transitionName" mode="out-in" appear>
<keep-alive :max="10" :include="keepAliveStore.keepAliveName"> <keep-alive :max="10" :include="keepAliveStore.keepAliveName">
@ -36,7 +39,7 @@ provide('refresh', refreshMainPage);
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.layout-main { .layout-main-overfow-hidden {
overflow: hidden; overflow: hidden;
} }

View File

@ -4,13 +4,7 @@ import ChatDefaul from '@/pages/chat/layouts/chatDefaul/index.vue';
import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue'; import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue';
const route = useRoute(); const route = useRoute();
const sessionId = computed(() => route.params?.id); const sessionId = computed(() => route.params?.id);
console.log(sessionId.value);
// const a = JSON.parse(`{"id":"chatcmpl-BVD1f4snw4KHOCKIgu4JOMoZbOwRh","object":"chat.completion.chunk","created":1746777939,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_ded0d14823","choices":[{"delta":{"content":"","role":"assistant"},"logprobs":null,"finish_reason":null,"index":0}],"usage":null}`)
// console.log(a);
</script> </script>
<template> <template>

View File

@ -1,16 +1,36 @@
<!-- 每个回话对应的聊天内容 --> <!-- 每个回话对应的聊天内容 -->
<script setup lang="ts"> <script setup lang="ts">
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
import { Loading, Position } from '@element-plus/icons-vue';
import { useXStream } from 'vue-element-plus-x';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useChatStore } from '@/stores/modules/chat'; import { useChatStore } from '@/stores/modules/chat';
interface ChatWithIdProps {} type MessageItem = BubbleProps & {
key: number;
role: 'ai' | 'user' | 'system';
avatar: string;
thinkingStatus?: ThinkingStatus;
expanded?: boolean;
};
const { startStream, cancel, data, error, isLoading } = useXStream();
const BASE_URL = 'https://api.siliconflow.cn/v1/chat/completions';
//
const API_KEY = 'sk-vfjyscildobjnrijtcllnkhtcouidcxdgjxtldzqzeowrbga';
const MODEL = 'THUDM/GLM-Z1-9B-0414';
const props = withDefaults(defineProps<ChatWithIdProps>(), {});
const route = useRoute(); const route = useRoute();
const chatStore = useChatStore(); const chatStore = useChatStore();
const senderValue = ref(''); const isDeepThinking = computed(() => chatStore.isDeepThinking);
const inputValue = ref('帮我写一篇小米手机介绍');
console.log('props ==> ', props); const senderRef = ref<any>(null);
const bubbleItems = ref<MessageItem[]>([]);
const bubbleListRef = ref<BubbleListInstance | null>(null);
const processedIndex = ref(0);
watch( watch(
() => route.params?.id, () => route.params?.id,
@ -19,19 +39,273 @@ watch(
chatStore.requestChatList(Number(_id_)); chatStore.requestChatList(Number(_id_));
const v = localStorage.getItem('chatContent'); const v = localStorage.getItem('chatContent');
if (v) { if (v) {
senderValue.value = v; inputValue.value = v;
localStorage.removeItem('chatContent'); localStorage.removeItem('chatContent');
// //
console.log('发送消息 v', v); console.log('发送消息 v', v);
setTimeout(() => {
startSSE();
}, 350);
} }
} }
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true },
); );
//
function setIsDeepThinking() {
chatStore.setDeepThinking(!chatStore.isDeepThinking);
}
//
function handleDataChunk(chunk: string) {
if (chunk === ' [DONE]') {
console.log('数据接收完毕');
//
if (bubbleItems.value.length) {
bubbleItems.value[bubbleItems.value.length - 1].typing = false;
}
cancel();
return;
}
try {
// console.log('New chunk:', JSON.parse(chunk))
const reasoningChunk = JSON.parse(chunk).choices[0].delta.reasoning_content;
if (reasoningChunk) {
//
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'thinking';
bubbleItems.value[bubbleItems.value.length - 1].loading = true;
if (bubbleItems.value.length) {
bubbleItems.value[bubbleItems.value.length - 1].reasoning_content += reasoningChunk;
}
}
const parsedChunk = JSON.parse(chunk).choices[0].delta.content;
if (parsedChunk) {
//
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'end';
bubbleItems.value[bubbleItems.value.length - 1].loading = false;
if (bubbleItems.value.length) {
bubbleItems.value[bubbleItems.value.length - 1].content += parsedChunk;
}
}
}
catch (err) {
console.error('解析数据时出错:', err);
}
}
watch(
data,
() => {
for (let i = processedIndex.value; i < data.value.length; i++) {
const chunk = data.value[i].data;
handleDataChunk(chunk);
processedIndex.value++;
}
},
{ deep: true },
);
//
function handleError(err: any) {
console.error('Fetch error:', err);
}
async function startSSE() {
try {
//
console.log('inputValue.value', inputValue.value);
addMessage(inputValue.value, true);
addMessage('', false);
// BubbleList
bubbleListRef.value!.scrollToBottom();
const response = await fetch(BASE_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify({
model: MODEL,
messages: bubbleItems.value
.filter((item: any) => item.role === 'user')
.map((item: any) => ({
role: item.role,
content: item.content,
})),
stream: true,
}),
});
const readableStream = response.body!;
//
processedIndex.value = 0;
await startStream({ readableStream });
}
catch (err) {
handleError(err);
}
}
// -
function addMessage(message: string, isUser: boolean) {
const i = bubbleItems.value.length;
const obj: MessageItem = {
key: i,
avatar: isUser
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
avatarSize: '48px',
role: isUser ? 'user' : 'system',
placement: isUser ? 'end' : 'start',
isMarkdown: !isUser,
variant: 'shadow',
shape: 'corner',
// maxWidth: '500px',
typing: isUser ? false : { step: 2, suffix: '❤️‍🔥', interval: 80 },
isFog: isUser ? false : { bgColor: '#FFFFFF' },
loading: !isUser,
content: message || '',
reasoning_content: '',
thinkingStatus: 'start',
};
bubbleItems.value.push(obj);
}
//
function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
console.log('value', payload.value, 'status', payload.status);
}
</script> </script>
<template> <template>
<div>新建对话当前页面对话id: {{ route.params?.id }}</div> <div class="chat-with-id-container">
<div class="chat-warp">
<div v-if="error" class="error">
{{ error.message }}
</div>
<BubbleList ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 240px)">
<template #header="{ item }">
<Thinking
v-if="item.reasoning_content"
:content="item.reasoning_content"
:status="item.thinkingStatus"
class="thinking-chain-warp"
@change="handleChange"
/>
</template>
</BubbleList>
<Sender
ref="senderRef"
v-model="inputValue"
class="chat-defaul-sender"
:auto-size="{
maxRows: 6,
minRows: 2,
}"
variant="updown"
clearable
allow-speech
@submit="startSSE"
>
<template #prefix>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
<div
class="flex items-center gap-4px px-12px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
>
<el-icon>
<Paperclip />
</el-icon>
</div>
<div
:class="{ 'is-select': isDeepThinking }"
class="flex items-center gap-4px px-10px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
@click="setIsDeepThinking"
>
<el-icon>
<ElementPlus />
</el-icon>
<span>深度思考</span>
</div>
</div>
</template>
<template #action-list>
<div class="footer-container">
<el-button v-if="!isLoading" type="danger" circle @click="senderRef.submit()">
<el-icon>
<Position />
</el-icon>
</el-button>
<el-button v-if="isLoading" type="primary" @click="cancel">
<el-icon class="is-loading">
<Loading />
</el-icon>
</el-button>
</div>
</template>
</Sender>
</div>
</div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
.chat-with-id-container {
position: relative;
width: 100%;
max-width: 800px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
.chat-warp {
width: 100%;
height: calc(100vh - 60px);
display: flex;
flex-direction: column;
justify-content: space-between;
.thinking-chain-warp {
margin-bottom: 12px;
}
}
:deep() {
.el-bubble-list {
padding-top: 24px;
}
.el-bubble {
padding: 0 12px;
padding-bottom: 24px;
}
.el-typewriter {
border-radius: 12px;
overflow: hidden;
}
.markdown-body {
background-color: transparent;
}
}
.chat-defaul-sender {
margin-bottom: 22px;
width: 100%;
}
.is-select {
color: var(--el-color-primary, #409eff);
border: 1px solid var(--el-color-primary, #409eff);
border-radius: 15px;
}
}
</style>

View File

@ -3,7 +3,7 @@
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable // biome-ignore lint: disable
export {}; export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {