forked from Leaf/amber-ui
改进 消息列表组件和设置组件
This commit is contained in:
parent
4a6cd454fe
commit
8b0c1284a6
3
src/components.d.ts
vendored
3
src/components.d.ts
vendored
@ -8,7 +8,8 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
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']
|
||||
Container: typeof import('./components/Container.vue')['default']
|
||||
LeftSettings: typeof import('./components/LeftSettings.vue')['default']
|
||||
|
@ -43,13 +43,30 @@
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="chap2" tab="助理">
|
||||
<Assistants />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="chap3" tab="工具">
|
||||
“威尔!着火了!快来帮忙!”我听到女朋友大喊。现在一个难题在我面前——是恢复一个重要的
|
||||
Amazon 服务,还是救公寓的火。<br /><br />
|
||||
我的脑海中忽然出现了 Amazon
|
||||
著名的领导力准则”客户至上“,有很多的客户还依赖我们的服务,我不能让他们失望!所以着火也不管了,女朋友喊我也无所谓,我开始
|
||||
debug 这个线上问题。
|
||||
</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
|
||||
|
6
src/components/assistants/index.vue
Normal file
6
src/components/assistants/index.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
助理
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
@ -20,7 +20,10 @@
|
||||
</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
|
||||
class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg"
|
||||
v-if="toolCalling"
|
||||
@ -111,13 +114,20 @@
|
||||
</div>
|
||||
</n-spin>
|
||||
</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>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, onUnmounted, Ref, ref } from "vue";
|
||||
import {
|
||||
SendOutline,
|
||||
MicOutline,
|
||||
@ -129,6 +139,8 @@ import getApi from "@/plugins/api";
|
||||
import MessageList from "./MessageList.vue";
|
||||
import { useChatStore } from "@/stores/chat";
|
||||
import router from "@/router";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import element from "@/config/element";
|
||||
|
||||
// 获取组件传入的 chatId
|
||||
const chatId: Ref<string | number | undefined | null> = ref(null);
|
||||
@ -165,6 +177,8 @@ const toolCalling = ref(false);
|
||||
const fileUpload = ref();
|
||||
const uploading = ref(false);
|
||||
const autoScroll = ref(true);
|
||||
const onBottom = ref(false);
|
||||
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
// 带 shift 不触发
|
||||
@ -464,7 +478,10 @@ function streamChat(streamId: String, redirect = false) {
|
||||
}
|
||||
|
||||
if (autoScroll.value) {
|
||||
// 滚动到
|
||||
element.mainContainer?.scrollTo({
|
||||
top: "999999",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,6 +513,40 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
@ -3,10 +3,29 @@
|
||||
<n-divider> <small class="select-none">这是聊天的开头</small> </n-divider>
|
||||
|
||||
<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">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center flex-nowrap">
|
||||
<!-- <vue-markdown-it
|
||||
:source="message.content"
|
||||
:options="markdownOptions"
|
||||
@ -14,19 +33,16 @@
|
||||
/> -->
|
||||
<!-- <v-md-preview :text="message.content" height="500px"></v-md-preview> -->
|
||||
<div v-html="mdIt.render(message.content)"></div>
|
||||
<n-avatar round size="large" :src="userStore.user.avatar" />
|
||||
</div>
|
||||
<n-avatar round size="large" :src="userStore.user.avatar" />
|
||||
</n-flex>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="message.role === 'assistant' && message.content"
|
||||
class="mt-10 mb-10"
|
||||
>
|
||||
<div v-else-if="message.role === 'assistant' && message.content">
|
||||
<!-- 助理消息 -->
|
||||
<n-flex justify="start">
|
||||
<n-avatar round size="large" :src="leaflowPng" />
|
||||
<n-flex justify="start" class="!flex-nowrap">
|
||||
<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
|
||||
:source="message.content"
|
||||
:options="markdownOptions"
|
||||
@ -56,6 +72,7 @@ import markdownKatex from "@traptitech/markdown-it-katex";
|
||||
import markdownIt from "markdown-it";
|
||||
// highlightjs
|
||||
import hljs from "highlight.js";
|
||||
import config from "@/config/config";
|
||||
|
||||
const mdIt = markdownIt();
|
||||
|
||||
@ -83,4 +100,5 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const chat_messages = toRef(props, "chat_messages") as Ref<EntityChatMessage[]>;
|
||||
const fileBaseUrl = config.backend + "/api/v1/files";
|
||||
</script>
|
||||
|
@ -20,7 +20,10 @@
|
||||
</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
|
||||
class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg"
|
||||
v-if="toolCalling"
|
||||
@ -111,13 +114,20 @@
|
||||
</div>
|
||||
</n-spin>
|
||||
</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>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, onUnmounted, Ref, ref } from "vue";
|
||||
import {
|
||||
SendOutline,
|
||||
MicOutline,
|
||||
@ -129,6 +139,8 @@ import getApi from "@/plugins/api";
|
||||
import MessageList from "./MessageList.vue";
|
||||
import { useChatStore } from "@/stores/chat";
|
||||
import router from "@/router";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import element from "@/config/element";
|
||||
|
||||
// 获取组件传入的 chatId
|
||||
const chatId: Ref<string | number | undefined | null> = ref(null);
|
||||
@ -165,6 +177,8 @@ const toolCalling = ref(false);
|
||||
const fileUpload = ref();
|
||||
const uploading = ref(false);
|
||||
const autoScroll = ref(true);
|
||||
const onBottom = ref(false);
|
||||
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
// 带 shift 不触发
|
||||
@ -464,7 +478,10 @@ function streamChat(streamId: String, redirect = false) {
|
||||
}
|
||||
|
||||
if (autoScroll.value) {
|
||||
// 滚动到
|
||||
element.mainContainer?.scrollTo({
|
||||
top: "999999",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,6 +513,40 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
@ -16,7 +16,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
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);
|
||||
|
||||
|
3
src/config/element.ts
Normal file
3
src/config/element.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
mainContainer: HTMLElement
|
||||
}
|
@ -4,10 +4,21 @@ import { useUserStore } from "../stores/user";
|
||||
import Guest from "../pages/guest/index.vue";
|
||||
import router from "../router";
|
||||
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 userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const mainContainer = ref();
|
||||
|
||||
onMounted(() => {
|
||||
element.mainContainer = mainContainer.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -23,9 +34,10 @@ const route = useRoute();
|
||||
></Header>
|
||||
|
||||
<n-layout
|
||||
:native-scrollbar="false"
|
||||
:native-scrollbar="isMobile"
|
||||
position="absolute"
|
||||
style="margin-top: var(--header-height)"
|
||||
ref="mainContainer"
|
||||
>
|
||||
<!-- <n-layout-sider
|
||||
v-if="userStore.logined && !isMobile"
|
||||
@ -42,12 +54,16 @@ const route = useRoute();
|
||||
>
|
||||
<Menu v-show="!isMobile"></Menu>
|
||||
</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'" />
|
||||
<Container v-else /> -->
|
||||
<Guest v-if="!userStore.logined && !currentRoute?.startsWith('/auth')" />
|
||||
<div v-else>
|
||||
<Guest
|
||||
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> -->
|
||||
|
||||
<router-view :key="route.path"> </router-view>
|
||||
|
@ -26,38 +26,41 @@
|
||||
class="select-none"
|
||||
>
|
||||
<n-drawer-content
|
||||
title="有什么我可以帮您的吗"
|
||||
title="Amberlet"
|
||||
closable
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<LeftSettings></LeftSettings>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<n-icon
|
||||
size="24"
|
||||
style="margin-left: 12px"
|
||||
<div
|
||||
@click="showDrawer = true"
|
||||
class="cursor-pointer"
|
||||
class="cursor-pointer ml-3 flex justify-center items-center"
|
||||
>
|
||||
<menu-outline />
|
||||
</n-icon>
|
||||
<n-icon size="24">
|
||||
<menu-outline />
|
||||
</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 class="flex items-center justify-center">
|
||||
<!-- 中间部分 -->
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<img
|
||||
:src="leaflowpng"
|
||||
class="w-10 cursor-pointer hidden lg:block"
|
||||
@click="backToHome"
|
||||
/>
|
||||
</template>
|
||||
<span> Leaflow 利飞 </span>
|
||||
</n-popover>
|
||||
<n-grid-item class="flex items-center justify-center select-none">
|
||||
<div v-show="!isMobile">
|
||||
<!-- 中间部分 -->
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<img
|
||||
:src="leaflowpng"
|
||||
class="w-10 cursor-pointer block select-none"
|
||||
@click="backToHome"
|
||||
/>
|
||||
</template>
|
||||
<span> Leaflow 利飞 </span>
|
||||
</n-popover>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
|
||||
<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 { useAppStore } from "../stores/app";
|
||||
import { useUserStore } from "../stores/user";
|
||||
import { useIsMobile } from "../utils/composables";
|
||||
import { useIsMobile, useIsTablet } from "../utils/composables";
|
||||
import {
|
||||
MenuOutline,
|
||||
PersonOutline,
|
||||
@ -128,8 +131,6 @@ import leaflowpng from "@/assets/images/leaflow.png";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isMobile = useIsMobile();
|
||||
// const isTablet = useIsTablet();
|
||||
const appStore = useAppStore();
|
||||
const showDrawer = ref(false);
|
||||
const width = ref(200);
|
||||
// const chatStore = useChatStore();
|
||||
|
Loading…
Reference in New Issue
Block a user