1
0
forked from Leaf/amber-ui

改进 消息列表组件和设置组件

This commit is contained in:
Twilight 2024-09-13 11:15:10 +08:00
parent 4a6cd454fe
commit 8b0c1284a6
10 changed files with 210 additions and 46 deletions

3
src/components.d.ts vendored
View File

@ -8,7 +8,8 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AssistantMenu: typeof import('./components/AssistantMenu.vue')['default'] AssistantMenu: typeof import('./components/AssistantMenu.vue')['default']
Chat: typeof import('./components/chat/chat.vue')['default'] Assistants: typeof import('./components/assistants/index.vue')['default']
Chat: typeof import('./components/chat/Chat.vue')['default']
ChatMenu: typeof import('./components/ChatMenu.vue')['default'] ChatMenu: typeof import('./components/ChatMenu.vue')['default']
Container: typeof import('./components/Container.vue')['default'] Container: typeof import('./components/Container.vue')['default']
LeftSettings: typeof import('./components/LeftSettings.vue')['default'] LeftSettings: typeof import('./components/LeftSettings.vue')['default']

View File

@ -43,13 +43,30 @@
</div> </div>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="chap2" tab="助理"> <n-tab-pane name="chap2" tab="助理">
<Assistants />
</n-tab-pane>
<n-tab-pane name="chap3" tab="工具">
威尔着火了快来帮忙我听到女朋友大喊现在一个难题在我面前是恢复一个重要的 威尔着火了快来帮忙我听到女朋友大喊现在一个难题在我面前是恢复一个重要的
Amazon 服务还是救公寓的火<br /><br /> Amazon 服务还是救公寓的火<br /><br />
我的脑海中忽然出现了 Amazon 我的脑海中忽然出现了 Amazon
著名的领导力准则客户至上有很多的客户还依赖我们的服务我不能让他们失望所以着火也不管了女朋友喊我也无所谓我开始 著名的领导力准则客户至上有很多的客户还依赖我们的服务我不能让他们失望所以着火也不管了女朋友喊我也无所谓我开始
debug 这个线上问题 debug 这个线上问题
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="chap3" tab="工具"> <n-tab-pane name="documents" tab="文档">
威尔着火了快来帮忙我听到女朋友大喊现在一个难题在我面前是恢复一个重要的
Amazon 服务还是救公寓的火<br /><br />
我的脑海中忽然出现了 Amazon
著名的领导力准则客户至上有很多的客户还依赖我们的服务我不能让他们失望所以着火也不管了女朋友喊我也无所谓我开始
debug 这个线上问题
</n-tab-pane>
<n-tab-pane name="chap4" tab="记忆">
威尔着火了快来帮忙我听到女朋友大喊现在一个难题在我面前是恢复一个重要的
Amazon 服务还是救公寓的火<br /><br />
我的脑海中忽然出现了 Amazon
著名的领导力准则客户至上有很多的客户还依赖我们的服务我不能让他们失望所以着火也不管了女朋友喊我也无所谓我开始
debug 这个线上问题
</n-tab-pane>
<n-tab-pane name="chap5" tab="账户">
威尔着火了快来帮忙我听到女朋友大喊现在一个难题在我面前是恢复一个重要的 威尔着火了快来帮忙我听到女朋友大喊现在一个难题在我面前是恢复一个重要的
Amazon 服务还是救公寓的火<br /><br /> Amazon 服务还是救公寓的火<br /><br />
我的脑海中忽然出现了 Amazon 我的脑海中忽然出现了 Amazon

View File

@ -0,0 +1,6 @@
<template>
助理
</template>
<script setup lang="ts">
</script>

View File

