477 lines
14 KiB
Vue
477 lines
14 KiB
Vue
<script>
|
|
import ChatContent from "./components/chatContent.vue";
|
|
import ThinkingBar from "./components/thinkingBar.vue";
|
|
import {mapState} from "vuex";
|
|
import AiDrag from "../basic/AiDrag.vue";
|
|
import AiLocateDialog from "../tools/AiLocateDialog.vue";
|
|
|
|
export default {
|
|
name: "AiCopilot",
|
|
props: {
|
|
http: Function,
|
|
title: {default: "Copilot小助理"}
|
|
},
|
|
data() {
|
|
return {
|
|
show: false,
|
|
expand: false,
|
|
loading: false,
|
|
prompt: "",
|
|
history: [],
|
|
apps: [],
|
|
filter: "",
|
|
conversations: [],
|
|
currentConversation: null,
|
|
app: {},
|
|
locate: false,
|
|
latlng: ""
|
|
}
|
|
},
|
|
computed: {
|
|
...mapState(["user"]),
|
|
profile: v => v.user.info || {},
|
|
expandBtn: v => v.expand ? "收起" : "展开",
|
|
btns: v => [
|
|
{
|
|
label: "文件", icon: "https://cdn.sinoecare.com/i/2024/07/04/668663436e46e.png", click: () => {
|
|
}
|
|
},
|
|
{
|
|
label: "位置", icon: "https://cdn.sinoecare.com/i/2024/07/04/668663436e46e.png", click: () => {
|
|
v.locate = true
|
|
}
|
|
}
|
|
],
|
|
rowBtns: v => [
|
|
{icon: "https://cdn.sinoecare.com/i/2024/07/04/66866edc2910a.png", label: "置顶", click: row => 0},
|
|
{icon: "https://cdn.sinoecare.com/i/2024/07/04/66866ed734540.png", label: "编辑", click: row => 0},
|
|
{icon: "https://cdn.sinoecare.com/i/2024/07/04/66866eda99e4d.png", label: "删除", click: row => v.handleDeleteConversation(row.conversationId)},
|
|
],
|
|
dialogWidth: v => v.expand ? 960 : 468
|
|
},
|
|
components: {AiLocateDialog, AiDrag, ThinkingBar, ChatContent},
|
|
methods: {
|
|
getHistory(params) {
|
|
this.http.post("/app/appaicopilotinfo/list", null, {params}).then(res => {
|
|
if (res?.data) {
|
|
this.history = res.data.records
|
|
}
|
|
})
|
|
},
|
|
getApps() {
|
|
return this.http.post("/app/appaiconfiginfo/list", null, {
|
|
params: {
|
|
status: 1, size: 999
|
|
}
|
|
}).then(res => {
|
|
if (res?.data) {
|
|
return this.apps = res.data.records
|
|
}
|
|
})
|
|
},
|
|
getConversations() {
|
|
return this.http.post("/app/appaicopilotinfo/listHistory", null, {
|
|
params: {content: this.filter}
|
|
}).then(res => {
|
|
if (res?.data) {
|
|
return this.conversations = res.data.records || []
|
|
}
|
|
})
|
|
},
|
|
handleHotkey(evt) {
|
|
if (evt.ctrlKey && evt.key == "Enter") {
|
|
this.handleSend()
|
|
}
|
|
},
|
|
handleSend() {
|
|
if (!this.prompt.trim()) return this.$message.error("无法发送空白信息")
|
|
const concatenateStr = (content, i = 0, target = this.history.at(-1)) => {
|
|
target.content += content.slice(i, i + 1)
|
|
if (++i < content.length) setTimeout(() => concatenateStr(content, i, target), 50)
|
|
}
|
|
this.$debounce(() => {
|
|
const {currentConversation: conversationId, app, prompt: content, latlng} = this.$data
|
|
const message = {appType: "2", userType: 0, content, conversationId, ...app, supplementContent: latlng}
|
|
this.history.push(message)
|
|
this.loading = true
|
|
this.prompt = ""
|
|
this.http.post("/app/appaicopilotinfo/add", message).then(res => {
|
|
if (res?.data?.length >= 2) {
|
|
const last = res.data.at(-1)
|
|
this.history.push({...last, content: ""})
|
|
concatenateStr(last.content)
|
|
}
|
|
}).finally(() => {
|
|
this.loading = false
|
|
this.getConversations()
|
|
})
|
|
}, 100)
|
|
},
|
|
handleDeleteConversation(conversationId) {
|
|
this.$confirm("是否要删除该会话历史?").then(() => {
|
|
this.http.post("/app/appaicopilotinfo/deleteConversation", null, {params: {conversationId}}).then(res => {
|
|
if (res?.code == '0') {
|
|
this.$message.success("删除成功!")
|
|
this.getConversations()
|
|
}
|
|
})
|
|
}).catch(() => 0)
|
|
},
|
|
handleChangeApp(item) {
|
|
const {appId, id: aiConfigId} = item
|
|
this.handleChangeConversation({appId, aiConfigId})
|
|
this.history = [{userType: 2, content: `当前应用已切换至【${item.appName}】`}]
|
|
},
|
|
handleChangeConversation({conversationId, appId, aiConfigId} = {}) {
|
|
this.currentConversation = conversationId
|
|
this.app = {appId, aiConfigId}
|
|
},
|
|
getIcon(item = {}) {
|
|
const icon = item.appIconUrl || "https://cdn.sinoecare.com/i/2024/07/04/66864da1684ad.png"
|
|
return {
|
|
backgroundImage: `url(${icon})`
|
|
}
|
|
},
|
|
handleLocate(v) {
|
|
const {lat, lng} = v.location
|
|
this.latlng = [lat, lng].join(",")
|
|
this.locate = false
|
|
}
|
|
},
|
|
watch: {
|
|
currentConversation(v) {
|
|
v && this.getHistory({conversationId: v})
|
|
}
|
|
},
|
|
created() {
|
|
Promise.all([this.getApps(), this.getConversations()]).then(() => {
|
|
const {appId, id: aiConfigId} = this.apps.at(0)
|
|
this.handleChangeConversation(this.conversations.at(0) || {appId, aiConfigId})
|
|
})
|
|
}
|
|
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="AiCopilot">
|
|
<ai-drag class="copilot" v-if="show" :w="dialogWidth" :h="600" :minHeight="600" :minWidth="dialogWidth" :x="-dialogWidth-12" :y="-542">
|
|
<div class="flex header">
|
|
<b class="fill" v-text="title"/>
|
|
<div class="expandBtn pointer" v-text="expandBtn" @click="expand=!expand"/>
|
|
<div class="minimal pointer" v-text="'最小化'" @click="show=false"/>
|
|
</div>
|
|
<thinking-bar v-show="loading"/>
|
|
<div class="flex content">
|
|
<div class="left" :class="{expand}" v-loading="loading" element-loading-text="小助手正在思考中.."
|
|
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255,255,255,0.8)">
|
|
<div class="profile">
|
|
<div v-text="profile.name"/>
|
|
<span v-text="profile.girdName"/>
|
|
</div>
|
|
<div class="apps">
|
|
<div v-for="(item,i) in apps" :key="i" class="app pointer" :class="{current:item.id==app.aiConfigId}" :style="getIcon(item)" v-text="item.appName" @click="handleChangeApp(item)"/>
|
|
</div>
|
|
<div class="conversation">
|
|
<el-input class="search" v-model="filter" placeholder="搜索历史对话记录" size="small" suffix-icon="el-icon-search" clearable @change="getConversations"/>
|
|
<div class="item pointer" v-for="item in conversations" :key="item.id" @click="currentConversation=item.conversationId" :class="{current:item.conversationId==currentConversation}">
|
|
{{ item.content }}
|
|
<div class="operation flex">
|
|
<div v-for="(btn,i) in rowBtns" :key="i" class="pointer" :style="{backgroundImage: `url(${btn.icon})`}" @click.stop="btn.click(item)"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="right flex column gap-14">
|
|
<chat-content class="fill" :list="history"/>
|
|
<div class="sendBox">
|
|
<div class="topBar flex">
|
|
<div v-for="(btn,i) in btns" :key="i" class="btn pointer" :style="{backgroundImage: `url(${btn.icon})`}" v-text="btn.label" @click="btn.click"/>
|
|
</div>
|
|
<div class="flex end">
|
|
<el-input type="textarea" class="fill input" autosize resize="none" v-model="prompt" placeholder="请输入..." :rows="5"
|
|
@keydown.native="handleHotkey" :disabled="loading" :placeholder="loading?'正在思考中...':'请输入'"/>
|
|
<div class="sendBtn" @click="handleSend"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ai-drag>
|
|
<ai-locate-dialog v-model="locate" :ins="http" @confirm="v=>handleLocate(v)" :modal="false"/>
|
|
<img class="icon" src="https://cdn.sinoecare.com/i/2024/06/04/665ec6f5ef213.png" @click="show=!show"/>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.AiCopilot {
|
|
position: fixed;
|
|
right: 20px;
|
|
bottom: 48px;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 18px;
|
|
z-index: 1888;
|
|
|
|
.copilot {
|
|
:deep(.vdr) {
|
|
border-radius: 0 0 8px 8px;
|
|
height: 600px;
|
|
background: #FFFFFF;
|
|
box-shadow: 0 0 20px 1px #0a255c1a;
|
|
border: none;
|
|
|
|
.handle {
|
|
opacity: 0;
|
|
}
|
|
|
|
&:hover {
|
|
.handle {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
.header {
|
|
height: 48px;
|
|
background-image: linear-gradient(90deg, #3577FD 0%, #216AFD 100%);
|
|
box-shadow: 0 2px 6px 0 #0a255c14;
|
|
border-radius: 8px 8px 0 0;
|
|
overflow: hidden;
|
|
padding: 0 8px 0 14px;
|
|
color: #fff;
|
|
font-size: 14px;
|
|
gap: 14px;
|
|
|
|
& > b {
|
|
font-size: 16px;
|
|
cursor: default;
|
|
}
|
|
|
|
.expandBtn, .minimal {
|
|
padding-left: 18px;
|
|
font-weight: normal;
|
|
background-position: left center;
|
|
background-repeat: no-repeat;
|
|
background-size: 14px 14px;
|
|
line-height: 20px;
|
|
}
|
|
|
|
.minimal {
|
|
background-image: url("https://cdn.sinoecare.com/i/2024/06/04/665ed2bd0a79e.png");
|
|
}
|
|
|
|
.expandBtn {
|
|
background-image: url("https://cdn.sinoecare.com/i/2024/06/04/665ed2bd8b021.png");
|
|
}
|
|
}
|
|
|
|
.content {
|
|
height: calc(100% - 50px);
|
|
float: right;
|
|
|
|
.left {
|
|
width: 0;
|
|
height: 100%;
|
|
padding: 14px 0;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
flex-shrink: 0;
|
|
|
|
& > * {
|
|
margin: 0 14px;
|
|
}
|
|
|
|
&.expand {
|
|
width: 306px;
|
|
|
|
& + .right {
|
|
border-left-color: #ddd;
|
|
min-width: 660px;
|
|
}
|
|
}
|
|
|
|
.profile {
|
|
padding: 18px 14px;
|
|
height: 88px;
|
|
background: url("https://cdn.sinoecare.com/i/2024/06/04/665ed2bc580fa.png") no-repeat;
|
|
background-size: 100% 100%;
|
|
font-size: 14px;
|
|
color: #222222;
|
|
letter-spacing: 0;
|
|
line-height: 20px;
|
|
|
|
& > div {
|
|
font-weight: bold;
|
|
font-size: 20px;
|
|
line-height: 28px;
|
|
}
|
|
}
|
|
|
|
.apps {
|
|
color: #333;
|
|
font-size: 14px;
|
|
margin-top: 18px;
|
|
padding-top: 29px;
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 6px;
|
|
|
|
&:before {
|
|
content: "常用功能";
|
|
font-weight: bold;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
.app {
|
|
line-height: 18px;
|
|
text-align: center;
|
|
width: 72px;
|
|
height: 72px;
|
|
padding-top: 47px;
|
|
background-repeat: no-repeat;
|
|
background-position: center 7px;
|
|
background-size: 36px 36px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
|
|
&:hover, &.current {
|
|
background-color: #286ffd14;
|
|
}
|
|
}
|
|
}
|
|
|
|
.conversation {
|
|
margin-top: 17px;
|
|
|
|
:deep(.search) {
|
|
margin-bottom: 10px;
|
|
|
|
input {
|
|
border-radius: 32px;
|
|
border: none;
|
|
background: #F4F6FA;
|
|
}
|
|
}
|
|
|
|
.item {
|
|
height: 32px;
|
|
border-radius: 16px;
|
|
font-size: 14px;
|
|
color: #666666;
|
|
line-height: 32px;
|
|
padding: 0 14px;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
position: relative;
|
|
|
|
.operation {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
height: 32px;
|
|
background-image: linear-gradient(90deg, #e1ebff00 0%, #E2EBFF 14%);
|
|
border-radius: 0 16px 16px 0;
|
|
gap: 10px;
|
|
padding: 0 12px 0 20px;
|
|
display: none;
|
|
|
|
.pointer {
|
|
width: 14px;
|
|
height: 14px;
|
|
background-repeat: no-repeat;
|
|
background-size: 100% 100%;
|
|
|
|
&:hover {
|
|
opacity: .8;
|
|
}
|
|
}
|
|
}
|
|
|
|
&:hover, &.current {
|
|
background: #2b71fd24;
|
|
|
|
.operation {
|
|
display: flex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.right {
|
|
width: 100%;
|
|
min-width: 468px;
|
|
height: 100%;
|
|
padding: 14px 0 14px 14px;
|
|
align-items: stretch;
|
|
border-left: 1px solid transparent;
|
|
transition: width 1s;
|
|
|
|
.sendBtn {
|
|
width: 36px;
|
|
height: 24px;
|
|
border-radius: 24px;
|
|
background: url("https://cdn.sinoecare.com/i/2024/07/04/668650935514e.png") no-repeat center;
|
|
background-size: 100% 100%;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.chatPanel {
|
|
width: 100%;
|
|
}
|
|
|
|
:deep(.sendBox) {
|
|
width: calc(100% - 14px);
|
|
margin-right: 14px;
|
|
padding: 8px 14px;
|
|
background: #F4F6FA;
|
|
align-items: flex-end;
|
|
border-radius: 4px;
|
|
|
|
.topBar {
|
|
width: 100%;
|
|
height: 28px;
|
|
border-bottom: 1px solid #E0E0E0;
|
|
margin-bottom: 10px;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
|
|
.btn {
|
|
font-size: 12px;
|
|
color: #525F7A;
|
|
padding-left: 16px;
|
|
background-repeat: no-repeat;
|
|
background-position: left center;
|
|
background-size: 14px 14px;
|
|
|
|
&:hover {
|
|
opacity: .8;
|
|
}
|
|
}
|
|
}
|
|
|
|
.input > textarea {
|
|
width: 100%;
|
|
line-height: 20px;
|
|
padding: 0 5px 0 0;
|
|
border: none;
|
|
box-sizing: border-box;
|
|
background: transparent;
|
|
min-height: 73px !important;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.icon {
|
|
width: 68px;
|
|
height: 58px;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
</style>
|