2024-07-14 16:42:38 +08:00
|
|
|
|
<template>
|
2024-07-25 20:30:28 +08:00
|
|
|
|
<div class="chat" ref="chatContainer">
|
2025-02-27 01:37:42 +08:00
|
|
|
|
<div class="chat-header">
|
2024-07-14 16:42:38 +08:00
|
|
|
|
<div class="header__left">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="!state.isSidebarOpen"
|
2025-04-15 12:39:46 +08:00
|
|
|
|
class="close nav-btn nav-btn-icon-only"
|
2024-07-14 16:42:38 +08:00
|
|
|
|
@click="state.isSidebarOpen = true"
|
|
|
|
|
|
>
|
2025-04-15 12:39:46 +08:00
|
|
|
|
<a-tooltip title="展开侧边栏" placement="right">
|
|
|
|
|
|
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
|
|
|
|
|
|
</a-tooltip>
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</div>
|
2025-03-30 11:53:18 +08:00
|
|
|
|
|
2025-04-15 12:39:46 +08:00
|
|
|
|
<div class="newchat nav-btn nav-btn-icon-only" @click="$emit('newconv')">
|
|
|
|
|
|
<a-tooltip title="新建对话" placement="right">
|
|
|
|
|
|
<PlusCircleOutlined />
|
|
|
|
|
|
</a-tooltip>
|
2025-03-30 11:53:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<a-dropdown>
|
|
|
|
|
|
<a class="model-select nav-btn" @click.prevent>
|
2025-04-15 12:39:46 +08:00
|
|
|
|
<BulbOutlined />
|
|
|
|
|
|
<a-tooltip :title="configStore.config?.model_name" placement="right">
|
|
|
|
|
|
<span class="model-text text"> {{ configStore.config?.model_name }} </span>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
<span class="text" style="color: #aaa;">{{ configStore.config?.model_provider }} </span>
|
2025-03-30 11:53:18 +08:00
|
|
|
|
</a>
|
|
|
|
|
|
<template #overlay>
|
|
|
|
|
|
<a-menu class="scrollable-menu">
|
|
|
|
|
|
<a-menu-item-group v-for="(item, key) in modelKeys" :key="key" :title="modelNames[item]?.name">
|
|
|
|
|
|
<a-menu-item v-for="(model, idx) in modelNames[item]?.models" :key="`${item}-${idx}`" @click="selectModel(item, model)">
|
2025-04-15 12:39:46 +08:00
|
|
|
|
{{ model }}
|
2025-03-30 11:53:18 +08:00
|
|
|
|
</a-menu-item>
|
|
|
|
|
|
</a-menu-item-group>
|
|
|
|
|
|
<a-menu-item-group v-if="customModels.length > 0" title="自定义模型">
|
|
|
|
|
|
<a-menu-item v-for="(model, idx) in customModels" :key="`custom-${idx}`" @click="selectModel('custom', model.custom_id)">
|
|
|
|
|
|
custom/{{ model.custom_id }}
|
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
|
</a-menu-item-group>
|
|
|
|
|
|
</a-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-dropdown>
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header__right">
|
2024-09-28 00:41:02 +08:00
|
|
|
|
<div class="nav-btn text" @click="opts.showPanel = !opts.showPanel">
|
2024-07-29 01:00:02 +08:00
|
|
|
|
<component :is="opts.showPanel ? FolderOpenOutlined : FolderOutlined" /> <span class="text">选项</span>
|
2024-07-22 00:01:01 +08:00
|
|
|
|
</div>
|
2024-09-28 00:41:02 +08:00
|
|
|
|
<div v-if="opts.showPanel" class="my-panal r0 top100 swing-in-top-fwd" ref="panel">
|
2024-10-15 00:38:24 +08:00
|
|
|
|
<div class="flex-center" @click="meta.summary_title = !meta.summary_title">
|
|
|
|
|
|
总结对话标题 <div @click.stop><a-switch v-model:checked="meta.summary_title" /></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex-center">
|
|
|
|
|
|
最大历史轮数 <a-input-number id="inputNumber" v-model:value="meta.history_round" :min="1" :max="50" />
|
|
|
|
|
|
</div>
|
2025-03-23 20:10:29 +08:00
|
|
|
|
<div class="flex-center">
|
|
|
|
|
|
字体大小
|
|
|
|
|
|
<a-select v-model:value="meta.fontSize" style="width: 100px" placeholder="选择字体大小">
|
|
|
|
|
|
<a-select-option value="smaller">更小</a-select-option>
|
|
|
|
|
|
<a-select-option value="default">默认</a-select-option>
|
|
|
|
|
|
<a-select-option value="larger">更大</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex-center" @click="meta.wideScreen = !meta.wideScreen">
|
|
|
|
|
|
宽屏模式 <div @click.stop><a-switch v-model:checked="meta.wideScreen" /></div>
|
|
|
|
|
|
</div>
|
2024-07-18 02:46:58 +08:00
|
|
|
|
</div>
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="conv.messages.length == 0" class="chat-examples">
|
2024-09-14 02:46:44 +08:00
|
|
|
|
<h1>你好,我是语析,一个基于知识图谱的智能助手</h1>
|
2024-07-29 01:00:02 +08:00
|
|
|
|
<div class="opts">
|
2024-07-14 16:42:38 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="opt__button"
|
|
|
|
|
|
v-for="(exp, key) in examples"
|
|
|
|
|
|
:key="key"
|
2024-09-28 00:41:02 +08:00
|
|
|
|
@click="conv.inputText = exp"
|
2024-07-14 16:42:38 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ exp }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-03-23 20:10:29 +08:00
|
|
|
|
<div class="chat-box" :class="{ 'wide-screen': meta.wideScreen, 'font-smaller': meta.fontSize === 'smaller', 'font-larger': meta.fontSize === 'larger' }">
|
2025-03-28 10:57:19 +08:00
|
|
|
|
<MessageComponent
|
2025-04-27 18:59:38 +08:00
|
|
|
|
v-for="(message, index) in conv.messages"
|
2025-03-30 20:46:26 +08:00
|
|
|
|
:message="message"
|
2024-07-14 16:42:38 +08:00
|
|
|
|
:key="message.id"
|
2025-03-28 10:57:19 +08:00
|
|
|
|
:is-processing="isStreaming"
|
2025-04-27 18:59:38 +08:00
|
|
|
|
:show-refs="['copy', 'regenerate', 'subGraph', 'webSearch', 'knowledgeBase']"
|
|
|
|
|
|
:is-latest-message="isLatestMessage(index)"
|
2025-03-28 10:57:19 +08:00
|
|
|
|
@retry="retryMessage(message.id)"
|
2025-03-30 20:46:26 +08:00
|
|
|
|
@retryStoppedMessage="retryStoppedMessage(message.id)"
|
2025-04-27 18:59:38 +08:00
|
|
|
|
@openRefs="handleOpenRefs"
|
2024-07-14 16:42:38 +08:00
|
|
|
|
>
|
2025-03-28 10:57:19 +08:00
|
|
|
|
</MessageComponent>
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</div>
|
2024-07-25 20:30:28 +08:00
|
|
|
|
<div class="bottom">
|
2025-03-28 10:57:19 +08:00
|
|
|
|
<div class="message-input-wrapper" :class="{ 'wide-screen': meta.wideScreen}">
|
2025-03-28 03:17:09 +08:00
|
|
|
|
<MessageInputComponent
|
|
|
|
|
|
v-model="conv.inputText"
|
|
|
|
|
|
:is-loading="isStreaming"
|
|
|
|
|
|
:send-button-disabled="!conv.inputText && !isStreaming"
|
|
|
|
|
|
:auto-size="{ minRows: 2, maxRows: 10 }"
|
2025-03-30 20:46:26 +08:00
|
|
|
|
@send="handleSendOrStop"
|
2025-03-28 03:17:09 +08:00
|
|
|
|
@keydown="handleKeyDown"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #options-left>
|
2025-02-24 19:43:40 +08:00
|
|
|
|
<div
|
|
|
|
|
|
:class="{'switch': true, 'opt-item': true, 'active': meta.use_web}"
|
|
|
|
|
|
v-if="configStore.config.enable_web_search"
|
|
|
|
|
|
@click="meta.use_web=!meta.use_web"
|
|
|
|
|
|
>
|
|
|
|
|
|
<CompassOutlined style="margin-right: 3px;"/>
|
|
|
|
|
|
联网搜索
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
:class="{'switch': true, 'opt-item': true, 'active': meta.use_graph}"
|
|
|
|
|
|
v-if="configStore.config.enable_knowledge_graph"
|
|
|
|
|
|
@click="meta.use_graph=!meta.use_graph"
|
|
|
|
|
|
>
|
2025-02-24 22:56:13 +08:00
|
|
|
|
<DeploymentUnitOutlined style="margin-right: 3px;"/>
|
2025-02-24 19:43:40 +08:00
|
|
|
|
知识图谱
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-dropdown
|
2025-02-26 12:37:06 +08:00
|
|
|
|
v-if="configStore.config.enable_knowledge_base && opts.databases.length > 0"
|
2025-02-24 19:43:40 +08:00
|
|
|
|
:class="{'opt-item': true, 'active': meta.selectedKB !== null}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a class="ant-dropdown-link" @click.prevent>
|
|
|
|
|
|
<BookOutlined style="margin-right: 3px;"/>
|
|
|
|
|
|
<span class="text">{{ meta.selectedKB === null ? '不使用知识库' : opts.databases[meta.selectedKB]?.name }}</span>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
<template #overlay>
|
|
|
|
|
|
<a-menu>
|
|
|
|
|
|
<a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="useDatabase(index)">
|
|
|
|
|
|
<a href="javascript:;">{{ db.name }}</a>
|
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
|
<a-menu-item @click="useDatabase(null)">
|
|
|
|
|
|
<a href="javascript:;">不使用</a>
|
|
|
|
|
|
</a-menu-item>
|
|
|
|
|
|
</a-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-dropdown>
|
2025-03-28 03:17:09 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</MessageInputComponent>
|
|
|
|
|
|
<p class="note">请注意辨别内容的可靠性 By {{ configStore.config?.model_provider }}: {{ configStore.config?.model_name }}</p>
|
2024-07-25 20:30:28 +08:00
|
|
|
|
</div>
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</div>
|
2025-04-27 18:59:38 +08:00
|
|
|
|
<!-- 添加全局Refs侧边栏 -->
|
|
|
|
|
|
<RefsSidebar
|
|
|
|
|
|
ref="refsSidebarRef"
|
|
|
|
|
|
:visible="refsSidebarVisible"
|
|
|
|
|
|
:latestRefs="currentRefs"
|
|
|
|
|
|
@update:visible="refsSidebarVisible = $event"
|
|
|
|
|
|
/>
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-03-30 11:53:18 +08:00
|
|
|
|
import { reactive, ref, onMounted, toRefs, nextTick, onUnmounted, watch, computed } from 'vue'
|
2024-07-17 18:52:20 +08:00
|
|
|
|
import {
|
|
|
|
|
|
SendOutlined,
|
|
|
|
|
|
MenuOutlined,
|
|
|
|
|
|
FormOutlined,
|
|
|
|
|
|
LoadingOutlined,
|
|
|
|
|
|
BookOutlined,
|
|
|
|
|
|
BookFilled,
|
2025-02-24 19:43:40 +08:00
|
|
|
|
CompassOutlined,
|
|
|
|
|
|
ArrowUpOutlined,
|
2024-07-18 02:46:58 +08:00
|
|
|
|
CompassFilled,
|
|
|
|
|
|
GoldenFilled,
|
2024-10-14 22:10:56 +08:00
|
|
|
|
GoldOutlined,
|
2024-07-21 18:15:04 +08:00
|
|
|
|
SettingOutlined,
|
2024-07-22 00:01:01 +08:00
|
|
|
|
SettingFilled,
|
2024-07-25 20:30:28 +08:00
|
|
|
|
PlusCircleOutlined,
|
2024-07-27 14:32:57 +08:00
|
|
|
|
FolderOutlined,
|
|
|
|
|
|
FolderOpenOutlined,
|
2024-08-25 20:29:24 +08:00
|
|
|
|
GlobalOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
2025-03-30 11:53:18 +08:00
|
|
|
|
BulbOutlined,
|
2025-02-23 17:40:09 +08:00
|
|
|
|
CaretRightOutlined,
|
2025-02-24 22:56:13 +08:00
|
|
|
|
DeploymentUnitOutlined,
|
2025-03-30 19:26:43 +08:00
|
|
|
|
PauseOutlined,
|
2025-03-30 14:51:18 +08:00
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
CopyOutlined
|
2024-07-17 18:52:20 +08:00
|
|
|
|
} from '@ant-design/icons-vue'
|
2024-07-29 01:00:02 +08:00
|
|
|
|
import { onClickOutside } from '@vueuse/core'
|
2024-07-27 14:32:57 +08:00
|
|
|
|
import { useConfigStore } from '@/stores/config'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
import { useUserStore } from '@/stores/user'
|
2024-09-09 17:07:03 +08:00
|
|
|
|
import { message } from 'ant-design-vue'
|
2025-03-28 03:17:09 +08:00
|
|
|
|
import MessageInputComponent from '@/components/MessageInputComponent.vue'
|
2025-03-28 10:57:19 +08:00
|
|
|
|
import MessageComponent from '@/components/MessageComponent.vue'
|
2025-04-27 18:59:38 +08:00
|
|
|
|
import RefsSidebar from '@/components/RefsSidebar.vue'
|
2025-05-02 23:56:59 +08:00
|
|
|
|
import { chatApi } from '@/apis/auth_api'
|
2025-05-04 21:04:01 +08:00
|
|
|
|
import { knowledgeBaseApi } from '@/apis/admin_api'
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
conv: Object,
|
|
|
|
|
|
state: Object
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2024-07-28 16:16:52 +08:00
|
|
|
|
const emit = defineEmits(['rename-title', 'newconv']);
|
2024-07-27 14:32:57 +08:00
|
|
|
|
const configStore = useConfigStore()
|
2025-05-02 23:56:59 +08:00
|
|
|
|
const userStore = useUserStore()
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
|
|
|
|
|
const { conv, state } = toRefs(props)
|
2024-07-25 20:30:28 +08:00
|
|
|
|
const chatContainer = ref(null)
|
2025-02-27 13:26:08 +08:00
|
|
|
|
|
2024-07-14 16:42:38 +08:00
|
|
|
|
const isStreaming = ref(false)
|
2025-02-27 13:26:08 +08:00
|
|
|
|
const userIsScrolling = ref(false);
|
|
|
|
|
|
const shouldAutoScroll = ref(true);
|
|
|
|
|
|
|
2024-07-17 18:52:20 +08:00
|
|
|
|
const panel = ref(null)
|
2024-09-28 00:41:02 +08:00
|
|
|
|
const modelCard = ref(null)
|
2024-07-14 16:42:38 +08:00
|
|
|
|
const examples = ref([
|
2025-03-15 02:02:26 +08:00
|
|
|
|
'写一个简单的冒泡排序',
|
|
|
|
|
|
'今天无锡天气怎么样?',
|
|
|
|
|
|
'介绍一下红楼梦',
|
|
|
|
|
|
'今天星期几?'
|
2024-07-14 16:42:38 +08:00
|
|
|
|
])
|
|
|
|
|
|
|
2024-07-28 16:16:52 +08:00
|
|
|
|
const opts = reactive({
|
2024-07-29 01:00:02 +08:00
|
|
|
|
showPanel: false,
|
2024-09-28 00:41:02 +08:00
|
|
|
|
showModelCard: false,
|
2024-07-29 01:00:02 +08:00
|
|
|
|
openDetail: false,
|
|
|
|
|
|
databases: [],
|
2024-07-28 16:16:52 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2024-07-29 01:00:02 +08:00
|
|
|
|
const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
|
2024-07-18 02:46:58 +08:00
|
|
|
|
use_graph: false,
|
2024-07-25 20:30:28 +08:00
|
|
|
|
use_web: false,
|
2024-07-18 02:46:58 +08:00
|
|
|
|
graph_name: "neo4j",
|
2024-07-29 01:00:02 +08:00
|
|
|
|
selectedKB: null,
|
2025-02-20 01:26:12 +08:00
|
|
|
|
summary_title: false,
|
2025-03-30 15:16:08 +08:00
|
|
|
|
history_round: 20,
|
2025-03-20 19:51:46 +08:00
|
|
|
|
db_id: null,
|
2025-03-23 20:10:29 +08:00
|
|
|
|
fontSize: 'default',
|
|
|
|
|
|
wideScreen: false,
|
2024-07-18 02:46:58 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-04-27 18:59:38 +08:00
|
|
|
|
// 添加全局refs状态
|
|
|
|
|
|
const refsSidebarVisible = ref(false)
|
|
|
|
|
|
const currentRefs = ref({})
|
|
|
|
|
|
|
|
|
|
|
|
// 处理打开refs侧边栏
|
|
|
|
|
|
const handleOpenRefs = ({ type, refs }) => {
|
|
|
|
|
|
console.log('ChatComponent handleOpenRefs called with type:', type);
|
|
|
|
|
|
console.log('Refs data structure:', JSON.stringify(refs));
|
|
|
|
|
|
|
|
|
|
|
|
// 先更新引用数据,确保数据在设置标签页之前已更新
|
|
|
|
|
|
currentRefs.value = Object.assign({}, refs);
|
|
|
|
|
|
|
|
|
|
|
|
// 强制在下一个tick更新,确保数据已经被正确应用
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
// 显示抽屉
|
|
|
|
|
|
refsSidebarVisible.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 再次检查引用是否正确
|
|
|
|
|
|
console.log('Updated refs data:', JSON.stringify(currentRefs.value));
|
|
|
|
|
|
|
|
|
|
|
|
// 根据type自动选择标签页
|
|
|
|
|
|
if (refsSidebarRef.value) {
|
|
|
|
|
|
console.log('Setting active tab to:', type);
|
|
|
|
|
|
// 延迟50毫秒设置标签页,确保抽屉已打开
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
refsSidebarRef.value.setActiveTab(type);
|
|
|
|
|
|
}, 50);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('refsSidebarRef is not available');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加对RefsSidebar的ref
|
|
|
|
|
|
const refsSidebarRef = ref(null)
|
2024-08-25 10:33:48 +08:00
|
|
|
|
|
2025-02-23 16:38:56 +08:00
|
|
|
|
const consoleMsg = (msg) => console.log(msg)
|
2024-07-29 01:00:02 +08:00
|
|
|
|
onClickOutside(panel, () => setTimeout(() => opts.showPanel = false, 30))
|
2024-09-28 00:41:02 +08:00
|
|
|
|
onClickOutside(modelCard, () => setTimeout(() => opts.showModelCard = false, 30))
|
|
|
|
|
|
|
2025-03-30 15:16:08 +08:00
|
|
|
|
// 从 message 中获取 history 信息,每个消息都是 {role, content} 的格式
|
|
|
|
|
|
const getHistory = () => {
|
|
|
|
|
|
const history = conv.value.messages.map((msg) => {
|
2025-03-30 21:40:49 +08:00
|
|
|
|
if (msg.content) {
|
2025-03-30 15:16:08 +08:00
|
|
|
|
return {
|
|
|
|
|
|
role: msg.role === 'sent' ? 'user' : 'assistant',
|
2025-03-30 21:40:49 +08:00
|
|
|
|
content: msg.content
|
2025-03-30 15:16:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}).reduce((acc, cur) => {
|
|
|
|
|
|
if (cur) {
|
|
|
|
|
|
acc.push(cur)
|
|
|
|
|
|
}
|
|
|
|
|
|
return acc
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
return history.slice(-meta.history_round)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-09 17:07:03 +08:00
|
|
|
|
const useDatabase = (index) => {
|
|
|
|
|
|
const selected = opts.databases[index]
|
|
|
|
|
|
console.log(selected)
|
|
|
|
|
|
if (index != null && configStore.config.embed_model != selected.embed_model) {
|
|
|
|
|
|
console.log(selected.embed_model, configStore.config.embed_model)
|
|
|
|
|
|
message.error(`所选知识库的向量模型(${selected.embed_model})与当前向量模型(${configStore.config.embed_model}) 不匹配,请重新选择`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
meta.selectedKB = index
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-18 02:46:58 +08:00
|
|
|
|
const handleKeyDown = (e) => {
|
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
sendMessage()
|
|
|
|
|
|
} else if (e.key === 'Enter' && e.shiftKey) {
|
|
|
|
|
|
// Insert a newline character at the current cursor position
|
|
|
|
|
|
const textarea = e.target;
|
|
|
|
|
|
const start = textarea.selectionStart;
|
|
|
|
|
|
const end = textarea.selectionEnd;
|
|
|
|
|
|
conv.value.inputText.value =
|
|
|
|
|
|
conv.value.inputText.value.substring(0, start) +
|
|
|
|
|
|
'\n' +
|
|
|
|
|
|
conv.value.inputText.value.substring(end);
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
textarea.setSelectionRange(start + 1, start + 1);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-14 16:42:38 +08:00
|
|
|
|
const renameTitle = () => {
|
2024-07-31 20:22:05 +08:00
|
|
|
|
if (meta.summary_title) {
|
|
|
|
|
|
const prompt = '请用一个很短的句子关于下面的对话内容的主题起一个名字,不要带标点符号:'
|
2025-03-30 21:40:49 +08:00
|
|
|
|
const firstUserMessage = conv.value.messages[0].content
|
|
|
|
|
|
const firstAiMessage = conv.value.messages[1].content
|
2024-07-31 20:22:05 +08:00
|
|
|
|
const context = `${prompt}\n\n问题: ${firstUserMessage}\n\n回复: ${firstAiMessage},主题是(一句话):`
|
|
|
|
|
|
simpleCall(context).then((data) => {
|
2024-09-14 02:46:44 +08:00
|
|
|
|
const response = data.response.split(":")[0].replace(/^["'"']/g, '').replace(/["'"']$/g, '')
|
|
|
|
|
|
emit('rename-title', response)
|
2024-07-31 20:22:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
} else {
|
2025-03-30 21:40:49 +08:00
|
|
|
|
emit('rename-title', conv.value.messages[0].content)
|
2024-07-31 20:22:05 +08:00
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-27 13:26:08 +08:00
|
|
|
|
const handleUserScroll = () => {
|
|
|
|
|
|
// 计算我们是否接近底部(100像素以内)
|
2025-03-14 10:07:53 +08:00
|
|
|
|
const isNearBottom = chatContainer.value.scrollHeight - chatContainer.value.scrollTop - chatContainer.value.clientHeight < 20;
|
2025-02-27 13:26:08 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果用户不在底部,则仅将其标记为用户滚动
|
|
|
|
|
|
userIsScrolling.value = !isNearBottom;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果用户再次滚动到底部,请恢复自动滚动
|
|
|
|
|
|
shouldAutoScroll.value = isNearBottom;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-07-14 16:42:38 +08:00
|
|
|
|
const scrollToBottom = () => {
|
2025-02-27 13:26:08 +08:00
|
|
|
|
if (shouldAutoScroll.value) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
chatContainer.value.scrollTop = chatContainer.value.scrollHeight - chatContainer.value.clientHeight;
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const generateRandomHash = (length) => {
|
|
|
|
|
|
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
|
|
|
|
let hash = '';
|
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
|
|
hash += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
|
|
|
|
}
|
|
|
|
|
|
return hash;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-23 16:38:56 +08:00
|
|
|
|
const appendUserMessage = (msg) => {
|
2024-07-14 16:42:38 +08:00
|
|
|
|
conv.value.messages.push({
|
|
|
|
|
|
id: generateRandomHash(16),
|
|
|
|
|
|
role: 'sent',
|
2025-03-30 21:40:49 +08:00
|
|
|
|
content: msg
|
2024-07-14 16:42:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-30 21:40:49 +08:00
|
|
|
|
const appendAiMessage = (content, refs=null) => {
|
2024-07-14 16:42:38 +08:00
|
|
|
|
conv.value.messages.push({
|
|
|
|
|
|
id: generateRandomHash(16),
|
|
|
|
|
|
role: 'received',
|
2025-03-30 21:40:49 +08:00
|
|
|
|
content: content,
|
2025-02-23 16:38:56 +08:00
|
|
|
|
reasoning_content: '',
|
2024-07-28 16:16:52 +08:00
|
|
|
|
refs,
|
2024-10-15 00:38:24 +08:00
|
|
|
|
status: "init",
|
2024-10-23 16:24:29 +08:00
|
|
|
|
meta: {},
|
2025-02-23 17:40:09 +08:00
|
|
|
|
showThinking: "show"
|
2024-07-14 16:42:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-15 00:38:24 +08:00
|
|
|
|
const updateMessage = (info) => {
|
2025-02-23 16:38:56 +08:00
|
|
|
|
const msg = conv.value.messages.find((msg) => msg.id === info.id);
|
|
|
|
|
|
if (msg) {
|
2025-02-20 01:26:12 +08:00
|
|
|
|
try {
|
2025-04-25 09:43:08 +08:00
|
|
|
|
// 特殊处理:content需要追加而不是替换
|
|
|
|
|
|
if (info.content != null && info.content !== '') {
|
2025-04-25 10:14:31 +08:00
|
|
|
|
// 检查新内容中是否有<think>标签
|
|
|
|
|
|
if (info.content.includes('<think>') && !msg.isCollectingThinking) {
|
|
|
|
|
|
// 开始收集思考内容
|
|
|
|
|
|
msg.isCollectingThinking = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 分割内容,获取标签前后的部分
|
|
|
|
|
|
const parts = info.content.split('<think>');
|
|
|
|
|
|
msg.content += parts[0]; // 添加标签前的内容到正文
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有标签后的内容,添加到思考内容
|
|
|
|
|
|
if (parts.length > 1) {
|
|
|
|
|
|
if (parts[1].includes('</think>')) {
|
|
|
|
|
|
const thinkParts = parts[1].split('</think>');
|
|
|
|
|
|
msg.reasoning_content = (msg.reasoning_content || '') + thinkParts[0];
|
|
|
|
|
|
msg.content += thinkParts[1]; // 添加结束标签后的内容到正文
|
|
|
|
|
|
msg.isCollectingThinking = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
msg.reasoning_content = (msg.reasoning_content || '') + parts[1];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 检查是否正在收集思考内容
|
|
|
|
|
|
else if (msg.isCollectingThinking) {
|
|
|
|
|
|
if (info.content.includes('</think>')) {
|
|
|
|
|
|
const parts = info.content.split('</think>');
|
|
|
|
|
|
msg.reasoning_content = (msg.reasoning_content || '') + parts[0];
|
|
|
|
|
|
msg.content += parts[1]; // 添加结束标签后的内容到正文
|
|
|
|
|
|
msg.isCollectingThinking = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
msg.reasoning_content = (msg.reasoning_content || '') + info.content;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 不在收集思考内容,正常追加
|
|
|
|
|
|
else {
|
|
|
|
|
|
msg.content += info.content;
|
|
|
|
|
|
}
|
2025-02-20 01:26:12 +08:00
|
|
|
|
}
|
2024-10-14 22:10:56 +08:00
|
|
|
|
|
2025-04-25 09:43:08 +08:00
|
|
|
|
// 批量处理其他属性,只有当属性值不为null/undefined且不为空字符串时才更新
|
|
|
|
|
|
const propertiesToUpdate = [
|
|
|
|
|
|
'reasoning_content', 'model_name', 'status', 'message', 'showThinking', 'refs', 'meta'
|
|
|
|
|
|
];
|
2025-02-23 16:38:56 +08:00
|
|
|
|
|
2025-04-25 09:43:08 +08:00
|
|
|
|
propertiesToUpdate.forEach(prop => {
|
|
|
|
|
|
if (info[prop] != null && (typeof info[prop] !== 'string' || info[prop] !== '')) {
|
|
|
|
|
|
msg[prop] = info[prop];
|
2025-04-27 18:59:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果更新了refs,同时更新全局refs
|
|
|
|
|
|
if (prop === 'refs' && info.refs) {
|
|
|
|
|
|
currentRefs.value = info.refs;
|
|
|
|
|
|
}
|
2025-04-25 09:43:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-02-23 17:40:09 +08:00
|
|
|
|
|
2025-02-20 01:26:12 +08:00
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error updating message:', error);
|
2025-02-23 16:38:56 +08:00
|
|
|
|
msg.status = 'error';
|
2025-03-30 21:40:49 +08:00
|
|
|
|
msg.content = '消息更新失败';
|
2024-10-23 16:24:29 +08:00
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
} else {
|
2025-02-20 01:26:12 +08:00
|
|
|
|
console.error('Message not found:', info.id);
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
2024-08-25 10:33:48 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-10-23 16:24:29 +08:00
|
|
|
|
const groupRefs = (id) => {
|
2025-02-23 16:38:56 +08:00
|
|
|
|
const msg = conv.value.messages.find((msg) => msg.id === id)
|
|
|
|
|
|
if (msg.refs && msg.refs.knowledge_base.results.length > 0) {
|
|
|
|
|
|
msg.groupedResults = msg.refs.knowledge_base.results
|
2024-10-14 16:51:20 +08:00
|
|
|
|
.filter(result => result.file && result.file.filename)
|
|
|
|
|
|
.reduce((acc, result) => {
|
2024-07-28 16:16:52 +08:00
|
|
|
|
const { filename } = result.file;
|
|
|
|
|
|
if (!acc[filename]) {
|
|
|
|
|
|
acc[filename] = []
|
|
|
|
|
|
}
|
|
|
|
|
|
acc[filename].push(result)
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {})
|
|
|
|
|
|
}
|
2024-09-03 16:37:59 +08:00
|
|
|
|
scrollToBottom()
|
2024-07-28 16:16:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-29 01:00:02 +08:00
|
|
|
|
const loadDatabases = () => {
|
2025-05-04 21:04:01 +08:00
|
|
|
|
// 由于这是管理功能,需要检查用户是否有管理权限
|
|
|
|
|
|
if (!userStore.isAdmin) {
|
|
|
|
|
|
console.log('非管理员用户,跳过加载数据库列表');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
knowledgeBaseApi.getDatabases()
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
console.log(data)
|
|
|
|
|
|
opts.databases = data.databases
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error('加载数据库列表失败:', error)
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取数据库列表失败:', error);
|
|
|
|
|
|
}
|
2024-07-29 01:00:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
const simpleCall = (msg) => {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
chatApi.simpleCall(msg)
|
|
|
|
|
|
.then(data => resolve(data))
|
|
|
|
|
|
.catch(error => reject(error))
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 替换fetchChatResponse函数
|
2024-10-02 20:11:28 +08:00
|
|
|
|
const fetchChatResponse = (user_input, cur_res_id) => {
|
2025-03-30 11:57:06 +08:00
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
const signal = controller.signal;
|
|
|
|
|
|
|
2025-03-30 15:16:08 +08:00
|
|
|
|
const params = {
|
|
|
|
|
|
query: user_input,
|
2025-05-02 23:56:59 +08:00
|
|
|
|
history: getHistory().slice(0, -1), // 去掉最后一条刚添加的用户消息
|
2025-03-30 15:16:08 +08:00
|
|
|
|
meta: meta,
|
|
|
|
|
|
cur_res_id: cur_res_id,
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(params)
|
|
|
|
|
|
|
2025-05-04 21:04:01 +08:00
|
|
|
|
// 使用API函数发送请求
|
|
|
|
|
|
chatApi.sendMessageWithAbort(params, signal)
|
2024-10-14 22:10:56 +08:00
|
|
|
|
.then((response) => {
|
2025-05-02 23:56:59 +08:00
|
|
|
|
if (!response.ok) {
|
2025-05-04 21:04:01 +08:00
|
|
|
|
// 检查是否是401错误(令牌过期)
|
|
|
|
|
|
if (response.status === 401) {
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
if (userStore.isLoggedIn) {
|
|
|
|
|
|
message.error('登录已过期,请重新登录');
|
|
|
|
|
|
userStore.logout();
|
|
|
|
|
|
|
|
|
|
|
|
// 使用setTimeout确保消息显示后再跳转
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
window.location.href = '/login';
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error('未授权,请先登录');
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
|
2025-05-02 23:56:59 +08:00
|
|
|
|
}
|
2025-05-04 21:04:01 +08:00
|
|
|
|
|
2024-10-14 22:10:56 +08:00
|
|
|
|
if (!response.body) throw new Error("ReadableStream not supported.");
|
2024-10-02 20:11:28 +08:00
|
|
|
|
const reader = response.body.getReader();
|
2024-10-14 22:10:56 +08:00
|
|
|
|
const decoder = new TextDecoder("utf-8");
|
2024-10-02 20:11:28 +08:00
|
|
|
|
let buffer = '';
|
|
|
|
|
|
|
|
|
|
|
|
const readChunk = () => {
|
|
|
|
|
|
return reader.read().then(({ done, value }) => {
|
|
|
|
|
|
if (done) {
|
2025-02-23 16:38:56 +08:00
|
|
|
|
const msg = conv.value.messages.find((msg) => msg.id === cur_res_id)
|
|
|
|
|
|
console.log(msg)
|
2025-02-24 19:43:40 +08:00
|
|
|
|
groupRefs(cur_res_id);
|
2025-02-23 17:40:09 +08:00
|
|
|
|
updateMessage({showThinking: "no", id: cur_res_id});
|
2025-04-27 18:59:38 +08:00
|
|
|
|
// 更新全局refs为最新消息的refs
|
|
|
|
|
|
if (msg && msg.refs) {
|
|
|
|
|
|
// 深拷贝refs以确保不会出现引用问题
|
|
|
|
|
|
currentRefs.value = JSON.parse(JSON.stringify(msg.refs));
|
|
|
|
|
|
console.log('Updated currentRefs on response completion:', currentRefs.value);
|
|
|
|
|
|
}
|
2024-10-02 20:11:28 +08:00
|
|
|
|
isStreaming.value = false;
|
|
|
|
|
|
if (conv.value.messages.length === 2) { renameTitle(); }
|
2024-10-14 22:10:56 +08:00
|
|
|
|
return;
|
2024-10-02 20:11:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-24 13:10:17 +08:00
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
|
const lines = buffer.split('\n');
|
|
|
|
|
|
|
|
|
|
|
|
// 处理除最后一行外的所有完整行
|
|
|
|
|
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
|
|
|
|
const line = lines[i].trim();
|
|
|
|
|
|
if (line) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(line);
|
|
|
|
|
|
updateMessage({
|
|
|
|
|
|
id: cur_res_id,
|
2025-03-30 21:40:49 +08:00
|
|
|
|
content: data.response,
|
2025-02-23 16:38:56 +08:00
|
|
|
|
reasoning_content: data.reasoning_content,
|
2024-10-24 13:10:17 +08:00
|
|
|
|
status: data.status,
|
|
|
|
|
|
meta: data.meta,
|
2025-02-23 16:38:56 +08:00
|
|
|
|
...data,
|
2024-10-24 13:10:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (data.history) {
|
|
|
|
|
|
conv.value.history = data.history;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('JSON 解析错误:', e, line);
|
|
|
|
|
|
}
|
2024-10-15 00:38:24 +08:00
|
|
|
|
}
|
2024-10-14 22:10:56 +08:00
|
|
|
|
}
|
2024-10-24 13:10:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 保留最后一个可能不完整的行
|
|
|
|
|
|
buffer = lines[lines.length - 1];
|
|
|
|
|
|
|
2024-10-02 20:11:28 +08:00
|
|
|
|
return readChunk(); // 继续读取
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2024-10-14 22:10:56 +08:00
|
|
|
|
readChunk();
|
2024-10-02 20:11:28 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
2025-03-30 11:57:06 +08:00
|
|
|
|
if (error.name === 'AbortError') {
|
|
|
|
|
|
console.log('Fetch aborted');
|
|
|
|
|
|
} else {
|
2025-05-04 21:04:01 +08:00
|
|
|
|
console.error('聊天请求错误:', error);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否是认证错误
|
|
|
|
|
|
if (error.message.includes('未授权') || error.message.includes('令牌已过期')) {
|
|
|
|
|
|
// 已在上面处理,这里不需要重复处理
|
|
|
|
|
|
} else {
|
|
|
|
|
|
updateMessage({
|
|
|
|
|
|
id: cur_res_id,
|
|
|
|
|
|
status: "error",
|
|
|
|
|
|
message: error.message || '请求失败',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-03-30 11:57:06 +08:00
|
|
|
|
}
|
2024-10-02 20:11:28 +08:00
|
|
|
|
isStreaming.value = false;
|
2024-10-24 13:10:17 +08:00
|
|
|
|
});
|
2025-03-30 11:57:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听 isStreaming 变化,当为 false 时中断请求
|
|
|
|
|
|
watch(isStreaming, (newValue) => {
|
|
|
|
|
|
if (!newValue) {
|
|
|
|
|
|
controller.abort();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2024-10-14 22:10:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新后的 sendMessage 函数
|
|
|
|
|
|
const sendMessage = () => {
|
|
|
|
|
|
const user_input = conv.value.inputText.trim();
|
2025-03-20 19:51:46 +08:00
|
|
|
|
const dbID = opts.databases.length > 0 ? opts.databases[meta.selectedKB]?.db_id : null;
|
2025-02-25 21:26:46 +08:00
|
|
|
|
if (isStreaming.value) {
|
|
|
|
|
|
message.error('请等待上一条消息处理完成');
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2024-10-02 20:11:28 +08:00
|
|
|
|
if (user_input) {
|
|
|
|
|
|
isStreaming.value = true;
|
|
|
|
|
|
appendUserMessage(user_input);
|
|
|
|
|
|
appendAiMessage("", null);
|
2025-02-27 13:26:08 +08:00
|
|
|
|
forceScrollToBottom();
|
|
|
|
|
|
|
2024-10-02 20:11:28 +08:00
|
|
|
|
const cur_res_id = conv.value.messages[conv.value.messages.length - 1].id;
|
|
|
|
|
|
conv.value.inputText = '';
|
2025-03-20 19:51:46 +08:00
|
|
|
|
meta.db_id = dbID;
|
2024-10-02 20:11:28 +08:00
|
|
|
|
fetchChatResponse(user_input, cur_res_id)
|
2024-07-14 16:42:38 +08:00
|
|
|
|
} else {
|
2024-10-02 20:11:28 +08:00
|
|
|
|
console.log('请输入消息');
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-15 00:38:24 +08:00
|
|
|
|
const retryMessage = (id) => {
|
|
|
|
|
|
// 找到 id 对应的 message,然后删除包含 message 在内以及后面所有的 message
|
2025-02-23 16:38:56 +08:00
|
|
|
|
const index = conv.value.messages.findIndex(msg => msg.id === id);
|
2024-10-15 00:38:24 +08:00
|
|
|
|
const pastMessage = conv.value.messages[index-1]
|
2024-10-23 16:24:29 +08:00
|
|
|
|
console.log("retryMessage", id, pastMessage)
|
2025-03-30 21:40:49 +08:00
|
|
|
|
conv.value.inputText = pastMessage.content
|
2024-10-15 00:38:24 +08:00
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
conv.value.messages = conv.value.messages.slice(0, index-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(conv.value.messages)
|
|
|
|
|
|
sendMessage();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-29 01:00:02 +08:00
|
|
|
|
// 从本地存储加载数据
|
2024-07-14 16:42:38 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
scrollToBottom()
|
2024-07-29 01:00:02 +08:00
|
|
|
|
loadDatabases()
|
2025-02-27 13:26:08 +08:00
|
|
|
|
|
|
|
|
|
|
chatContainer.value.addEventListener('scroll', handleUserScroll);
|
|
|
|
|
|
|
2025-03-30 11:36:03 +08:00
|
|
|
|
// 检查现有消息中是否有内容为空的情况
|
|
|
|
|
|
if (conv.value.messages && conv.value.messages.length > 0) {
|
|
|
|
|
|
conv.value.messages.forEach(msg => {
|
2025-03-30 21:40:49 +08:00
|
|
|
|
if (msg.role === 'received' && (!msg.content || msg.content.trim() === '')) {
|
2025-03-30 11:36:03 +08:00
|
|
|
|
msg.status = 'error';
|
|
|
|
|
|
msg.message = '内容加载失败';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(conv.value.messages)
|
|
|
|
|
|
|
2025-02-27 13:26:08 +08:00
|
|
|
|
// 从本地存储加载数据
|
2024-07-29 01:00:02 +08:00
|
|
|
|
const storedMeta = localStorage.getItem('meta');
|
|
|
|
|
|
if (storedMeta) {
|
|
|
|
|
|
const parsedMeta = JSON.parse(storedMeta);
|
|
|
|
|
|
Object.assign(meta, parsedMeta);
|
|
|
|
|
|
}
|
2025-04-27 18:59:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查refsSidebarRef是否正确挂载
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
console.log('Is refsSidebarRef mounted?', !!refsSidebarRef.value);
|
|
|
|
|
|
});
|
2024-07-29 01:00:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-02-27 13:26:08 +08:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (chatContainer.value) {
|
|
|
|
|
|
chatContainer.value.removeEventListener('scroll', handleUserScroll);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 添加新函数来处理特定的滚动行为
|
|
|
|
|
|
const forceScrollToBottom = () => {
|
|
|
|
|
|
shouldAutoScroll.value = true;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
chatContainer.value.scrollTop = chatContainer.value.scrollHeight - chatContainer.value.clientHeight;
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-07-29 01:00:02 +08:00
|
|
|
|
// 监听 meta 对象的变化,并保存到本地存储
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => meta,
|
|
|
|
|
|
(newMeta) => {
|
|
|
|
|
|
localStorage.setItem('meta', JSON.stringify(newMeta));
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
);
|
2025-03-30 11:53:18 +08:00
|
|
|
|
|
2025-03-30 11:57:06 +08:00
|
|
|
|
// 处理发送或停止
|
|
|
|
|
|
const handleSendOrStop = () => {
|
|
|
|
|
|
if (isStreaming.value) {
|
|
|
|
|
|
// 停止生成
|
|
|
|
|
|
isStreaming.value = false;
|
|
|
|
|
|
const lastMessage = conv.value.messages[conv.value.messages.length - 1];
|
|
|
|
|
|
if (lastMessage) {
|
|
|
|
|
|
lastMessage.isStoppedByUser = true;
|
|
|
|
|
|
lastMessage.status = 'stopped';
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 发送消息
|
|
|
|
|
|
sendMessage();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重试被停止的消息
|
2025-03-30 20:46:26 +08:00
|
|
|
|
const retryStoppedMessage = (id) => {
|
2025-03-30 11:57:06 +08:00
|
|
|
|
// 找到用户的原始问题
|
2025-03-30 20:46:26 +08:00
|
|
|
|
const messageIndex = conv.value.messages.findIndex(msg => msg.id === id);
|
2025-03-30 11:57:06 +08:00
|
|
|
|
if (messageIndex > 0) {
|
|
|
|
|
|
const userMessage = conv.value.messages[messageIndex - 1];
|
|
|
|
|
|
if (userMessage && userMessage.role === 'sent') {
|
2025-03-30 21:40:49 +08:00
|
|
|
|
conv.value.inputText = userMessage.content;
|
2025-03-30 20:46:26 +08:00
|
|
|
|
// 删除被停止的消息,以及所有后面的消息
|
2025-03-30 15:21:01 +08:00
|
|
|
|
conv.value.messages = conv.value.messages.slice(0, messageIndex-1);
|
2025-03-30 20:46:26 +08:00
|
|
|
|
// sendMessage();
|
2025-03-30 11:57:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-30 12:31:36 +08:00
|
|
|
|
}
|
2025-03-30 12:21:18 +08:00
|
|
|
|
|
2025-03-30 11:53:18 +08:00
|
|
|
|
const modelNames = computed(() => configStore.config?.model_names)
|
|
|
|
|
|
const modelStatus = computed(() => configStore.config?.model_provider_status)
|
|
|
|
|
|
const customModels = computed(() => configStore.config?.custom_models || [])
|
|
|
|
|
|
|
|
|
|
|
|
// 筛选 modelStatus 中为真的key
|
|
|
|
|
|
const modelKeys = computed(() => {
|
|
|
|
|
|
return Object.keys(modelStatus.value || {}).filter(key => modelStatus.value?.[key])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 选择模型的方法
|
|
|
|
|
|
const selectModel = (provider, name) => {
|
2025-05-09 23:48:06 +08:00
|
|
|
|
configStore.setConfigValues({
|
|
|
|
|
|
model_provider: provider,
|
|
|
|
|
|
model_name: name,
|
|
|
|
|
|
})
|
2025-04-15 12:39:46 +08:00
|
|
|
|
// message.success(`已切换到模型: ${name} | ${provider}`)
|
2025-03-30 11:53:18 +08:00
|
|
|
|
}
|
2025-04-27 18:59:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 判断是否是最新的助手消息
|
|
|
|
|
|
const isLatestMessage = (index) => {
|
|
|
|
|
|
// 找到最后一条助手消息的索引
|
|
|
|
|
|
const lastAssistantMsgIndex = findLastIndex(conv.value.messages,
|
|
|
|
|
|
msg => (msg.role === 'received' || msg.role === 'assistant') && msg.status === 'finished');
|
|
|
|
|
|
|
|
|
|
|
|
// 如果当前索引等于最后一条助手消息的索引,则为最新消息
|
|
|
|
|
|
return index === lastAssistantMsgIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 辅助函数:从后向前查找满足条件的元素索引
|
|
|
|
|
|
const findLastIndex = (array, predicate) => {
|
|
|
|
|
|
for (let i = array.length - 1; i >= 0; i--) {
|
|
|
|
|
|
if (predicate(array[i])) {
|
|
|
|
|
|
return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
|
.chat {
|
2024-07-25 20:30:28 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
2024-09-24 11:58:28 +08:00
|
|
|
|
max-height: 100vh;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
overflow-x: hidden;
|
2025-04-05 13:40:53 +08:00
|
|
|
|
background: var(--main-light-7);
|
2024-07-14 16:42:38 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
box-sizing: border-box;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
flex: 5 5 200px;
|
|
|
|
|
|
overflow-y: scroll;
|
2024-07-17 18:52:20 +08:00
|
|
|
|
|
2025-02-27 01:37:42 +08:00
|
|
|
|
.chat-header {
|
2024-07-25 20:30:28 +08:00
|
|
|
|
user-select: none;
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
z-index: 10;
|
2025-03-28 03:17:09 +08:00
|
|
|
|
background-color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
backdrop-filter: blur(10px);
|
2024-07-25 20:30:28 +08:00
|
|
|
|
height: var(--header-height);
|
2024-07-17 18:52:20 +08:00
|
|
|
|
display: flex;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
justify-content: space-between;
|
2024-07-17 18:52:20 +08:00
|
|
|
|
align-items: center;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
padding: 1rem;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.header__left, .header__right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.nav-btn {
|
|
|
|
|
|
height: 2.5rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
border-radius: 8px;
|
2024-09-25 13:46:51 +08:00
|
|
|
|
color: var(--gray-900);
|
2024-07-25 20:30:28 +08:00
|
|
|
|
cursor: pointer;
|
2025-03-30 11:53:18 +08:00
|
|
|
|
// font-size: 1rem;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
width: auto;
|
2025-03-25 05:40:07 +08:00
|
|
|
|
transition: background-color 0.3s;
|
2025-03-30 11:53:18 +08:00
|
|
|
|
padding: 0.5rem 0.75rem;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.text {
|
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2024-07-26 14:13:05 +08:00
|
|
|
|
background-color: var(--main-light-3);
|
2024-07-25 20:30:28 +08:00
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-15 12:39:46 +08:00
|
|
|
|
.nav-btn-icon-only {
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-30 11:53:18 +08:00
|
|
|
|
.model-select {
|
|
|
|
|
|
// color: var(--gray-900);
|
2025-04-15 12:39:46 +08:00
|
|
|
|
max-width: 350px;
|
2025-03-30 11:53:18 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
|
2025-04-15 12:39:46 +08:00
|
|
|
|
.model-text {
|
2025-03-30 11:53:18 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-25 20:30:28 +08:00
|
|
|
|
}
|
2024-07-18 02:46:58 +08:00
|
|
|
|
.metas {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-17 18:52:20 +08:00
|
|
|
|
.my-panal {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
background-color: white;
|
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
|
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.05);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 12px;
|
2024-07-31 20:22:05 +08:00
|
|
|
|
z-index: 11;
|
2024-09-09 17:07:03 +08:00
|
|
|
|
width: 280px;
|
2025-03-25 05:40:07 +08:00
|
|
|
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
2024-07-18 02:46:58 +08:00
|
|
|
|
|
|
|
|
|
|
.flex-center {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 8px 16px;
|
2024-08-11 17:48:32 +08:00
|
|
|
|
border-radius: 8px;
|
2024-07-18 02:46:58 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2024-07-26 14:13:05 +08:00
|
|
|
|
background-color: var(--main-light-3);
|
2024-07-18 02:46:58 +08:00
|
|
|
|
}
|
2025-02-20 01:26:12 +08:00
|
|
|
|
|
|
|
|
|
|
.anticon {
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ant-switch {
|
|
|
|
|
|
&.ant-switch-checked {
|
|
|
|
|
|
background-color: var(--main-500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-18 02:46:58 +08:00
|
|
|
|
}
|
2024-07-17 18:52:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-28 00:41:02 +08:00
|
|
|
|
.my-panal.r0.top100 {
|
|
|
|
|
|
top: 100%;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.my-panal.l0.top100 {
|
|
|
|
|
|
top: 100%;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.chat-examples {
|
2024-07-14 16:42:38 +08:00
|
|
|
|
padding: 0 50px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 20%;
|
|
|
|
|
|
width: 100%;
|
2024-07-31 20:22:05 +08:00
|
|
|
|
z-index: 9;
|
2024-10-24 20:08:23 +08:00
|
|
|
|
animation: slideInUp 0.5s ease-out;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
h1 {
|
|
|
|
|
|
margin-bottom: 20px;
|
2025-03-23 19:22:00 +08:00
|
|
|
|
font-size: 1.2rem;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-29 01:00:02 +08:00
|
|
|
|
.opts {
|
2024-07-25 20:30:28 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 10px;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.opt__button {
|
2024-09-28 00:41:02 +08:00
|
|
|
|
background-color: var(--gray-200);
|
2024-09-18 21:03:12 +08:00
|
|
|
|
color: #333;
|
|
|
|
|
|
padding: .5rem 1.5rem;
|
|
|
|
|
|
border-radius: 2rem;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
cursor: pointer;
|
2024-07-26 14:13:05 +08:00
|
|
|
|
// border: 2px solid var(--main-light-4);
|
2024-07-25 20:30:28 +08:00
|
|
|
|
transition: background-color 0.3s;
|
2024-09-18 21:03:12 +08:00
|
|
|
|
// box-shadow: 0px 0px 10px 2px var(--main-light-4);
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2024-09-18 21:03:12 +08:00
|
|
|
|
background-color: #f0f1f1;
|
|
|
|
|
|
// box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
|
2024-07-25 20:30:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
2024-07-25 20:30:28 +08:00
|
|
|
|
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-box {
|
2024-07-25 20:30:28 +08:00
|
|
|
|
width: 100%;
|
2025-02-27 13:26:08 +08:00
|
|
|
|
max-width: 800px;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
flex-grow: 1;
|
2024-09-03 16:37:59 +08:00
|
|
|
|
padding: 1rem 2rem;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-03-23 20:10:29 +08:00
|
|
|
|
transition: max-width 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
&.wide-screen {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.font-smaller {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
|
|
|
|
|
|
.message-box {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.font-larger {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.message-box {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-28 16:16:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.bottom {
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 100%;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
margin: 0 auto;
|
2024-08-25 10:33:48 +08:00
|
|
|
|
padding: 4px 2rem 0 2rem;
|
2024-07-14 16:42:38 +08:00
|
|
|
|
|
2025-03-28 03:17:09 +08:00
|
|
|
|
.message-input-wrapper {
|
2024-07-25 20:30:28 +08:00
|
|
|
|
width: 100%;
|
2025-02-27 13:26:08 +08:00
|
|
|
|
max-width: 800px;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
margin: 0 auto;
|
2025-03-28 03:17:09 +08:00
|
|
|
|
background-color: white;
|
2025-03-28 10:57:19 +08:00
|
|
|
|
animation: width 0.3s ease-in-out;
|
2025-03-23 20:10:29 +08:00
|
|
|
|
|
|
|
|
|
|
&.wide-screen {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-28 03:17:09 +08:00
|
|
|
|
.note {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
font-size: small;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
color: #ccc;
|
2025-03-28 11:40:58 +08:00
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
margin-bottom: 0;
|
2025-03-28 03:17:09 +08:00
|
|
|
|
user-select: none;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
}
|
2024-09-18 21:03:12 +08:00
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.ant-dropdown-link {
|
2024-09-25 13:46:51 +08:00
|
|
|
|
color: var(--gray-900);
|
2024-07-25 20:30:28 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-31 20:22:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.chat::-webkit-scrollbar {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat::-webkit-scrollbar-thumb {
|
2024-09-25 13:46:51 +08:00
|
|
|
|
background: var(--gray-400);
|
2024-07-31 20:22:05 +08:00
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: rgb(100, 100, 100);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat::-webkit-scrollbar-thumb:active {
|
|
|
|
|
|
background: rgb(68, 68, 68);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-25 10:33:48 +08:00
|
|
|
|
.loading-dots {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-dots div {
|
2024-09-14 02:46:44 +08:00
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
margin: 0 4px;
|
|
|
|
|
|
background-color: #666;
|
2024-08-25 10:33:48 +08:00
|
|
|
|
border-radius: 50%;
|
2024-09-14 02:46:44 +08:00
|
|
|
|
opacity: 0.3;
|
2025-03-11 00:03:29 +08:00
|
|
|
|
animation: pulse 0.5s infinite ease-in-out both;
|
2024-08-25 10:33:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-dots div:nth-child(1) {
|
2024-09-14 02:46:44 +08:00
|
|
|
|
animation-delay: -0.32s;
|
2024-08-25 10:33:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-dots div:nth-child(2) {
|
2024-09-14 02:46:44 +08:00
|
|
|
|
animation-delay: -0.16s;
|
2024-08-25 10:33:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-14 02:46:44 +08:00
|
|
|
|
@keyframes pulse {
|
|
|
|
|
|
0%, 80%, 100% {
|
2024-10-24 20:08:23 +08:00
|
|
|
|
transform: scale(0.8);
|
2024-09-14 02:46:44 +08:00
|
|
|
|
opacity: 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
40% {
|
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
2024-08-25 10:33:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}}
|
|
|
|
|
|
|
|
|
|
|
|
.slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
|
2025-03-25 05:40:07 +08:00
|
|
|
|
.swing-in-top-fwd {
|
|
|
|
|
|
-webkit-animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
|
|
|
|
|
|
animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes swing-in-top-fwd {
|
|
|
|
|
|
0% {
|
|
|
|
|
|
-webkit-transform: rotateX(-100deg);
|
|
|
|
|
|
transform: rotateX(-100deg);
|
|
|
|
|
|
-webkit-transform-origin: top;
|
|
|
|
|
|
transform-origin: top;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
100% {
|
|
|
|
|
|
-webkit-transform: rotateX(0deg);
|
|
|
|
|
|
transform: rotateX(0deg);
|
|
|
|
|
|
-webkit-transform-origin: top;
|
|
|
|
|
|
transform-origin: top;
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-31 20:22:05 +08:00
|
|
|
|
|
2024-10-24 20:08:23 +08:00
|
|
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
|
|
|
|
@keyframes slideInUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
|
|
|
|
|
2024-07-25 20:30:28 +08:00
|
|
|
|
@media (max-width: 520px) {
|
|
|
|
|
|
.chat {
|
|
|
|
|
|
height: calc(100vh - 60px);
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
2024-07-25 20:30:28 +08:00
|
|
|
|
|
2025-02-27 01:37:42 +08:00
|
|
|
|
.chat-container .chat .chat-header {
|
2024-07-26 14:13:05 +08:00
|
|
|
|
background: var(--main-light-4);
|
2024-07-25 20:30:28 +08:00
|
|
|
|
.header__left, .header__right {
|
2025-02-27 01:37:42 +08:00
|
|
|
|
gap: 24px;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-btn {
|
2025-02-27 01:37:42 +08:00
|
|
|
|
font-size: 1.3rem;
|
2024-07-25 20:30:28 +08:00
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
color: black;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.text {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
2024-07-25 20:30:28 +08:00
|
|
|
|
|
2024-07-27 14:32:57 +08:00
|
|
|
|
.bottom {
|
|
|
|
|
|
padding: 0.5rem 0.5rem;
|
|
|
|
|
|
|
|
|
|
|
|
.input-box {
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
|
|
|
|
|
|
|
textarea.user-input {
|
|
|
|
|
|
padding: 0.5rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.note {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
}
|
2024-10-24 20:08:23 +08:00
|
|
|
|
|
2025-02-20 01:26:12 +08:00
|
|
|
|
.controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
.search-switch {
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-30 11:53:18 +08:00
|
|
|
|
|
|
|
|
|
|
.scrollable-menu {
|
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: var(--gray-400);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: var(--gray-500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-07-14 16:42:38 +08:00
|
|
|
|
</style>
|
2024-08-25 10:33:48 +08:00
|
|
|
|
|
2025-03-30 11:53:18 +08:00
|
|
|
|
<style lang="less">
|
|
|
|
|
|
// 添加全局样式以确保滚动功能在dropdown内正常工作
|
|
|
|
|
|
.ant-dropdown-menu {
|
|
|
|
|
|
&.scrollable-menu {
|
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
2024-10-24 20:08:23 +08:00
|
|
|
|
|