@ -20,7 +20,10 @@
</div> </div>
</div> </div>
<div class="fixed bottom-0 left-0 right-0 pb-10"> <div
class="fixed bottom-0 left-0 right-0"
:class="onBottom ? 'hidden' : 'mb-6'"
>
<div <div
class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg" class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg"
v-if="toolCalling" v-if="toolCalling"
@ -111,13 +114,20 @@
</div> </div>
</n-spin> </n-spin>
</div> </div>
<n-text
depth="3"
class="text-center block mt-2 mb-2 text-sm select-none"
v-show="onBottom"
>
AI 也有可能犯错误请在使用之前核查信息
</n-text>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "../../stores/user"; import { useUserStore } from "../../stores/user";
import { onMounted, ref } from "vue"; import { onMounted, onUnmounted, Ref, ref } from "vue";
import { import {
SendOutline, SendOutline,
MicOutline, MicOutline,
@ -129,6 +139,8 @@ import getApi from "@/plugins/api";
import MessageList from "./MessageList.vue"; import MessageList from "./MessageList.vue";
import { useChatStore } from "@/stores/chat"; import { useChatStore } from "@/stores/chat";
import router from "@/router"; import router from "@/router";
import { useAppStore } from "@/stores/app";
import element from "@/config/element";
// chatId // chatId
const chatId: Ref<string | number | undefined | null> = ref(null); const chatId: Ref<string | number | undefined | null> = ref(null);
@ -165,6 +177,8 @@ const toolCalling = ref(false);
const fileUpload = ref(); const fileUpload = ref();
const uploading = ref(false); const uploading = ref(false);
const autoScroll = ref(true); const autoScroll = ref(true);
const onBottom = ref(false);
function onKeydown(e: KeyboardEvent) { function onKeydown(e: KeyboardEvent) {
// shift // shift
@ -464,7 +478,10 @@ function streamChat(streamId: String, redirect = false) {
} }
if (autoScroll.value) { if (autoScroll.value) {
// element.mainContainer?.scrollTo({
top: "999999",
behavior: "smooth",
});
} }
} }
@ -496,6 +513,40 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
chatStore.currentChatId = 0; chatStore.currentChatId = 0;
}); });
const uploadFile = () => {
if (!fileUpload.value) {
return;
}
uploading.value = true;
getApi()
.ChatMessage.apiV1ChatsIdFilesPost(
chatId,
{
file: fileUpload.value,
url: "",
},
{
headers: {
"Content-Type": "multipart/form-data",
},
}
)
.then((r) => {
//
if (r.status === 200 || r.status === 201) {
fileUpload.value = null;
getChatMessages();
}
})
.catch((err) => {
alert(err.response.data.message);
})
.finally(() => {
uploading.value = false;
});
};
</script> </script>
<style scoped> <style scoped>

View File

@ -3,10 +3,29 @@
<n-divider> <small class="select-none">这是聊天的开头</small> </n-divider> <n-divider> <small class="select-none">这是聊天的开头</small> </n-divider>
<div v-for="(message, index) in chat_messages" :key="index"> <div v-for="(message, index) in chat_messages" :key="index">
<div v-if="message.role === 'user' && message.content"> <div class="mt-10 mb-10"></div>
<div v-if="message.role === 'file'">
<!-- 文件类型 -->
<n-flex justify="end">
<div class="flex items-center flex-nowrap">
<!-- 如果是 user file -->
<n-image
v-if="message.user_file"
width="100"
:src="fileBaseUrl + '/' + message.user_file.id + '/download'"
/>
<n-image
v-if="message.file"
width="100"
:src="fileBaseUrl + '/' + message.file.id + '/download'"
/>
</div>
</n-flex>
</div>
<div v-else-if="message.role === 'user' && message.content">
<!-- 用户消息 --> <!-- 用户消息 -->
<n-flex justify="end"> <n-flex justify="end">
<div class="flex items-center"> <div class="flex items-center flex-nowrap">
<!-- <vue-markdown-it <!-- <vue-markdown-it
:source="message.content" :source="message.content"
:options="markdownOptions" :options="markdownOptions"
@ -14,19 +33,16 @@
/> --> /> -->
<!-- <v-md-preview :text="message.content" height="500px"></v-md-preview> --> <!-- <v-md-preview :text="message.content" height="500px"></v-md-preview> -->
<div v-html="mdIt.render(message.content)"></div> <div v-html="mdIt.render(message.content)"></div>
</div>
<n-avatar round size="large" :src="userStore.user.avatar" /> <n-avatar round size="large" :src="userStore.user.avatar" />
</div>
</n-flex> </n-flex>
</div> </div>
<div <div v-else-if="message.role === 'assistant' && message.content">
v-else-if="message.role === 'assistant' && message.content"
class="mt-10 mb-10"
>
<!-- 助理消息 --> <!-- 助理消息 -->
<n-flex justify="start"> <n-flex justify="start" class="!flex-nowrap">
<n-avatar round size="large" :src="leaflowPng" /> <n-avatar round size="large" :src="leaflowPng" class="min-w-10 min-h-10"/>
<div class="flex items-center"> <div class="flex items-center flex-nowrap">
<!-- <vue-markdown-it <!-- <vue-markdown-it
:source="message.content" :source="message.content"
:options="markdownOptions" :options="markdownOptions"
@ -56,6 +72,7 @@ import markdownKatex from "@traptitech/markdown-it-katex";
import markdownIt from "markdown-it"; import markdownIt from "markdown-it";
// highlightjs // highlightjs
import hljs from "highlight.js"; import hljs from "highlight.js";
import config from "@/config/config";
const mdIt = markdownIt(); const mdIt = markdownIt();
@ -83,4 +100,5 @@ const props = defineProps({
}); });
const chat_messages = toRef(props, "chat_messages") as Ref<EntityChatMessage[]>; const chat_messages = toRef(props, "chat_messages") as Ref<EntityChatMessage[]>;
const fileBaseUrl = config.backend + "/api/v1/files";
</script> </script>

