|
@@ -0,0 +1,316 @@
|
|
|
+<script setup>
|
|
|
+import { ref, unref, onMounted, computed, watch } from 'vue';
|
|
|
+import { useMessage, NInput, NSwitch, NPopover, NScrollbar } from 'naive-ui';
|
|
|
+import SvgIcon from '@/components/SvgIcon';
|
|
|
+
|
|
|
+import 'load-awesome/css/ball-running-dots.min.css';
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ options: {
|
|
|
+ type: Array,
|
|
|
+ default: []
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits(['onClick', 'onEnter']);
|
|
|
+
|
|
|
+const modelLoading = defineModel('loading');
|
|
|
+const switchStatus = defineModel('switch');
|
|
|
+
|
|
|
+const message = useMessage();
|
|
|
+
|
|
|
+const inpVal = ref('');
|
|
|
+const inpRef = ref(null);
|
|
|
+const isFocusState = ref(false);
|
|
|
+
|
|
|
+const isOpen = ref(false);
|
|
|
+const selectedOption = ref(null);
|
|
|
+const highlightedIndex = ref(0);
|
|
|
+
|
|
|
+const agentOptions = computed(() => props.options.filter(({ tools }) => tools));
|
|
|
+
|
|
|
+const focusInput = _ => isFocusState.value = true;
|
|
|
+
|
|
|
+const blurInput = _ => isFocusState.value = false;
|
|
|
+
|
|
|
+watch(inpVal, (curVal) => {
|
|
|
+ if (curVal === "@" && curVal.length === 1) {
|
|
|
+ if ( !unref(agentOptions).length ) {
|
|
|
+ return message.warning('当前未配置智能体');
|
|
|
+ }
|
|
|
+ isOpen.value = true;
|
|
|
+ } else {
|
|
|
+ isOpen.value = false;
|
|
|
+ }
|
|
|
+ // isOpen.value = (curVal === "@" && curVal.length === 1);
|
|
|
+})
|
|
|
+
|
|
|
+const handleInpFocus = () => {
|
|
|
+ inpRef.value?.focus();
|
|
|
+}
|
|
|
+
|
|
|
+const commonEmitEvent = (eventName) => {
|
|
|
+ const val = unref(inpVal);
|
|
|
+ const len = val.trim().length;
|
|
|
+
|
|
|
+ if ( !len ) {
|
|
|
+ return message.warning('请输入您的问题或需求');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( len > 2000 ) {
|
|
|
+ return message.warning('问题限制2000个字以内');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( modelLoading.value ) {
|
|
|
+ return message.warning('当前对话进行中');
|
|
|
+ }
|
|
|
+
|
|
|
+ emit(eventName, val);
|
|
|
+
|
|
|
+ inpVal.value = '';
|
|
|
+}
|
|
|
+
|
|
|
+const handleInpEnter = (event) => {
|
|
|
+ console.log( !unref(isOpen) );
|
|
|
+ if (event.key === 'Enter' && !event.shiftKey) {
|
|
|
+ event.preventDefault();
|
|
|
+ // commonEmitEvent('onEnter');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleBtnClick = () => {
|
|
|
+ commonEmitEvent("onClick")
|
|
|
+}
|
|
|
+
|
|
|
+const clearInpVal = () => {
|
|
|
+ inpVal.value = '';
|
|
|
+}
|
|
|
+
|
|
|
+// 键盘上下事件
|
|
|
+const handleKeyDown = (event) => {
|
|
|
+
|
|
|
+ const len = unref(agentOptions).length;
|
|
|
+
|
|
|
+ switch (event.key) {
|
|
|
+ case 'ArrowUp':
|
|
|
+ event.preventDefault();
|
|
|
+ highlightedIndex.value = (unref(highlightedIndex) - 1 + len) % len;
|
|
|
+ break;
|
|
|
+ case 'ArrowDown':
|
|
|
+ event.preventDefault();
|
|
|
+ highlightedIndex.value = (unref(highlightedIndex) + 1) % len;
|
|
|
+ break;
|
|
|
+ case 'Enter':
|
|
|
+ selectOption(unref(highlightedIndex));
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const selectOption = (index) => {
|
|
|
+ selectedOption.value = agentOptions.value[index];
|
|
|
+ isOpen.value = false;
|
|
|
+ highlightedIndex.value = index;
|
|
|
+ clearInpVal();
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ document.addEventListener('keydown', handleKeyDown);
|
|
|
+})
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ clearInpVal,
|
|
|
+ handleInpFocus,
|
|
|
+ inpVal,
|
|
|
+})
|
|
|
+
|
|
|
+</script>
|
|
|
+<template>
|
|
|
+ <NPopover
|
|
|
+ trigger="hover"
|
|
|
+ width="trigger"
|
|
|
+ content-style="padding: 0;"
|
|
|
+ :show-arrow="false"
|
|
|
+ :show="isOpen"
|
|
|
+ >
|
|
|
+ <template #trigger>
|
|
|
+ <div>
|
|
|
+ <div class="chat-inp-outer border-[1px]" :class="[{ 'border-[#2454FF]': isFocusState }]">
|
|
|
+ <div class="helper-tools py-[10px] px-[10px] bg-[#fcfcfc] space-x-[10px]" v-show="selectedOption">
|
|
|
+ <span>与</span>
|
|
|
+ <p class="agent-name space-x-[5px]" @click="isOpen = true">
|
|
|
+ <img src="https://static.fuxicarbon.com/userupload/db77ffe0cef843278a23b0d2db9505fa.png" alt="">
|
|
|
+ <span>{{ selectedOption?.title }}</span>
|
|
|
+ </p>
|
|
|
+ <span>对话中</span>
|
|
|
+ </div>
|
|
|
+ <div class="chat-inp-inner">
|
|
|
+ <div class="inp-wrapper flex-1" @click="handleInpFocus">
|
|
|
+ <NInput
|
|
|
+ class="flex-1"
|
|
|
+ ref="inpRef"
|
|
|
+ type="textarea"
|
|
|
+ size="medium"
|
|
|
+ placeholder="输入您的问题或需求,Enter发送,Shift+Enter换行"
|
|
|
+ v-model:value="inpVal"
|
|
|
+ :autosize="{ minRows: 1, maxRows: 5 }"
|
|
|
+ @focus="focusInput"
|
|
|
+ @blur="blurInput"
|
|
|
+ @keypress="handleInpEnter"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="submit-btn">
|
|
|
+ <button class="btn bg-[#1A2029] hover:bg-[#3C4148]" @click="handleBtnClick">
|
|
|
+ <SvgIcon name="tool-send-plane" size="22" v-show="!modelLoading"></SvgIcon>
|
|
|
+ <div style="color: #fff" class="la-ball-running-dots la-sm" v-show="modelLoading">
|
|
|
+ <div v-for="item in 5" :key="item"></div>
|
|
|
+ </div>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="switch-inner pt-[8px] space-x-[6px]">
|
|
|
+ <NSwitch size="small" v-model:value="switchStatus"></NSwitch>
|
|
|
+ <span class="text-[12px] text-[#9E9E9E]">使用搜索增强</span>
|
|
|
+ </div>
|
|
|
+ <div class="masking-inner text-center text-[#2454FF]"></div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="popover-inner">
|
|
|
+ <div class="header">
|
|
|
+ <span>选择智能体</span>
|
|
|
+ <p class="close" @click="isOpen = false">
|
|
|
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M3.14905 3.14777L12.8508 12.8496" stroke="#838A95" stroke-linecap="round"></path>
|
|
|
+ <path d="M13.0016 2.99786L2.99725 13.0022" stroke="#838A95" stroke-linecap="round"></path>
|
|
|
+ <g opacity="0.01" style="mix-blend-mode: darken;">
|
|
|
+ <rect width="16" height="16" fill="white"></rect>
|
|
|
+ </g>
|
|
|
+ </svg>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div class="content">
|
|
|
+ <NScrollbar style="max-height: 240px;">
|
|
|
+ <div class="item" v-for="item, index in agentOptions" :class="['item', { active: highlightedIndex === index }]" @click="selectOption(index)">
|
|
|
+ <p class="icon">
|
|
|
+ <img :src="item.banner" alt="">
|
|
|
+ </p>
|
|
|
+ <p class="ml-[10px] space-x-[5px] text">
|
|
|
+ <span class="text-[15px]">{{item.title}}</span>
|
|
|
+ <!-- <span class="text-[#888] text-[14px]">/span> -->
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </NScrollbar>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </NPopover>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.popover-inner {
|
|
|
+ .header {
|
|
|
+ @include flex(x, center, between);
|
|
|
+ padding-bottom: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ .close {
|
|
|
+ @include flex(x, center, center);
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background: #e9eef8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .content {
|
|
|
+ .item {
|
|
|
+ @include flex(x, center, start);
|
|
|
+ padding: 8px 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background: #f0fafe;
|
|
|
+ }
|
|
|
+ .icon {
|
|
|
+ @include flex(x, center, center);
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border-radius: 100%;
|
|
|
+ background: #e9eef8;
|
|
|
+ img {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .text {
|
|
|
+ text-align: left;
|
|
|
+ overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .active {
|
|
|
+ background: #f0fafe;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.chat-inp-outer {
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0px 3px 12px 0px #97D3FF40;
|
|
|
+
|
|
|
+ .helper-tools {
|
|
|
+ @include flex(x, center, start);
|
|
|
+ color: #666;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ .agent-name {
|
|
|
+ @include flex(x, center, start);
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.chat-inp-inner {
|
|
|
+ position: relative;
|
|
|
+ @include flex(x, center, between);
|
|
|
+ background: #fff;
|
|
|
+
|
|
|
+ .inp-wrapper {
|
|
|
+ padding: 17px 0px 17px 34px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .submit-btn {
|
|
|
+ @include flex(x, center, center);
|
|
|
+ width: 84px;
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ @include flex(x, center, center);
|
|
|
+ width: 50px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 32px;
|
|
|
+ transition: all .3s;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.masking-inner {
|
|
|
+ position: absolute;
|
|
|
+ top: -30px;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 30px;
|
|
|
+ background: linear-gradient(180deg, rgba(232, 241, 250, 0) 0%, #E7F0FA 95%);
|
|
|
+}
|
|
|
+</style>
|