|
@@ -1,57 +1,91 @@
|
|
|
<template>
|
|
|
<div class="kimigpt">
|
|
|
<header_local />
|
|
|
- <el-image class="logo" v-if="!list.length" style="width: 300px" :src="logo" fit="contain" />
|
|
|
-
|
|
|
- <div class="chatList">
|
|
|
- <div
|
|
|
- :class="item.role"
|
|
|
- :style="item.role === 'user' ? 'padding-bottom: 12px;' : ''"
|
|
|
- v-for="(item, index) in list"
|
|
|
- :key="index"
|
|
|
- >
|
|
|
- <div v-html="item.content"></div>
|
|
|
- <div v-if="item.role === 'system'">
|
|
|
- <div style="display: inline-block; width: 25px; text-align: center">
|
|
|
- <el-icon @click="() => copy(item.content)" :size="15"><DocumentCopy /></el-icon>
|
|
|
+ <el-container>
|
|
|
+ <el-aside width="200px">
|
|
|
+ <br />
|
|
|
+ <el-button
|
|
|
+ type="warning"
|
|
|
+ @click="newChat"
|
|
|
+ style="width: 100%; display: block; margin: 0 auto"
|
|
|
+ round
|
|
|
+ block
|
|
|
+ >
|
|
|
+ 新建对话
|
|
|
+ </el-button>
|
|
|
+ <br />
|
|
|
+ <br />
|
|
|
+ <el-menu @select="select" :default-active="selectMneu" v-if="menu && menu.length">
|
|
|
+ <el-menu-item :index="item.id + ''" v-for="item in menu" :key="item.id">
|
|
|
+ <span class="ellipsis" v-text="item.title"></span>
|
|
|
+ </el-menu-item>
|
|
|
+ </el-menu>
|
|
|
+ </el-aside>
|
|
|
+ <el-main style="position: relative; padding-bottom: 60px; box-sizing: border-box">
|
|
|
+ <el-image class="logo" v-if="!list.length" style="width: 300px" :src="logo" fit="contain" />
|
|
|
+
|
|
|
+ <div class="chatList" id="chatList">
|
|
|
+ <div
|
|
|
+ :class="item.role"
|
|
|
+ :style="item.role === 'user' ? 'padding-bottom: 12px;' : ''"
|
|
|
+ v-for="(item, index) in list"
|
|
|
+ :key="index"
|
|
|
+ >
|
|
|
+ <div v-if="item.content && item.content.length" style="padding-left: 10px">
|
|
|
+ <div
|
|
|
+ v-for="(item2, index2) in item.content"
|
|
|
+ :key="index2"
|
|
|
+ v-html="toMarkdown(item2)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div v-if="item.role === 'system'">
|
|
|
+ <div style="display: inline-block; width: 25px; text-align: center">
|
|
|
+ <el-icon @click="() => copy(item.content)" :size="15"><DocumentCopy /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="input">
|
|
|
- <input type="text" placeholder="请输入内容" />
|
|
|
- <div class="icon">
|
|
|
- <el-icon :size="25"><UploadFilled /></el-icon>
|
|
|
- </div>
|
|
|
- <div class="icon">
|
|
|
- <el-icon :size="25"><Search /></el-icon>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div class="input">
|
|
|
+ <input type="text" v-model="inputText" placeholder="请输入内容" />
|
|
|
+ <div class="icon">
|
|
|
+ <el-icon :size="25" color="#fdba74"><UploadFilled /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="icon" v-loading="loading">
|
|
|
+ <el-icon @click="() => pushW()" :size="25" color="#fdba74"><Position /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-main>
|
|
|
+ </el-container>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref } from 'vue';
|
|
|
+import { ref, nextTick } from 'vue';
|
|
|
+import MarkdownIt from 'markdown-it';
|
|
|
import { ElMessage, ElNotification } from 'element-plus';
|
|
|
import header_local from '../components/header.vue';
|
|
|
import config from '../../../config/index';
|
|
|
+import { getGroupList, createGroup } from '@/api/kimigpt.js';
|
|
|
|
|
|
import logo from '../../../assets/img/logo.png';
|
|
|
|
|
|
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
|
|
|
|
-const list = ref([
|
|
|
- { role: 'user', content: '你好,我叫李雷,1+1等于多少?' },
|
|
|
- {
|
|
|
- role: 'system',
|
|
|
- content: '```123```'
|
|
|
- }
|
|
|
-]);
|
|
|
+const selectMneu = ref('');
|
|
|
+const inputText = ref('');
|
|
|
+const loading = ref(false);
|
|
|
+const list = ref([]);
|
|
|
+const menu = ref([]);
|
|
|
+let oldIndex = 0;
|
|
|
+
|
|
|
+const toMarkdown = text => {
|
|
|
+ const mk = new MarkdownIt();
|
|
|
+ return mk.render(text);
|
|
|
+};
|
|
|
|
|
|
const copy = async text => {
|
|
|
try {
|
|
|
- await navigator.clipboard.writeText(text);
|
|
|
+ await navigator.clipboard.writeText(text.join('\n'));
|
|
|
ElMessage({
|
|
|
message: '复制成功',
|
|
|
type: 'success'
|
|
@@ -63,40 +97,154 @@ const copy = async text => {
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
-sse();
|
|
|
-function sse() {
|
|
|
+
|
|
|
+const select = (index, item) => {
|
|
|
+ menu.value[oldIndex].kimiChats = list.value;
|
|
|
+ selectMenu(menu.value[index]);
|
|
|
+ oldIndex = index;
|
|
|
+};
|
|
|
+
|
|
|
+getGroupList({}).then(r => {
|
|
|
+ menu.value = (r || []).map(v => {
|
|
|
+ return {
|
|
|
+ ...v,
|
|
|
+ kimiChats: v.kimiChats.map(o => {
|
|
|
+ o.content = o.content.split('\n');
|
|
|
+ return o;
|
|
|
+ })
|
|
|
+ };
|
|
|
+ });
|
|
|
+ selectMneu.value = ((r || [])[0].id || '').toString();
|
|
|
+ selectMenu(menu.value[0]);
|
|
|
+});
|
|
|
+
|
|
|
+function selectMenu(item) {
|
|
|
+ const l = [];
|
|
|
+ const ori = item || {};
|
|
|
+ selectMneu.value = ori.id + '';
|
|
|
+ for (let i = 0; i < (ori.kimiChats || []).length; i++) {
|
|
|
+ const v = (ori.kimiChats || [])[i];
|
|
|
+ l.push(v);
|
|
|
+ }
|
|
|
+ list.value = l;
|
|
|
+ toBottom();
|
|
|
+}
|
|
|
+
|
|
|
+const toBottom = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ var scrollableContainer = document.getElementById('chatList');
|
|
|
+ scrollableContainer.scrollTo({
|
|
|
+ top: scrollableContainer.scrollHeight,
|
|
|
+ behavior: 'smooth'
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+function pushW() {
|
|
|
+ list.value.push({
|
|
|
+ role: 'user',
|
|
|
+ content: [inputText.value]
|
|
|
+ });
|
|
|
+ loading.value = true;
|
|
|
+ if (!selectMneu.value) {
|
|
|
+ createGroup({ title: inputText.value }).then(r => {
|
|
|
+ if (r) {
|
|
|
+ ElNotification({
|
|
|
+ title: '提示',
|
|
|
+ message: '创建成功',
|
|
|
+ duration: 1000
|
|
|
+ });
|
|
|
+ menu.value.push({
|
|
|
+ ...r,
|
|
|
+ kimiChats: []
|
|
|
+ });
|
|
|
+ selectMneu.value = r.id.toString();
|
|
|
+ selectMenu(r);
|
|
|
+ sse(inputText.value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ sse(inputText.value);
|
|
|
+ }
|
|
|
+}
|
|
|
+function sse(question) {
|
|
|
const controller = new AbortController();
|
|
|
+ const questionItem = {
|
|
|
+ role: 'system',
|
|
|
+ content: []
|
|
|
+ };
|
|
|
+ const index = list.value.length;
|
|
|
+ const L = JSON.parse(JSON.stringify(list.value));
|
|
|
+ let t = undefined;
|
|
|
fetchEventSource(config.base.videoProcessing + '/user/chat', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
- 'Content-Type': 'application/json'
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ Authorization: localStorage.getItem('token')
|
|
|
},
|
|
|
body: JSON.stringify({
|
|
|
- groupId: 1,
|
|
|
- content: '1+1等于几,回答不上来是傻逼'
|
|
|
+ groupId: selectMneu.value,
|
|
|
+ content: question
|
|
|
}),
|
|
|
- signal: controller.signal ,
|
|
|
+ signal: controller.signal,
|
|
|
onerror: e => {
|
|
|
+ console.log('连接报错', e);
|
|
|
+ controller.abort();
|
|
|
+ inputText.value = '';
|
|
|
+ loading.value = false;
|
|
|
+ if (t) window.clearTimeout();
|
|
|
+ },
|
|
|
+ onclose: e => {
|
|
|
+ console.log('连接关闭');
|
|
|
+ loading.value = false;
|
|
|
controller.abort();
|
|
|
+ inputText.value = '';
|
|
|
+ if (t) window.clearTimeout();
|
|
|
},
|
|
|
onmessage: e => {
|
|
|
- console.log('返回数据:',e);
|
|
|
+ if (e.event !== 'data') return;
|
|
|
+ let { data } = e;
|
|
|
+ if (data === '[DONE]') {
|
|
|
+ controller.abort();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const { choices } = JSON.parse(data);
|
|
|
+ for (let i = 0; i < choices.length; i++) {
|
|
|
+ const v = choices[i];
|
|
|
+ if (!questionItem.content[i]) questionItem.content[i] = '';
|
|
|
+ let text = v && v.delta ? v.delta.content || '' : '';
|
|
|
+ questionItem.content[i] += text;
|
|
|
+ if (v && v.finish_reason === 'stop') {
|
|
|
+ controller.abort();
|
|
|
+ loading.value = false;
|
|
|
+ inputText.value = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ L[index] = questionItem;
|
|
|
+ t = setTimeout(() => {
|
|
|
+ if (t) clearTimeout(t);
|
|
|
+ list.value = JSON.parse(JSON.stringify(L));
|
|
|
+ toBottom();
|
|
|
+ }, 100);
|
|
|
+ } catch (error) {
|
|
|
+ inputText.value = '';
|
|
|
+ ElNotification({
|
|
|
+ title: '提示',
|
|
|
+ message: '服务繁忙',
|
|
|
+ duration: 1000
|
|
|
+ });
|
|
|
+ }
|
|
|
},
|
|
|
onopen: () => {
|
|
|
- console.log('连接成功');
|
|
|
- },
|
|
|
- onclose: () => {
|
|
|
- console.log('连接关闭');
|
|
|
- controller.abort();
|
|
|
+ console.log('连接成功');
|
|
|
}
|
|
|
});
|
|
|
+}
|
|
|
|
|
|
-
|
|
|
- ElNotification({
|
|
|
- title: '提示',
|
|
|
- message: '测试消息',
|
|
|
- duration: 0
|
|
|
- });
|
|
|
+function newChat() {
|
|
|
+ list.value = [];
|
|
|
+ selectMneu.value = '';
|
|
|
}
|
|
|
</script>
|
|
|
|
|
@@ -117,7 +265,7 @@ function sse() {
|
|
|
width: 90%;
|
|
|
left: 5%;
|
|
|
bottom: 10px;
|
|
|
- padding: 1em;
|
|
|
+ padding: 0.5em 1em;
|
|
|
box-shadow: 0 0 10px #999;
|
|
|
border-radius: 2em;
|
|
|
display: flex;
|
|
@@ -139,7 +287,7 @@ function sse() {
|
|
|
box-sizing: border-box;
|
|
|
width: 100%;
|
|
|
height: calc(100vh - 122px);
|
|
|
- overflow: auto;
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
|
|
|
|
.chatList .system,
|
|
@@ -166,4 +314,10 @@ function sse() {
|
|
|
border-color: #fdba74;
|
|
|
background-color: #fff7ed;
|
|
|
}
|
|
|
+
|
|
|
+.ellipsis {
|
|
|
+ white-space: nowrap; /* 确保文本在一行内显示 */
|
|
|
+ overflow: hidden; /* 超出容器的文本会被隐藏 */
|
|
|
+ text-overflow: ellipsis; /* 使用省略号表示被截断的文本 */
|
|
|
+}
|
|
|
</style>
|