View File

@ -20,7 +20,10 @@
</div> </div>
</div> </div>
<div class="fixed bottom-0 left-0 right-0 pb-10"> <div
class="fixed bottom-0 left-0 right-0"
:class="onBottom ? 'hidden' : 'mb-6'"
>
<div <div
class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg" class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg"
v-if="toolCalling" v-if="toolCalling"
@ -111,13 +114,20 @@
</div> </div>
</n-spin> </n-spin>
</div> </div>
<n-text
depth="3"
class="text-center block mt-2 mb-2 text-sm select-none"
v-show="onBottom"
>
AI 也有可能犯错误请在使用之前核查信息
</n-text>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "../../stores/user"; import { useUserStore } from "../../stores/user";
import { onMounted, ref } from "vue"; import { onMounted, onUnmounted, Ref, ref } from "vue";
import { import {
SendOutline, SendOutline,
MicOutline, MicOutline,
@ -129,6 +139,8 @@ import getApi from "@/plugins/api";
import MessageList from "./MessageList.vue"; import MessageList from "./MessageList.vue";
import { useChatStore } from "@/stores/chat"; import { useChatStore } from "@/stores/chat";
import router from "@/router"; import router from "@/router";
import { useAppStore } from "@/stores/app";
import element from "@/config/element";
// chatId // chatId
const chatId: Ref<string | number | undefined | null> = ref(null); const chatId: Ref<string | number | undefined | null> = ref(null);
@ -165,6 +177,8 @@ const toolCalling = ref(false);
const fileUpload = ref(); const fileUpload = ref();
const uploading = ref(false); const uploading = ref(false);
const autoScroll = ref(true); const autoScroll = ref(true);
const onBottom = ref(false);
function onKeydown(e: KeyboardEvent) { function onKeydown(e: KeyboardEvent) {
// shift // shift
@ -464,7 +478,10 @@ function streamChat(streamId: String, redirect = false) {
} }
if (autoScroll.value) { if (autoScroll.value) {
// element.mainContainer?.scrollTo({
top: "999999",
behavior: "smooth",
});
} }
} }
@ -496,6 +513,40 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
chatStore.currentChatId = 0; chatStore.currentChatId = 0;
}); });
const uploadFile = () => {
if (!fileUpload.value) {
return;
}
uploading.value = true;
getApi()
.ChatMessage.apiV1ChatsIdFilesPost(
chatId,
{
file: fileUpload.value,
url: "",
},
{
headers: {
"Content-Type": "multipart/form-data",
},
}
)
.then((r) => {
//
if (r.status === 200 || r.status === 201) {
fileUpload.value = null;
getChatMessages();
}
})
.catch((err) => {
alert(err.response.data.message);
})
.finally(() => {
uploading.value = false;
});
};
</script> </script>
<style scoped> <style scoped>

View File

@ -16,7 +16,7 @@ if (process.env.NODE_ENV === "production") {
config.oauth_client_id = "16"; config.oauth_client_id = "16";
} }
// config.backend = "https://amber-api.leaflow.cn"; config.backend = "https://amber-api.leaflow.cn";
// console.log("api endpoint: " + config.backend); // console.log("api endpoint: " + config.backend);

3
src/config/element.ts Normal file
View File

@ -0,0 +1,3 @@
export default {
mainContainer: HTMLElement
}

View File

