|
|
@@ -1,8 +1,8 @@
|
|
|
<template>
|
|
|
<McLayout class="container">
|
|
|
- <Header @logoClicked="startPage = true"></Header>
|
|
|
+ <Header @logoClicked="startChat = true"></Header>
|
|
|
<McLayoutContent
|
|
|
- v-if="startPage"
|
|
|
+ v-if="!startChat"
|
|
|
style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px"
|
|
|
>
|
|
|
<McIntroduction
|
|
|
@@ -15,24 +15,26 @@
|
|
|
:list="introPrompt.list"
|
|
|
:direction="introPrompt.direction"
|
|
|
class="intro-prompt"
|
|
|
- @itemClick="onSubmit($event.label)"
|
|
|
+ @itemClick="onItemClick($event.label)"
|
|
|
></McPrompt>
|
|
|
</McLayoutContent>
|
|
|
<McLayoutContent v-else>
|
|
|
<template v-for="(msg, idx) in messages" :key="idx">
|
|
|
- <McBubble
|
|
|
- v-if="msg.from === 'user'"
|
|
|
- :content="msg.content"
|
|
|
- :align="'right'"
|
|
|
- :avatarConfig="{ imgSrc: 'https://matechat.gitcode.com/png/demo/userAvatar.svg' }"
|
|
|
- >
|
|
|
+ <McBubble v-if="msg.from === 'user'" :content="msg.content" :align="'right'" :avatarConfig="msg.avatarConfig"></McBubble>
|
|
|
+ <McBubble v-else :content="msg.content" :loading="msg.loading ?? false" :avatarConfig="msg.avatarConfig">
|
|
|
+ <template #bottom>
|
|
|
+ <div class="bubble-bottom-operations">
|
|
|
+ <i class="icon-copy-new"></i>
|
|
|
+ <i class="icon-like"></i>
|
|
|
+ <i class="icon-dislike"></i>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</McBubble>
|
|
|
- <McBubble v-else :content="msg.content" :avatarConfig="{ imgSrc: 'https://matechat.gitcode.com/logo.svg' }"> </McBubble>
|
|
|
</template>
|
|
|
</McLayoutContent>
|
|
|
<div class="shortcut" style="display: flex; align-items: center; gap: 8px">
|
|
|
<McPrompt
|
|
|
- v-if="!startPage"
|
|
|
+ v-if="startChat"
|
|
|
:list="simplePrompt"
|
|
|
:direction="'horizontal'"
|
|
|
style="flex: 1"
|
|
|
@@ -45,165 +47,165 @@
|
|
|
title="新建对话"
|
|
|
size="large"
|
|
|
@click="
|
|
|
- startPage = true;
|
|
|
+ startChat = true;
|
|
|
messages = [];
|
|
|
"
|
|
|
/>
|
|
|
</div>
|
|
|
<McLayoutSender>
|
|
|
- <McInput
|
|
|
+ <ChatInput
|
|
|
:value="inputValue"
|
|
|
- :maxLength="2000"
|
|
|
- @change="(value: string) => inputValue = value"
|
|
|
@submit="onSubmit"
|
|
|
- >
|
|
|
- <template #extra>
|
|
|
- <div class="input-foot-wrapper">
|
|
|
- <div class="input-foot-left">
|
|
|
- <span v-for="(item, index) in inputFootIcons" :key="index">
|
|
|
- <i :class="item.icon"></i>
|
|
|
- {{ item.text }}
|
|
|
- </span>
|
|
|
- <span class="input-foot-dividing-line"></span>
|
|
|
- <span class="input-foot-maxlength">{{ inputValue.length }}/2000</span>
|
|
|
- </div>
|
|
|
- <div class="input-foot-right">
|
|
|
- <d-button icon="op-clearup" shape="round" :disabled="!inputValue" @click="inputValue = ''">清空输入</d-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </McInput>
|
|
|
+ @update:value="(value: string) => inputValue = value"
|
|
|
+ />
|
|
|
</McLayoutSender>
|
|
|
</McLayout>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, computed } from 'vue';
|
|
|
+import { ref, nextTick, onMounted } from 'vue';
|
|
|
import { useLocalStorage } from '@vueuse/core';
|
|
|
import OpenAI from 'openai';
|
|
|
import Header from './components/Header.vue';
|
|
|
+import ChatInput from './components/ChatInput.vue';
|
|
|
+import { introPrompt, simplePrompt, mockAnswer } from './mock.constants.ts';
|
|
|
+const startChat = ref(false);
|
|
|
+const conversationRef = ref();
|
|
|
+const themeService = (window as any)['devuiThemeService'];
|
|
|
+const theme = ref('light');
|
|
|
+const inputValue = ref('');
|
|
|
|
|
|
-interface Message {
|
|
|
- from: 'user' | 'model';
|
|
|
- content: string;
|
|
|
- loading?: boolean;
|
|
|
- id?: string;
|
|
|
- avatarConfig?: {
|
|
|
- imgSrc: string;
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-const client = reactive({
|
|
|
- instance: null as OpenAI | null,
|
|
|
-});
|
|
|
+const messages = ref<any[]>([]);
|
|
|
|
|
|
const settingsStore = useLocalStorage('ai-settings', {
|
|
|
apiBase: 'https://aiapi.magong.site/v1',
|
|
|
openaiApiKey: 'sk-jTzZGIAMZux11AA6666d54D5A27541C28c92Ca54F8D33c83',
|
|
|
- model: 'deepseek-chat',
|
|
|
+ model: 'glm-4-flash',
|
|
|
});
|
|
|
|
|
|
-client.instance = new OpenAI({
|
|
|
- apiKey: computed(() => settingsStore.value.openaiApiKey).value,
|
|
|
- baseURL: computed(() => settingsStore.value.apiBase).value,
|
|
|
+const client = ref<OpenAI>();
|
|
|
+ client.value = new OpenAI({
|
|
|
+ apiKey: settingsStore.value.openaiApiKey,
|
|
|
+ baseURL: settingsStore.value.apiBase,
|
|
|
dangerouslyAllowBrowser: true,
|
|
|
});
|
|
|
|
|
|
+const customerAvatar = {
|
|
|
+ imgSrc: 'https://matechat.gitcode.com/png/demo/userAvatar.svg',
|
|
|
+ width: 32,
|
|
|
+ height: 32,
|
|
|
+};
|
|
|
+const aiModelAvatar = {
|
|
|
+ imgSrc: 'https://matechat.gitcode.com/logo.svg',
|
|
|
+ width: 32,
|
|
|
+ height: 32,
|
|
|
+};
|
|
|
const description = [
|
|
|
'我正在开发一些常用的AI工具,你可以点击下方按钮访问相关工具,或者在对话栏输入工具名',
|
|
|
'如果你更多需求想实现,欢迎在下方社交账号联系我',
|
|
|
];
|
|
|
-const introPrompt = {
|
|
|
- direction: 'horizontal',
|
|
|
- list: [
|
|
|
- {
|
|
|
- value: 'quickSort',
|
|
|
- label: 'AI翻译PDF',
|
|
|
- iconConfig: { name: 'icon-info-o', color: '#5e7ce0' },
|
|
|
- desc: '利用AI强大分析能力,翻译PDF,不会打乱排版结构,翻译能保持段落格式、图标、表格等结构完整。',
|
|
|
- },
|
|
|
- ],
|
|
|
-};
|
|
|
-const simplePrompt = [
|
|
|
- {
|
|
|
- value: 'quickSort',
|
|
|
- iconConfig: { name: 'icon-info-o', color: '#5e7ce0' },
|
|
|
- label: '帮我写一个快速排序',
|
|
|
- },
|
|
|
- {
|
|
|
- value: 'helpMd',
|
|
|
- iconConfig: { name: 'icon-star', color: 'rgb(255, 215, 0)' },
|
|
|
- label: '你可以帮我做些什么?',
|
|
|
- },
|
|
|
-];
|
|
|
-const startPage = ref(true);
|
|
|
-const inputValue = ref('');
|
|
|
-const inputFootIcons = [
|
|
|
- { icon: 'icon-at', text: '智能体' },
|
|
|
- { icon: 'icon-standard', text: '词库' },
|
|
|
- { icon: 'icon-add', text: '附件' },
|
|
|
-];
|
|
|
|
|
|
-const messages = ref<any[]>([
|
|
|
- {
|
|
|
- from: 'user',
|
|
|
- content: '你好',
|
|
|
- },
|
|
|
- {
|
|
|
- from: 'model',
|
|
|
- content: '你好,我是 MateChat',
|
|
|
- id: 'init-msg',
|
|
|
- },
|
|
|
-]);
|
|
|
+// messages.value.push(
|
|
|
+// {
|
|
|
+// from: 'user',
|
|
|
+// content: '你好',
|
|
|
+// avatarPosition:'side-right',
|
|
|
+// avatarConfig: {...customerAvatar },
|
|
|
+// loading: false,
|
|
|
+// id: '',
|
|
|
+// },
|
|
|
+// {
|
|
|
+// content: '你好,我是AI助手,你可以问我任何问题',
|
|
|
+// from: 'ai-model',
|
|
|
+// avatarPosition: 'side-left',
|
|
|
+// avatarConfig: { ...aiModelAvatar },
|
|
|
+// loading: false,
|
|
|
|
|
|
-const onSubmit = async (evt: string) => {
|
|
|
- startPage.value = false;
|
|
|
+// })
|
|
|
+const onSubmit = (e: any, answer = undefined) => {
|
|
|
+ if (!e.trim()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
inputValue.value = '';
|
|
|
-
|
|
|
- // 用户发送消息
|
|
|
+ if (!messages.value.length) {
|
|
|
+ startChat.value = true;
|
|
|
+ }
|
|
|
messages.value.push({
|
|
|
from: 'user',
|
|
|
- content: evt,
|
|
|
- avatarConfig: { imgSrc: 'https://matechat.gitcode.com/png/demo/userAvatar.svg' }
|
|
|
+ content: e,
|
|
|
+ avatarPosition: 'side-right',
|
|
|
+ avatarConfig: { ...customerAvatar },
|
|
|
});
|
|
|
-
|
|
|
- // 模型的回复消息
|
|
|
- messages.value.push({
|
|
|
- from: 'model',
|
|
|
+ nextTick(() => {
|
|
|
+ conversationRef.value?.scrollTo({
|
|
|
+ top: conversationRef.value.scrollHeight,
|
|
|
+ behavior: 'smooth',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ getAIAnswer(answer ?? e);
|
|
|
+};
|
|
|
+const getAIAnswer = async (content: string) => {
|
|
|
+ const newMessage = {
|
|
|
+ from: 'ai-model',
|
|
|
content: '',
|
|
|
- avatarConfig: { imgSrc: 'https://matechat.gitcode.com/logo.svg' },
|
|
|
+ avatarPosition: 'side-left',
|
|
|
+ avatarConfig: { ...aiModelAvatar },
|
|
|
loading: true,
|
|
|
- });
|
|
|
-
|
|
|
+ id: '',
|
|
|
+ };
|
|
|
+
|
|
|
+ messages.value.push(newMessage);
|
|
|
+
|
|
|
try {
|
|
|
- const completion = await client.instance!.chat.completions.create({
|
|
|
+ const completion = await client.value!.chat.completions.create({
|
|
|
model: settingsStore.value.model,
|
|
|
- messages: [{ role: 'user', content: evt }],
|
|
|
+ messages: [{ role: 'user', content: content }],
|
|
|
stream: true,
|
|
|
});
|
|
|
|
|
|
- messages.value[messages.value.length - 1].loading = false;
|
|
|
-
|
|
|
+ // 找到当前消息的索引
|
|
|
+ // 生成唯一ID用于查找消息
|
|
|
+ const messageId = Date.now().toString();
|
|
|
+ newMessage.id = messageId;
|
|
|
+ newMessage.loading = false;
|
|
|
+
|
|
|
for await (const chunk of completion) {
|
|
|
const content = chunk.choices[0]?.delta?.content || '';
|
|
|
const chatId = chunk.id;
|
|
|
- if (messages.value[messages.value.length - 1].id === chatId) {
|
|
|
- messages.value[messages.value.length - 1].content += content;
|
|
|
- } else {
|
|
|
- messages.value.push({
|
|
|
- from: 'model',
|
|
|
- content: content,
|
|
|
- id: chatId,
|
|
|
- avatarConfig: { imgSrc: 'https://matechat.gitcode.com/logo.svg' }
|
|
|
- });
|
|
|
- }
|
|
|
+ messages.value[messages.value.length - 1].content += content;
|
|
|
+ messages.value[messages.value.length - 1].id = chatId;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('API请求失败:', error);
|
|
|
- messages.value[messages.value.length - 1].content = '请求失败,请检查 API 设置';
|
|
|
- messages.value[messages.value.length - 1].loading = false;
|
|
|
+ const messageIndex = messages.value.findIndex(m => m.id === newMessage.id);
|
|
|
+ if (messages.value[messageIndex]) {
|
|
|
+ messages.value[messageIndex].content = '抱歉,请求服务失败,请稍后重试';
|
|
|
+ messages.value[messageIndex].loading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+const onItemClick = (item:any) => {
|
|
|
+ if (mockAnswer[item.value]) {
|
|
|
+ // 使用 mock 数据
|
|
|
+ onSubmit(item.label, mockAnswer[item.value]);
|
|
|
}
|
|
|
};
|
|
|
+
|
|
|
+const themeChange = () => {
|
|
|
+ if (themeService) {
|
|
|
+ theme.value = themeService.currentTheme.id === 'infinity-theme' ? 'light' : 'dark';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ themeChange();
|
|
|
+ if (themeService && themeService.eventBus) {
|
|
|
+ themeService.eventBus.add('themeChanged', themeChange);
|
|
|
+ }
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style>
|
|
|
@@ -216,6 +218,7 @@ const onSubmit = async (evt: string) => {
|
|
|
background: #fff;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
.input-foot-wrapper {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|