ForcePilot/web/src/components/ChatComponent.vue

944 lines
24 KiB
Vue
Raw Normal View History

2024-07-14 16:42:38 +08:00
<template>
<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"
class="close nav-btn"
@click="state.isSidebarOpen = true"
>
2025-03-28 00:48:33 +08:00
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-24" alt="设置" />
</div>
2024-09-29 16:13:47 +08:00
<a-tooltip :title="configStore.config?.model_name" placement="rightTop">
2024-09-28 00:41:02 +08:00
<div class="newchat nav-btn" @click="$emit('newconv')">
<PlusCircleOutlined /> <span class="text">新对话</span>
</div>
</a-tooltip>
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">
<div class="flex-center" @click="meta.stream = !meta.stream">
流式输出 <div @click.stop><a-switch v-model:checked="meta.stream" /></div>
</div>
<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' }">
<MessageComponent
2024-07-14 16:42:38 +08:00
v-for="message in conv.messages"
:key="message.id"
:role="message.role"
:content="message.text"
:content-html="message.text.length > 0 ? renderMarkdown(message) : ''"
:reasoning-content="message.reasoning_content"
:reasoning-header="message.status=='reasoning' ? '正在思考...' : '推理过程'"
:status="message.status"
:is-processing="isStreaming"
:error-message="message.message"
@retry="retryMessage(message.id)"
2024-07-14 16:42:38 +08:00
>
<template #refs v-if="message.role=='received' && message.status=='finished'">
<RefsComponent :message="message" />
</template>
</MessageComponent>
2024-07-14 16:42:38 +08:00
</div>
<div class="bottom">
<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 }"
@send="sendMessage"
@keydown="handleKeyDown"
>
<template #options-left>
<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;"/>
知识图谱
</div>
<a-dropdown
2025-02-26 12:37:06 +08:00
v-if="configStore.config.enable_knowledge_base && opts.databases.length > 0"
: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>
</div>
2024-07-14 16:42:38 +08:00
</div>
</div>
</template>
<script setup>
2025-02-27 13:26:08 +08:00
import { reactive, ref, onMounted, toRefs, nextTick, onUnmounted, watch } from 'vue'
2024-07-17 18:52:20 +08:00
import {
SendOutlined,
MenuOutlined,
FormOutlined,
LoadingOutlined,
BookOutlined,
BookFilled,
CompassOutlined,
ArrowUpOutlined,
2024-07-18 02:46:58 +08:00
CompassFilled,
GoldenFilled,
GoldOutlined,
2024-07-21 18:15:04 +08:00
SettingOutlined,
2024-07-22 00:01:01 +08:00
SettingFilled,
PlusCircleOutlined,
2024-07-27 14:32:57 +08:00
FolderOutlined,
FolderOpenOutlined,
2024-08-25 20:29:24 +08:00
GlobalOutlined,
FileTextOutlined,
2024-09-14 02:46:44 +08:00
RobotOutlined,
2025-02-23 17:40:09 +08:00
CaretRightOutlined,
2025-02-24 22:56:13 +08:00
DeploymentUnitOutlined,
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-08-25 10:33:48 +08:00
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
2024-07-27 14:32:57 +08:00
import { useConfigStore } from '@/stores/config'
2024-09-09 17:07:03 +08:00
import { message } from 'ant-design-vue'
2024-08-25 20:29:24 +08:00
import RefsComponent from '@/components/RefsComponent.vue'
2024-08-25 10:33:48 +08:00
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
2025-03-28 03:17:09 +08:00
import MessageInputComponent from '@/components/MessageInputComponent.vue'
import MessageComponent from '@/components/MessageComponent.vue'
2024-07-14 16:42:38 +08:00
const props = defineProps({
conv: Object,
state: Object
})
const emit = defineEmits(['rename-title', 'newconv']);
2024-07-27 14:32:57 +08:00
const configStore = useConfigStore()
2024-07-14 16:42:38 +08:00
const { conv, state } = toRefs(props)
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
])
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-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,
use_web: false,
2024-07-18 02:46:58 +08:00
graph_name: "neo4j",
2024-07-29 01:00:02 +08:00
selectedKB: null,
2024-07-31 20:22:05 +08:00
stream: true,
summary_title: false,
history_round: 5,
db_id: null,
2025-03-23 20:10:29 +08:00
fontSize: 'default',
wideScreen: false,
2024-07-18 02:46:58 +08:00
})
2024-08-25 10:33:48 +08:00
const marked = new Marked(
{
gfm: true,
breaks: true,
tables: true,
},
markedHighlight({
langPrefix: 'hljs language-',
highlight(code) {
return hljs.highlightAuto(code).value;
}
})
);
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-02-23 16:38:56 +08:00
const renderMarkdown = (msg) => {
if (msg.status === 'loading') {
return marked.parse(msg.text + '🟢')
2024-08-25 10:33:48 +08:00
} else {
2025-02-23 16:38:56 +08:00
return marked.parse(msg.text)
2024-08-25 10:33:48 +08:00
}
}
2024-07-17 18:52:20 +08:00
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 = '请用一个很短的句子关于下面的对话内容的主题起一个名字,不要带标点符号:'
const firstUserMessage = conv.value.messages[0].text
const firstAiMessage = conv.value.messages[1].text
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 {
emit('rename-title', conv.value.messages[0].text)
}
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-02-23 16:38:56 +08:00
text: msg
2024-07-14 16:42:38 +08:00
})
scrollToBottom()
}
const appendAiMessage = (text, refs=null) => {
2024-07-14 16:42:38 +08:00
conv.value.messages.push({
id: generateRandomHash(16),
role: 'received',
text: text,
2025-02-23 16:38:56 +08:00
reasoning_content: '',
refs,
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()
}
const updateMessage = (info) => {
2025-02-23 16:38:56 +08:00
const msg = conv.value.messages.find((msg) => msg.id === info.id);
if (msg) {
try {
// 只有在 text 不为空时更新
if (info.text !== null && info.text !== undefined && info.text !== '') {
2025-03-25 05:40:07 +08:00
msg.text += info.text;
}
2025-02-23 16:38:56 +08:00
if (info.reasoning_content !== null && info.reasoning_content !== undefined && info.reasoning_content !== '') {
msg.reasoning_content = info.reasoning_content;
}
// 只有在 refs 不为空时更新
if (info.refs !== null && info.refs !== undefined) {
2025-02-23 16:38:56 +08:00
msg.refs = info.refs;
}
if (info.model_name !== null && info.model_name !== undefined && info.model_name !== '') {
2025-02-23 16:38:56 +08:00
msg.model_name = info.model_name;
}
2025-02-23 16:38:56 +08:00
// 只有在 status 不为空时更新
if (info.status !== null && info.status !== undefined && info.status !== '') {
2025-02-23 16:38:56 +08:00
msg.status = info.status;
}
2024-10-23 16:24:29 +08:00
if (info.meta !== null && info.meta !== undefined) {
2025-02-23 16:38:56 +08:00
msg.meta = info.meta;
}
if (info.message !== null && info.message !== undefined) {
msg.message = info.message;
}
2025-02-23 17:40:09 +08:00
if (info.showThinking !== null && info.showThinking !== undefined) {
msg.showThinking = info.showThinking;
}
scrollToBottom();
} catch (error) {
console.error('Error updating message:', error);
2025-02-23 16:38:56 +08:00
msg.status = 'error';
msg.text = '消息更新失败';
2024-10-23 16:24:29 +08:00
}
2024-07-14 16:42:38 +08:00
} else {
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) => {
const { filename } = result.file;
if (!acc[filename]) {
acc[filename] = []
}
acc[filename].push(result)
return acc;
}, {})
}
2024-09-03 16:37:59 +08:00
scrollToBottom()
}
2025-02-23 16:38:56 +08:00
const simpleCall = (msg) => {
2024-07-14 16:42:38 +08:00
return new Promise((resolve, reject) => {
fetch('/api/chat/call_lite', {
2024-07-14 16:42:38 +08:00
method: 'POST',
2025-02-23 16:38:56 +08:00
body: JSON.stringify({ query: msg, }),
2024-07-29 01:00:02 +08:00
headers: { 'Content-Type': 'application/json' }
2024-07-14 16:42:38 +08:00
})
.then((response) => response.json())
.then((data) => resolve(data))
.catch((error) => reject(error))
})
}
2024-07-29 01:00:02 +08:00
const loadDatabases = () => {
2024-10-02 20:11:28 +08:00
fetch('/api/data/', { method: "GET", })
2024-07-29 01:00:02 +08:00
.then(response => response.json())
.then(data => {
console.log(data)
opts.databases = data.databases
})
}
2024-10-02 20:11:28 +08:00
// 新函数用于处理 fetch 请求
const fetchChatResponse = (user_input, cur_res_id) => {
2024-10-08 22:16:17 +08:00
fetch('/api/chat/', {
2024-10-02 20:11:28 +08:00
method: 'POST',
body: JSON.stringify({
query: user_input,
history: conv.value.history,
meta: meta,
cur_res_id: cur_res_id,
2025-03-25 05:40:07 +08:00
thread_id: conv.value.id.toString(),
2024-10-02 20:11:28 +08:00
}),
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (!response.body) throw new Error("ReadableStream not supported.");
2024-10-02 20:11:28 +08:00
const reader = response.body.getReader();
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)
groupRefs(cur_res_id);
2025-02-23 17:40:09 +08:00
updateMessage({showThinking: "no", id: cur_res_id});
2024-10-02 20:11:28 +08:00
isStreaming.value = false;
if (conv.value.messages.length === 2) { renameTitle(); }
return;
2024-10-02 20:11:28 +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,
text: data.response,
2025-02-23 16:38:56 +08:00
reasoning_content: data.reasoning_content,
status: data.status,
meta: data.meta,
2025-02-23 16:38:56 +08:00
...data,
});
// console.log("Last message", conv.value.messages[conv.value.messages.length - 1].text)
// console.log("Last message", conv.value.messages[conv.value.messages.length - 1].status)
if (data.history) {
conv.value.history = data.history;
}
} catch (e) {
console.error('JSON 解析错误:', e, line);
}
}
}
// 保留最后一个可能不完整的行
buffer = lines[lines.length - 1];
2024-10-02 20:11:28 +08:00
return readChunk(); // 继续读取
});
};
readChunk();
2024-10-02 20:11:28 +08:00
})
.catch((error) => {
console.error(error);
2024-10-23 16:24:29 +08:00
updateMessage({
id: cur_res_id,
status: "error",
});
2024-10-02 20:11:28 +08:00
isStreaming.value = false;
});
}
2024-10-02 20:11:28 +08:00
// 更新后的 sendMessage 函数
const sendMessage = () => {
const user_input = conv.value.inputText.trim();
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 = '';
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
}
}
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);
const pastMessage = conv.value.messages[index-1]
2024-10-23 16:24:29 +08:00
console.log("retryMessage", id, pastMessage)
conv.value.inputText = pastMessage.text
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);
// 从本地存储加载数据
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-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 }
);
2024-07-14 16:42:38 +08:00
</script>
<style lang="less" scoped>
.chat {
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;
overflow-x: hidden;
2025-03-28 03:17:09 +08:00
background: var(--main-light-6);
2024-07-14 16:42:38 +08:00
position: relative;
box-sizing: border-box;
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 {
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);
height: var(--header-height);
2024-07-17 18:52:20 +08:00
display: flex;
justify-content: space-between;
2024-07-17 18:52:20 +08:00
align-items: center;
padding: 1rem;
2024-07-14 16:42:38 +08:00
.header__left, .header__right {
display: flex;
align-items: center;
}
2025-03-25 05:40:07 +08:00
.header__left {
.close {
margin-right: 12px;
}
}
}
2024-07-14 16:42:38 +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);
cursor: pointer;
2024-07-14 16:42:38 +08:00
font-size: 1rem;
width: auto;
padding: 0.5rem 1rem;
2025-03-25 05:40:07 +08:00
transition: background-color 0.3s;
2024-07-14 16:42:38 +08:00
.text {
margin-left: 10px;
}
&:hover {
2024-07-26 14:13:05 +08:00
background-color: var(--main-light-3);
}
2024-07-14 16:42:38 +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
}
.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
.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
h1 {
margin-bottom: 20px;
2025-03-23 19:22:00 +08:00
font-size: 1.2rem;
color: #333;
}
2024-07-14 16:42:38 +08:00
2024-07-29 01:00:02 +08:00
.opts {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
2024-07-14 16:42:38 +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;
cursor: pointer;
2024-07-26 14:13:05 +08:00
// border: 2px solid var(--main-light-4);
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
&: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-14 16:42:38 +08:00
}
2024-07-14 16:42:38 +08:00
}
.chat-box {
width: 100%;
2025-02-27 13:26:08 +08:00
max-width: 800px;
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;
}
}
}
.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 {
width: 100%;
2025-02-27 13:26:08 +08:00
max-width: 800px;
margin: 0 auto;
2025-03-28 03:17:09 +08:00
background-color: white;
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;
margin: 4px 0;
user-select: none;
}
2024-09-18 21:03:12 +08:00
}
2024-07-14 16:42:38 +08:00
}
.ant-dropdown-link {
2024-09-25 13:46:51 +08:00
color: var(--gray-900);
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; } }
@media (max-width: 520px) {
.chat {
height: calc(100vh - 60px);
2024-07-14 16:42:38 +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);
.header__left, .header__right {
2025-02-27 01:37:42 +08:00
gap: 24px;
}
.nav-btn {
2025-02-27 01:37:42 +08:00
font-size: 1.3rem;
padding: 0;
&:hover {
background-color: transparent;
color: black;
}
.text {
display: none;
}
}
2024-07-14 16:42:38 +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
.controls {
display: flex;
align-items: center;
gap: 8px;
.search-switch {
margin-right: 8px;
}
}
2024-07-14 16:42:38 +08:00
</style>