@ -4,10 +4,21 @@ import { useUserStore } from "../stores/user";
import Guest from "../pages/guest/index.vue"; import Guest from "../pages/guest/index.vue";
import router from "../router"; import router from "../router";
import Header from "./Header.vue"; import Header from "./Header.vue";
import { useAppStore } from "@/stores/app";
import element from "@/config/element";
import { useIsMobile } from "@/utils/composables";
const currentRoute = computed(() => router.currentRoute.value.name); const currentRoute = computed(() => router.currentRoute.value.name);
const userStore = useUserStore(); const userStore = useUserStore();
const route = useRoute(); const route = useRoute();
const isMobile = useIsMobile();
const mainContainer = ref();
onMounted(() => {
element.mainContainer = mainContainer.value;
});
</script> </script>
<template> <template>
@ -23,9 +34,10 @@ const route = useRoute();
></Header> ></Header>
<n-layout <n-layout
:native-scrollbar="false" :native-scrollbar="isMobile"
position="absolute" position="absolute"
style="margin-top: var(--header-height)" style="margin-top: var(--header-height)"
ref="mainContainer"
> >
<!-- <n-layout-sider <!-- <n-layout-sider
v-if="userStore.logined && !isMobile" v-if="userStore.logined && !isMobile"
@ -42,12 +54,16 @@ const route = useRoute();
> >
<Menu v-show="!isMobile"></Menu> <Menu v-show="!isMobile"></Menu>
</n-layout-sider> --> </n-layout-sider> -->
<n-back-top v-if="!isMobile" :right="100" />
<n-layout :native-scrollbar="false"> <n-layout :native-scrollbar="isMobile">
<!-- <Guest v-if="!userStore.logined && currentRoute != '/auth/login'" /> <!-- <Guest v-if="!userStore.logined && currentRoute != '/auth/login'" />
<Container v-else /> --> <Container v-else /> -->
<Guest v-if="!userStore.logined && !currentRoute?.startsWith('/auth')" /> <Guest
<div v-else> v-if="!userStore.logined && !currentRoute?.startsWith('/auth')"
style="min-height: 85vh"
/>
<div v-else class="pt-2">
<!-- <div style="height: calc(var(--header-height)*2)"></div> --> <!-- <div style="height: calc(var(--header-height)*2)"></div> -->
<router-view :key="route.path"> </router-view> <router-view :key="route.path"> </router-view>

View File

@ -26,38 +26,41 @@
class="select-none" class="select-none"
> >
<n-drawer-content <n-drawer-content
title="有什么我可以帮您的吗" title="Amberlet"
closable closable
:native-scrollbar="false" :native-scrollbar="false"
> >
<LeftSettings></LeftSettings> <LeftSettings></LeftSettings>
</n-drawer-content> </n-drawer-content>
</n-drawer> </n-drawer>
<n-icon <div
size="24"
style="margin-left: 12px"
@click="showDrawer = true" @click="showDrawer = true"
class="cursor-pointer" class="cursor-pointer ml-3 flex justify-center items-center"
> >
<n-icon size="24">
<menu-outline /> <menu-outline />
</n-icon> </n-icon>
<span class="ml-1.5"> Leaflow 利飞 </span>
</div>
<!-- 更新状态 --> <!-- 更新状态 -->
<div v-show="appStore.updating">正在更新数据</div> <!-- <div v-show="appStore.updating">正在更新数据</div> -->
</n-grid-item> </n-grid-item>
<n-grid-item class="flex items-center justify-center"> <n-grid-item class="flex items-center justify-center select-none">
<div v-show="!isMobile">
<!-- 中间部分 --> <!-- 中间部分 -->
<n-popover trigger="hover"> <n-popover trigger="hover">
<template #trigger> <template #trigger>
<img <img
:src="leaflowpng" :src="leaflowpng"
class="w-10 cursor-pointer hidden lg:block" class="w-10 cursor-pointer block select-none"
@click="backToHome" @click="backToHome"
/> />
</template> </template>
<span> Leaflow 利飞 </span> <span> Leaflow 利飞 </span>
</n-popover> </n-popover>
</div>
</n-grid-item> </n-grid-item>
<n-grid-item class="flex items-center justify-end mr-1.5"> <n-grid-item class="flex items-center justify-end mr-1.5">
@ -114,7 +117,7 @@ import UserMenu from "../components/UserMenu.vue";
import AssistantMenu from "../components/AssistantMenu.vue"; import AssistantMenu from "../components/AssistantMenu.vue";
import { useAppStore } from "../stores/app"; import { useAppStore } from "../stores/app";
import { useUserStore } from "../stores/user"; import { useUserStore } from "../stores/user";
import { useIsMobile } from "../utils/composables"; import { useIsMobile, useIsTablet } from "../utils/composables";
import { import {
MenuOutline, MenuOutline,
PersonOutline, PersonOutline,
@ -128,8 +131,6 @@ import leaflowpng from "@/assets/images/leaflow.png";
const userStore = useUserStore(); const userStore = useUserStore();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// const isTablet = useIsTablet();
const appStore = useAppStore();
const showDrawer = ref(false); const showDrawer = ref(false);
const width = ref(200); const width = ref(200);
// const chatStore = useChatStore(); // const chatStore = useChatStore();