|
@@ -1,18 +1,18 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, nextTick, onMounted, computed, onUnmounted, reactive, watch, isReactive } from 'vue'
|
|
|
-import { Local } from '/@/utils/storage'
|
|
|
-import { User, ChatDotRound, Delete, Edit, Check, Close, ArrowDown, Star } from '@element-plus/icons-vue'
|
|
|
-import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
|
|
|
+import {ref, nextTick, onMounted, computed, onUnmounted, reactive, watch, isReactive} from 'vue'
|
|
|
+import {Local} from '/@/utils/storage'
|
|
|
+import {User, ChatDotRound, Delete, Edit, Check, Close, ArrowDown, Star, StarFilled} from '@element-plus/icons-vue'
|
|
|
+import {MarkdownPlugin} from '/@/components/markdown/type/markdown'
|
|
|
import EChartsPlugin from '/@/components/markdown/plugins/echarts'
|
|
|
import ToolsLoadingPlugin from '/@/components/markdown/plugins/tools-loading'
|
|
|
import TablePlugin from '/@/components/markdown/plugins/table'
|
|
|
import Markdown from '/@/components/markdown/Markdown.vue'
|
|
|
import assist from '/@/api/assist'
|
|
|
-import { ChatResponse, LmConfigInfo, LmSession, Message } from '/@/api/assist/type'
|
|
|
-import { useLoading } from '/@/utils/loading-util'
|
|
|
-import { Setting as EleSetting } from '@element-plus/icons-vue'
|
|
|
-import { useRouter } from 'vue-router'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
+import {ChatResponse, LmConfigInfo, LmSession, Message} from '/@/api/assist/type'
|
|
|
+import {useLoading} from '/@/utils/loading-util'
|
|
|
+import {Setting as EleSetting} from '@element-plus/icons-vue'
|
|
|
+import {useRouter} from 'vue-router'
|
|
|
+import {ElMessage} from 'element-plus'
|
|
|
|
|
|
const plugins: Array<MarkdownPlugin<any>> = [EChartsPlugin(), ToolsLoadingPlugin(), TablePlugin()]
|
|
|
|
|
@@ -28,52 +28,52 @@ const messagesContainer = ref<HTMLElement>()
|
|
|
const selectedTool = ref([])
|
|
|
// 工具选择
|
|
|
const toolOptions = ref([
|
|
|
- {
|
|
|
- value: 'code',
|
|
|
- label: '代码工具',
|
|
|
- children: [
|
|
|
- {
|
|
|
- value: 'codebase-retrieval',
|
|
|
- label: '代码库检索',
|
|
|
- },
|
|
|
- {
|
|
|
- value: 'str-replace-editor',
|
|
|
- label: '代码编辑器',
|
|
|
- },
|
|
|
- {
|
|
|
- value: 'save-file',
|
|
|
- label: '文件保存',
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- {
|
|
|
- value: 'web',
|
|
|
- label: '网络工具',
|
|
|
- children: [
|
|
|
- {
|
|
|
- value: 'web-search',
|
|
|
- label: '网络搜索',
|
|
|
- },
|
|
|
- {
|
|
|
- value: 'web-fetch',
|
|
|
- label: '网页获取',
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- {
|
|
|
- value: 'system',
|
|
|
- label: '系统工具',
|
|
|
- children: [
|
|
|
- {
|
|
|
- value: 'execute-command',
|
|
|
- label: '命令执行',
|
|
|
- },
|
|
|
- {
|
|
|
- value: 'launch-process',
|
|
|
- label: '进程启动',
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
+ {
|
|
|
+ value: 'code',
|
|
|
+ label: '代码工具',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ value: 'codebase-retrieval',
|
|
|
+ label: '代码库检索',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'str-replace-editor',
|
|
|
+ label: '代码编辑器',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'save-file',
|
|
|
+ label: '文件保存',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'web',
|
|
|
+ label: '网络工具',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ value: 'web-search',
|
|
|
+ label: '网络搜索',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'web-fetch',
|
|
|
+ label: '网页获取',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'system',
|
|
|
+ label: '系统工具',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ value: 'execute-command',
|
|
|
+ label: '命令执行',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: 'launch-process',
|
|
|
+ label: '进程启动',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
])
|
|
|
|
|
|
const prompt = ref<string>('')
|
|
@@ -82,15 +82,15 @@ const openPromptDialog = ref(false)
|
|
|
// 模型选择
|
|
|
const modelOptions = ref<LmConfigInfo[]>([])
|
|
|
|
|
|
-const { loading: loadingModels, doLoading: loadModel } = useLoading(async () => {
|
|
|
- const data: { list: LmConfigInfo[]; total: number } = await assist.model.getList().catch(() => {
|
|
|
- return {
|
|
|
- list: [],
|
|
|
- }
|
|
|
- })
|
|
|
+const {loading: loadingModels, doLoading: loadModel} = useLoading(async () => {
|
|
|
+ const data: { list: LmConfigInfo[]; total: number } = await assist.model.getList().catch(() => {
|
|
|
+ return {
|
|
|
+ list: [],
|
|
|
+ }
|
|
|
+ })
|
|
|
|
|
|
- modelOptions.value = data.list
|
|
|
- selectedModel.value = data.list[0]?.id ?? undefined
|
|
|
+ modelOptions.value = data.list
|
|
|
+ selectedModel.value = data.list[0]?.id ?? undefined
|
|
|
})
|
|
|
|
|
|
onMounted(loadModel)
|
|
@@ -102,137 +102,137 @@ onUnmounted(() => chatInstance.value?.())
|
|
|
// 是否正在对话
|
|
|
const isConversationActive = computed(() => chatInstance.value !== undefined)
|
|
|
|
|
|
-const { loading: loadingClearMessage, doLoading: clearMessage } = useLoading(async () => {
|
|
|
- stopConversation()
|
|
|
- if (messages.value.length !== 0 && activeConversationId.value !== undefined) {
|
|
|
- const res = await assist.session.message
|
|
|
- .save({
|
|
|
- sessionId: activeConversationId.value,
|
|
|
- messages: [],
|
|
|
- })
|
|
|
- .then(() => true)
|
|
|
- .catch(() => false)
|
|
|
- if (!res) {
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- messages.value = []
|
|
|
+const {loading: loadingClearMessage, doLoading: clearMessage} = useLoading(async () => {
|
|
|
+ stopConversation()
|
|
|
+ if (messages.value.length !== 0 && activeConversationId.value !== undefined) {
|
|
|
+ const res = await assist.session.message
|
|
|
+ .save({
|
|
|
+ sessionId: activeConversationId.value,
|
|
|
+ messages: [],
|
|
|
+ })
|
|
|
+ .then(() => true)
|
|
|
+ .catch(() => false)
|
|
|
+ if (!res) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ messages.value = []
|
|
|
})
|
|
|
// 发送消息
|
|
|
const sendMessage = async () => {
|
|
|
- if (!inputMessage.value.trim()) return
|
|
|
-
|
|
|
- if (activeConversationId.value === undefined) {
|
|
|
- //未选中任何会话则创建新会话
|
|
|
- await createConversationAndSetItActive()
|
|
|
- }
|
|
|
- await nextTick()
|
|
|
- messages.value.push({
|
|
|
- id: messages.value.length,
|
|
|
- role: 'user',
|
|
|
- render_content: inputMessage.value,
|
|
|
- content: inputMessage.value,
|
|
|
- timestamp: Date.now(),
|
|
|
- })
|
|
|
-
|
|
|
- const rtn = reactive<Message>({
|
|
|
- id: messages.value.length,
|
|
|
- role: 'assistant',
|
|
|
- render_content: '',
|
|
|
- content: '',
|
|
|
- timestamp: Date.now(),
|
|
|
- tool_calls: [],
|
|
|
- })
|
|
|
-
|
|
|
- inputMessage.value = ''
|
|
|
- scrollToBottom()
|
|
|
- chatInternal(rtn)
|
|
|
- messages.value.push(rtn)
|
|
|
+ if (!inputMessage.value.trim()) return
|
|
|
+
|
|
|
+ if (activeConversationId.value === undefined) {
|
|
|
+ //未选中任何会话则创建新会话
|
|
|
+ await createConversationAndSetItActive()
|
|
|
+ }
|
|
|
+ await nextTick()
|
|
|
+ messages.value.push({
|
|
|
+ id: messages.value.length,
|
|
|
+ role: 'user',
|
|
|
+ render_content: inputMessage.value,
|
|
|
+ content: inputMessage.value,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ })
|
|
|
+
|
|
|
+ const rtn = reactive<Message>({
|
|
|
+ id: messages.value.length,
|
|
|
+ role: 'assistant',
|
|
|
+ render_content: '',
|
|
|
+ content: '',
|
|
|
+ timestamp: Date.now(),
|
|
|
+ tool_calls: [],
|
|
|
+ })
|
|
|
+
|
|
|
+ inputMessage.value = ''
|
|
|
+ scrollToBottom()
|
|
|
+ chatInternal(rtn)
|
|
|
+ messages.value.push(rtn)
|
|
|
}
|
|
|
|
|
|
const replaceMessage = (index: number) => {
|
|
|
- // 获取当前用户消息
|
|
|
- const userMessage = messages.value[index]
|
|
|
- if (!userMessage || userMessage.role !== 'user') {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 查找对应的AI回复消息(通常是下一条消息)
|
|
|
- let aiMessageIndex = -1
|
|
|
- for (let i = index + 1; i < messages.value.length; i++) {
|
|
|
- if (messages.value[i].role === 'assistant') {
|
|
|
- aiMessageIndex = i
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let rtn: Message
|
|
|
-
|
|
|
- if (aiMessageIndex !== -1) {
|
|
|
- // 找到了AI回复消息
|
|
|
- const aiMessage = messages.value[aiMessageIndex]
|
|
|
-
|
|
|
- if (isReactive(aiMessage)) {
|
|
|
- // 如果是reactive对象,清空内容
|
|
|
- aiMessage.render_content = ''
|
|
|
- aiMessage.content = ''
|
|
|
- aiMessage.tool_calls = []
|
|
|
- rtn = aiMessage
|
|
|
- } else {
|
|
|
- // 如果不是reactive对象,创建新的reactive对象替换
|
|
|
- rtn = reactive<Message>({
|
|
|
- id: aiMessage.id,
|
|
|
- role: 'assistant',
|
|
|
- render_content: '',
|
|
|
- content: '',
|
|
|
- timestamp: aiMessage.timestamp,
|
|
|
- tool_calls: [],
|
|
|
- })
|
|
|
- messages.value[aiMessageIndex] = rtn
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 没有找到AI回复消息,创建新的
|
|
|
- rtn = reactive<Message>({
|
|
|
- id: messages.value.length,
|
|
|
- role: 'assistant',
|
|
|
- render_content: '',
|
|
|
- content: '',
|
|
|
- timestamp: Date.now(),
|
|
|
- tool_calls: [],
|
|
|
- })
|
|
|
- messages.value.push(rtn)
|
|
|
- }
|
|
|
-
|
|
|
- // 重新发起对话
|
|
|
- chatInternal(rtn, messages.value.slice(0, aiMessageIndex))
|
|
|
+ // 获取当前用户消息
|
|
|
+ const userMessage = messages.value[index]
|
|
|
+ if (!userMessage || userMessage.role !== 'user') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找对应的AI回复消息(通常是下一条消息)
|
|
|
+ let aiMessageIndex = -1
|
|
|
+ for (let i = index + 1; i < messages.value.length; i++) {
|
|
|
+ if (messages.value[i].role === 'assistant') {
|
|
|
+ aiMessageIndex = i
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let rtn: Message
|
|
|
+
|
|
|
+ if (aiMessageIndex !== -1) {
|
|
|
+ // 找到了AI回复消息
|
|
|
+ const aiMessage = messages.value[aiMessageIndex]
|
|
|
+
|
|
|
+ if (isReactive(aiMessage)) {
|
|
|
+ // 如果是reactive对象,清空内容
|
|
|
+ aiMessage.render_content = ''
|
|
|
+ aiMessage.content = ''
|
|
|
+ aiMessage.tool_calls = []
|
|
|
+ rtn = aiMessage
|
|
|
+ } else {
|
|
|
+ // 如果不是reactive对象,创建新的reactive对象替换
|
|
|
+ rtn = reactive<Message>({
|
|
|
+ id: aiMessage.id,
|
|
|
+ role: 'assistant',
|
|
|
+ render_content: '',
|
|
|
+ content: '',
|
|
|
+ timestamp: aiMessage.timestamp,
|
|
|
+ tool_calls: [],
|
|
|
+ })
|
|
|
+ messages.value[aiMessageIndex] = rtn
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 没有找到AI回复消息,创建新的
|
|
|
+ rtn = reactive<Message>({
|
|
|
+ id: messages.value.length,
|
|
|
+ role: 'assistant',
|
|
|
+ render_content: '',
|
|
|
+ content: '',
|
|
|
+ timestamp: Date.now(),
|
|
|
+ tool_calls: [],
|
|
|
+ })
|
|
|
+ messages.value.push(rtn)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新发起对话
|
|
|
+ chatInternal(rtn, messages.value.slice(0, aiMessageIndex))
|
|
|
}
|
|
|
|
|
|
const chatInternal = (rtn: Message, context: Message[] = messages.value) => {
|
|
|
- chatInstance.value = assist.chat({
|
|
|
- chatRequest: {
|
|
|
- message: prompt.value
|
|
|
- ? [
|
|
|
- {
|
|
|
- id: messages.value.length,
|
|
|
- role: 'system',
|
|
|
- render_content: prompt.value,
|
|
|
- content: prompt.value,
|
|
|
- timestamp: Date.now(),
|
|
|
- },
|
|
|
- ...context,
|
|
|
- ]
|
|
|
- : context,
|
|
|
- modelClassId: selectedModel.value,
|
|
|
- },
|
|
|
- onReceive: (resp: ChatResponse) => {
|
|
|
- switch (resp.type) {
|
|
|
- case 'message':
|
|
|
- rtn.render_content += resp.message
|
|
|
- rtn.content += resp.message
|
|
|
- break
|
|
|
- case 'toolres': {
|
|
|
- if (showToolCalls.value) {
|
|
|
- rtn.render_content += `
|
|
|
+ chatInstance.value = assist.chat({
|
|
|
+ chatRequest: {
|
|
|
+ message: prompt.value
|
|
|
+ ? [
|
|
|
+ {
|
|
|
+ id: messages.value.length,
|
|
|
+ role: 'system',
|
|
|
+ render_content: prompt.value,
|
|
|
+ content: prompt.value,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ },
|
|
|
+ ...context,
|
|
|
+ ]
|
|
|
+ : context,
|
|
|
+ modelClassId: selectedModel.value,
|
|
|
+ },
|
|
|
+ onReceive: (resp: ChatResponse) => {
|
|
|
+ switch (resp.type) {
|
|
|
+ case 'message':
|
|
|
+ rtn.render_content += resp.message
|
|
|
+ rtn.content += resp.message
|
|
|
+ break
|
|
|
+ case 'toolres': {
|
|
|
+ if (showToolCalls.value) {
|
|
|
+ rtn.render_content += `
|
|
|
\`\`\`tools-loading
|
|
|
resp
|
|
|
${resp.response.name}
|
|
@@ -240,24 +240,24 @@ ${resp.response.data.replace('\n', '')}
|
|
|
\`\`\`
|
|
|
|
|
|
`
|
|
|
- }
|
|
|
-
|
|
|
- messages.value.push({
|
|
|
- id: messages.value.length,
|
|
|
- tool_call_id: resp.response.id,
|
|
|
- role: 'tool',
|
|
|
- render_content: '',
|
|
|
- name: resp.response.name,
|
|
|
- content: resp.response.data,
|
|
|
- timestamp: Date.now(),
|
|
|
- tool_calls: [],
|
|
|
- })
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- case 'toolcall': {
|
|
|
- if (showToolCalls.value) {
|
|
|
- rtn.render_content += `
|
|
|
+ }
|
|
|
+
|
|
|
+ messages.value.push({
|
|
|
+ id: messages.value.length,
|
|
|
+ tool_call_id: resp.response.id,
|
|
|
+ role: 'tool',
|
|
|
+ render_content: '',
|
|
|
+ name: resp.response.name,
|
|
|
+ content: resp.response.data,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ tool_calls: [],
|
|
|
+ })
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ case 'toolcall': {
|
|
|
+ if (showToolCalls.value) {
|
|
|
+ rtn.render_content += `
|
|
|
\`\`\`tools-loading
|
|
|
request
|
|
|
${resp.request.name}
|
|
@@ -265,79 +265,85 @@ ${resp.request.data.replace('\n', '')}
|
|
|
\`\`\`
|
|
|
|
|
|
`
|
|
|
- }
|
|
|
-
|
|
|
- rtn.tool_calls?.push({
|
|
|
- id: resp.request.id,
|
|
|
- type: 'function',
|
|
|
- function: {
|
|
|
- name: resp.request.name,
|
|
|
- arguments: resp.request.data,
|
|
|
- },
|
|
|
- })
|
|
|
- break
|
|
|
- }
|
|
|
- case 'error':
|
|
|
- rtn.render_content += `
|
|
|
+ }
|
|
|
+
|
|
|
+ rtn.tool_calls?.push({
|
|
|
+ id: resp.request.id,
|
|
|
+ type: 'function',
|
|
|
+ function: {
|
|
|
+ name: resp.request.name,
|
|
|
+ arguments: resp.request.data,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ break
|
|
|
+ }
|
|
|
+ case 'error':
|
|
|
+ rtn.render_content += `
|
|
|
> ### 系统报错
|
|
|
>
|
|
|
>
|
|
|
> ${resp.error.split('\n').join('\n> ')}
|
|
|
|
|
|
`
|
|
|
- break
|
|
|
- }
|
|
|
- },
|
|
|
- onComplete: async (e) => {
|
|
|
- if (e !== undefined) {
|
|
|
- rtn.content += `
|
|
|
+ break
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onComplete: async (e) => {
|
|
|
+ if (e !== undefined) {
|
|
|
+ rtn.content += `
|
|
|
|
|
|
`
|
|
|
- }
|
|
|
- rtn.render_content += '\n'
|
|
|
+ }
|
|
|
+ rtn.render_content += '\n'
|
|
|
|
|
|
- await assist.session.message.save({
|
|
|
- sessionId: activeConversationId.value!,
|
|
|
- messages: messages.value,
|
|
|
- })
|
|
|
+ await assist.session.message.save({
|
|
|
+ sessionId: activeConversationId.value!,
|
|
|
+ messages: messages.value,
|
|
|
+ })
|
|
|
|
|
|
- chatInstance.value = undefined
|
|
|
- },
|
|
|
- })
|
|
|
+ chatInstance.value = undefined
|
|
|
+ },
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
// 终止对话
|
|
|
const stopConversation = () => {
|
|
|
- chatInstance.value?.()
|
|
|
- chatInstance.value = undefined
|
|
|
+ chatInstance.value?.()
|
|
|
+ chatInstance.value = undefined
|
|
|
}
|
|
|
|
|
|
// 滚动到底部
|
|
|
const scrollToBottom = () => {
|
|
|
- nextTick(() => {
|
|
|
- if (messagesContainer.value) {
|
|
|
- messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
- }
|
|
|
- })
|
|
|
+ nextTick(() => {
|
|
|
+ if (messagesContainer.value) {
|
|
|
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
// 会话管理模块
|
|
|
// 所有会话
|
|
|
const conversations = ref<LmSession[]>([])
|
|
|
-const { loading: loadConversations, doLoading: doLoadConversations } = useLoading(async () => {
|
|
|
- const data: { list: LmSession[]; total: number } = await assist.session
|
|
|
- .list({
|
|
|
- pageNum: 1,
|
|
|
- pageSize: 30,
|
|
|
- })
|
|
|
- .catch(() => {
|
|
|
- return {
|
|
|
- list: [],
|
|
|
- total: 0,
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- conversations.value = data.list
|
|
|
+const {loading: loadConversations, doLoading: doLoadConversations} = useLoading(async () => {
|
|
|
+ const data: { list: LmSession[]; total: number } = await assist.session
|
|
|
+ .list({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 30,
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ return {
|
|
|
+ list: [],
|
|
|
+ total: 0,
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ conversations.value = [
|
|
|
+ {
|
|
|
+ session_id: -1,
|
|
|
+ title: '收藏对话'
|
|
|
+ },
|
|
|
+ ...data.list
|
|
|
+ ]
|
|
|
})
|
|
|
|
|
|
onMounted(doLoadConversations)
|
|
@@ -352,63 +358,79 @@ const activeConversationId = ref<number | undefined>(undefined)
|
|
|
// await doLoadingMessage(newVal)
|
|
|
// })
|
|
|
|
|
|
-const { loading: loadingMessage, doLoading: doLoadingMessage } = useLoading(async (id: number) => {
|
|
|
- const data: {
|
|
|
- messages: Message[]
|
|
|
- total: number
|
|
|
- } = await assist.session.message.list({ sessionId: id }).catch(() => {
|
|
|
- return {
|
|
|
- list: [],
|
|
|
- total: 0,
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- messages.value = data.messages ?? []
|
|
|
+const {loading: loadingMessage, doLoading: doLoadingMessage} = useLoading(async (id: number) => {
|
|
|
+ //如果id是-1,拉取收藏的记录
|
|
|
+ if (id === -1) {
|
|
|
+ const data: {
|
|
|
+ messages: Message[]
|
|
|
+ total: number
|
|
|
+ } = await assist.session.message.bookmark_list().catch(() => {
|
|
|
+ return {
|
|
|
+ list: [],
|
|
|
+ total: 0,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ messages.value = data.messages ?? []
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ const data: {
|
|
|
+ messages: Message[]
|
|
|
+ total: number
|
|
|
+ } = await assist.session.message.list({sessionId: id}).catch(() => {
|
|
|
+ return {
|
|
|
+ list: [],
|
|
|
+ total: 0,
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ messages.value = data.messages ?? []
|
|
|
})
|
|
|
|
|
|
// 选择会话
|
|
|
const selectConversation = async (id: number) => {
|
|
|
- if (activeConversationId.value === id) {
|
|
|
- return
|
|
|
- }
|
|
|
- activeConversationId.value = id
|
|
|
- await doLoadingMessage(id)
|
|
|
+ if (activeConversationId.value === id) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ activeConversationId.value = id
|
|
|
+ await doLoadingMessage(id)
|
|
|
}
|
|
|
|
|
|
// 删除会话
|
|
|
const currentDeletingConversation = ref(-1)
|
|
|
-const { loading: loadingDeleteConversation, doLoading: deleteConversation } = useLoading(async (id: number) => {
|
|
|
- currentDeletingConversation.value = id
|
|
|
- const res = await assist.session
|
|
|
- .del([id])
|
|
|
- .then(() => true)
|
|
|
- .catch(() => false)
|
|
|
- .finally(() => (currentDeletingConversation.value = -1))
|
|
|
- if (!res) {
|
|
|
- return
|
|
|
- }
|
|
|
- ElMessage.success('删除成功')
|
|
|
- activeConversationId.value = undefined
|
|
|
- messages.value = []
|
|
|
- await nextTick()
|
|
|
- await doLoadConversations()
|
|
|
+const {loading: loadingDeleteConversation, doLoading: deleteConversation} = useLoading(async (id: number) => {
|
|
|
+ currentDeletingConversation.value = id
|
|
|
+ const res = await assist.session
|
|
|
+ .del([id])
|
|
|
+ .then(() => true)
|
|
|
+ .catch(() => false)
|
|
|
+ .finally(() => (currentDeletingConversation.value = -1))
|
|
|
+ if (!res) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ activeConversationId.value = undefined
|
|
|
+ messages.value = []
|
|
|
+ await nextTick()
|
|
|
+ await doLoadConversations()
|
|
|
})
|
|
|
|
|
|
// 创建新对话
|
|
|
-const { loading: creatingConversation, doLoading: createConversationAndSetItActive } = useLoading(async () => {
|
|
|
- try {
|
|
|
- // 调用API创建新对话,默认标题为"新对话"
|
|
|
- const { id } = await assist.session.add('新对话')
|
|
|
- // 刷新对话列表
|
|
|
- await doLoadConversations()
|
|
|
-
|
|
|
- await nextTick()
|
|
|
- activeConversationId.value = id
|
|
|
- } catch (error) {
|
|
|
- console.error('创建对话失败:', error)
|
|
|
- // 可以在这里添加错误提示
|
|
|
- throw error
|
|
|
- }
|
|
|
+const {loading: creatingConversation, doLoading: createConversationAndSetItActive} = useLoading(async () => {
|
|
|
+ try {
|
|
|
+ // 调用API创建新对话,默认标题为"新对话"
|
|
|
+ const {id} = await assist.session.add('新对话')
|
|
|
+ // 刷新对话列表
|
|
|
+ await doLoadConversations()
|
|
|
+
|
|
|
+ await nextTick()
|
|
|
+ activeConversationId.value = id
|
|
|
+ } catch (error) {
|
|
|
+ console.error('创建对话失败:', error)
|
|
|
+ // 可以在这里添加错误提示
|
|
|
+ throw error
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
// 编辑会话状态管理
|
|
@@ -416,58 +438,58 @@ const editingConversationId = ref<number | undefined>(undefined)
|
|
|
const editingTitle = ref('')
|
|
|
// 编辑会话摘要
|
|
|
const editSummary = (id: number) => {
|
|
|
- const conversation = conversations.value.find((conv) => conv.session_id === id)
|
|
|
- if (conversation) {
|
|
|
- // 设置当前编辑的会话ID
|
|
|
- editingConversationId.value = id
|
|
|
- editingTitle.value = conversation.title
|
|
|
- // 下一帧聚焦到输入框
|
|
|
- nextTick(() => {
|
|
|
- const editInput = document.querySelector('.edit-input .el-input__inner') as HTMLInputElement
|
|
|
- if (editInput) {
|
|
|
- editInput.focus()
|
|
|
- editInput.select()
|
|
|
- }
|
|
|
- })
|
|
|
- }
|
|
|
+ const conversation = conversations.value.find((conv) => conv.session_id === id)
|
|
|
+ if (conversation) {
|
|
|
+ // 设置当前编辑的会话ID
|
|
|
+ editingConversationId.value = id
|
|
|
+ editingTitle.value = conversation.title
|
|
|
+ // 下一帧聚焦到输入框
|
|
|
+ nextTick(() => {
|
|
|
+ const editInput = document.querySelector('.edit-input .el-input__inner') as HTMLInputElement
|
|
|
+ if (editInput) {
|
|
|
+ editInput.focus()
|
|
|
+ editInput.select()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
// 确认编辑
|
|
|
-const { doLoading: confirmEdit, loading: loadingConfirmEdit } = useLoading(async (id: number) => {
|
|
|
- const conversation = conversations.value.find((conv) => conv.session_id === id)
|
|
|
- if (conversation && editingTitle.value.trim()) {
|
|
|
- const edit = await assist.session
|
|
|
- .edit(id, editingTitle.value.trim())
|
|
|
- .then(() => true)
|
|
|
- .catch(() => false)
|
|
|
- if (!edit) {
|
|
|
- return
|
|
|
- }
|
|
|
- conversation.title = editingTitle.value.trim()
|
|
|
- // 清除编辑状态
|
|
|
- editingConversationId.value = undefined
|
|
|
- editingTitle.value = ''
|
|
|
- }
|
|
|
+const {doLoading: confirmEdit, loading: loadingConfirmEdit} = useLoading(async (id: number) => {
|
|
|
+ const conversation = conversations.value.find((conv) => conv.session_id === id)
|
|
|
+ if (conversation && editingTitle.value.trim()) {
|
|
|
+ const edit = await assist.session
|
|
|
+ .edit(id, editingTitle.value.trim())
|
|
|
+ .then(() => true)
|
|
|
+ .catch(() => false)
|
|
|
+ if (!edit) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ conversation.title = editingTitle.value.trim()
|
|
|
+ // 清除编辑状态
|
|
|
+ editingConversationId.value = undefined
|
|
|
+ editingTitle.value = ''
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
// 取消编辑
|
|
|
const cancelEdit = () => {
|
|
|
- // 清除编辑状态
|
|
|
- editingConversationId.value = undefined
|
|
|
- editingTitle.value = ''
|
|
|
+ // 清除编辑状态
|
|
|
+ editingConversationId.value = undefined
|
|
|
+ editingTitle.value = ''
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
- scrollToBottom()
|
|
|
+ scrollToBottom()
|
|
|
})
|
|
|
|
|
|
//杂项
|
|
|
const getUserInfos = ref<{
|
|
|
- avatar: string
|
|
|
- userName: string
|
|
|
+ avatar: string
|
|
|
+ userName: string
|
|
|
}>(Local.get('userInfo') || {})
|
|
|
|
|
|
const canSendMessage = computed(() => {
|
|
|
- return !inputMessage.value.trim() || loadingModels.value || loadConversations.value || loadingMessage.value
|
|
|
+ return !inputMessage.value.trim() || loadingModels.value || loadConversations.value || loadingMessage.value
|
|
|
})
|
|
|
|
|
|
const router = useRouter()
|
|
@@ -482,1040 +504,1040 @@ const activeTab = ref('history')
|
|
|
|
|
|
// 收藏消息功能
|
|
|
const favoriteMessageIdx = ref(-1)
|
|
|
-const { loading: loadingFavoriteMessage, doLoading: toggleFavorite } = useLoading(async (messageIndex: number) => {
|
|
|
- favoriteMessageIdx.value = messageIndex
|
|
|
- const data = messages.value[messageIndex]
|
|
|
- const active = activeConversationId.value
|
|
|
- if (active === undefined) {
|
|
|
- return
|
|
|
- }
|
|
|
- const success = assist.session.message
|
|
|
- .bookmark(active, data.id, !(data.like ?? false))
|
|
|
- .then(() => true)
|
|
|
- .catch(() => false)
|
|
|
- .finally(() => (favoriteMessageIdx.value = -1))
|
|
|
- if (!success) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- messages.value[messageIndex] = {
|
|
|
- ...data,
|
|
|
- like: !(data.like ?? false),
|
|
|
- }
|
|
|
+const {loading: loadingFavoriteMessage, doLoading: toggleFavorite} = useLoading(async (messageIndex: number) => {
|
|
|
+ favoriteMessageIdx.value = messageIndex
|
|
|
+ const data = messages.value[messageIndex]
|
|
|
+ //@ts-ignore
|
|
|
+ const active = activeConversationId.value === -1 ? data["session_id"] : (activeConversationId.value ?? undefined)
|
|
|
+ if (active === undefined) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const success = assist.session.message
|
|
|
+ .bookmark(active, data.id, !(data.like ?? false))
|
|
|
+ .then(() => true)
|
|
|
+ .catch(() => false)
|
|
|
+ .finally(() => (favoriteMessageIdx.value = -1))
|
|
|
+ if (!success) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ messages.value[messageIndex] = {
|
|
|
+ ...data,
|
|
|
+ like: !(data.like ?? false),
|
|
|
+ }
|
|
|
})
|
|
|
+
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <el-container class="chat-container">
|
|
|
- <!-- 左侧会话列表 -->
|
|
|
- <el-aside width="300px" class="chat-sidebar">
|
|
|
- <div class="sidebar-header">
|
|
|
- <h3>对话历史</h3>
|
|
|
- <el-dropdown v-model="showSettingsPanel" trigger="click" placement="bottom-end">
|
|
|
- <el-button round :icon="EleSetting" size="small">
|
|
|
- <el-icon class="el-icon--right">
|
|
|
- <ArrowDown />
|
|
|
- </el-icon>
|
|
|
- </el-button>
|
|
|
- <template #dropdown>
|
|
|
- <el-dropdown-menu>
|
|
|
- <el-dropdown-item class="settings-item">
|
|
|
- <div class="settings-row">
|
|
|
- <span class="settings-label">显示工具调用</span>
|
|
|
- <el-switch v-model="showToolCalls" size="small" />
|
|
|
- </div>
|
|
|
- </el-dropdown-item>
|
|
|
- <el-dropdown-item @click="redirectToModelManager">
|
|
|
- <span class="settings-label">模型管理</span>
|
|
|
- </el-dropdown-item>
|
|
|
- </el-dropdown-menu>
|
|
|
- </template>
|
|
|
- </el-dropdown>
|
|
|
- </div>
|
|
|
- <el-scrollbar class="conversation-list" v-loading="loadConversations">
|
|
|
- <div
|
|
|
- v-for="conv in conversations"
|
|
|
- :key="conv.session_id"
|
|
|
- @click="editingConversationId !== conv.session_id ? selectConversation(conv.session_id) : () => {}"
|
|
|
- :class="['conversation-item', { active: activeConversationId === conv.session_id, editing: editingConversationId === conv.session_id }]"
|
|
|
- >
|
|
|
- <!-- 非编辑状态 -->
|
|
|
- <div v-if="editingConversationId !== conv.session_id" class="conversation-content">
|
|
|
- <span class="conversation-title">{{ conv.title }}</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 编辑状态 -->
|
|
|
- <div v-else class="conversation-edit-content">
|
|
|
- <el-input
|
|
|
- v-model="editingTitle"
|
|
|
- size="small"
|
|
|
- @keydown.enter="confirmEdit(conv.session_id)"
|
|
|
- @keydown.esc="cancelEdit"
|
|
|
- class="edit-input"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 操作按钮 -->
|
|
|
- <div class="conversation-actions">
|
|
|
- <!-- 非编辑状态的按钮 -->
|
|
|
- <template v-if="editingConversationId !== conv.session_id">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- size="small"
|
|
|
- :icon="Edit"
|
|
|
- @click.stop="editSummary(conv.session_id)"
|
|
|
- class="action-btn edit-btn"
|
|
|
- title="编辑摘要"
|
|
|
- plain
|
|
|
- circle
|
|
|
- />
|
|
|
- <el-button
|
|
|
- type="danger"
|
|
|
- size="small"
|
|
|
- :icon="Delete"
|
|
|
- :loading="loadingDeleteConversation && currentDeletingConversation == conv.session_id"
|
|
|
- @click.stop="deleteConversation(conv.session_id)"
|
|
|
- class="action-btn delete-btn"
|
|
|
- title="删除对话"
|
|
|
- plain
|
|
|
- circle
|
|
|
- :disabled="isConversationActive"
|
|
|
- />
|
|
|
- </template>
|
|
|
-
|
|
|
- <!-- 编辑状态的按钮 -->
|
|
|
- <template v-else>
|
|
|
- <el-button
|
|
|
- type="success"
|
|
|
- size="small"
|
|
|
- :icon="Check"
|
|
|
- :loading="loadingConfirmEdit"
|
|
|
- @click.stop="confirmEdit(conv.session_id)"
|
|
|
- class="action-btn confirm-btn"
|
|
|
- title="确认修改"
|
|
|
- plain
|
|
|
- circle
|
|
|
- />
|
|
|
- <el-button
|
|
|
- type="info"
|
|
|
- size="small"
|
|
|
- @click.stop="cancelEdit(conv.session_id)"
|
|
|
- class="action-btn cancel-btn"
|
|
|
- title="取消编辑"
|
|
|
- plain
|
|
|
- circle
|
|
|
- >
|
|
|
- <el-icon>
|
|
|
- <Close />
|
|
|
- </el-icon>
|
|
|
- </el-button>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-scrollbar>
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- size="large"
|
|
|
- class="create-conversation-btn"
|
|
|
- @click="createConversationAndSetItActive"
|
|
|
- :loading="creatingConversation"
|
|
|
- :disabled="isConversationActive"
|
|
|
- >创建对话
|
|
|
- </el-button>
|
|
|
- </el-aside>
|
|
|
-
|
|
|
- <!-- 右侧聊天区域 -->
|
|
|
- <el-main class="chat-main">
|
|
|
- <!-- 消息展示区域 -->
|
|
|
- <div class="messages-container" ref="messagesContainer" v-loading="loadingMessage">
|
|
|
- <div v-if="messages.length !== 0">
|
|
|
- <div v-for="(message, idx) in messages" :key="message.id" :class="['message-wrapper', message.role]">
|
|
|
- <!-- AI消息 -->
|
|
|
- <div v-if="message.role === 'assistant'" class="ai-message-container">
|
|
|
- <el-avatar class="message-avatar" :icon="ChatDotRound" />
|
|
|
- <div class="ai-message-content">
|
|
|
- <div class="message-bubble ai-bubble">
|
|
|
- <Markdown v-if="message.render_content !== ''" :content="message.render_content" :plugins="plugins" class="markdown-content" />
|
|
|
- <div v-else class="loading-container">
|
|
|
- <div class="loading-dots">
|
|
|
- <span class="dot"></span>
|
|
|
- <span class="dot"></span>
|
|
|
- <span class="dot"></span>
|
|
|
- </div>
|
|
|
- <span class="loading-text">AI正在思考中...</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="ai-message-actions">
|
|
|
- <el-button
|
|
|
- :loading="favoriteMessageIdx == idx && loadingFavoriteMessage"
|
|
|
- @click="toggleFavorite(idx)"
|
|
|
- class="favorite-btn"
|
|
|
- :class="{ 'favorited': message.like ?? false }"
|
|
|
- :disabled="isConversationActive"
|
|
|
- text
|
|
|
- circle
|
|
|
- >
|
|
|
- <el-icon>
|
|
|
- <svg viewBox="0 0 1024 1024" width="16" height="16">
|
|
|
- <path
|
|
|
- :fill="(message.like ?? false) ? '#ff4757' : 'currentColor'"
|
|
|
- d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3.1-35.3-7-69.6-20.9-101.9z"
|
|
|
- />
|
|
|
- </svg>
|
|
|
- </el-icon>
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 用户消息 -->
|
|
|
- <div v-if="message.role === 'user'" class="user-message-container">
|
|
|
- <div class="user-message-content">
|
|
|
- <div class="message-bubble user-bubble">
|
|
|
- {{ message.render_content }}
|
|
|
- </div>
|
|
|
- <div class="user-message-actions">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- size="small"
|
|
|
- @click="replaceMessage(messages.indexOf(message))"
|
|
|
- class="retry-btn"
|
|
|
- plain
|
|
|
- :disabled="isConversationActive"
|
|
|
- >
|
|
|
- 重试
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <el-avatar class="message-avatar" :src="getUserInfos.avatar" :icon="User" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 空状态页面 -->
|
|
|
-
|
|
|
- <div v-else class="empty-content">
|
|
|
- <!-- 主图标 -->
|
|
|
- <div class="empty-icon">
|
|
|
- <el-icon :size="80" color="#d1d5db">
|
|
|
- <ChatDotRound />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 标题和描述 -->
|
|
|
- <div class="empty-text">
|
|
|
- <h2 class="empty-title">开始新的对话</h2>
|
|
|
- </div>
|
|
|
- <!-- 示例问题 -->
|
|
|
- <div class="example-questions">
|
|
|
- <h4>试试这些问题:</h4>
|
|
|
- <div class="question-tags">
|
|
|
- <el-tag class="question-tag" @click="inputMessage = '帮我查看设备运行状态和告警信息'" type="info">
|
|
|
- 帮我查看设备运行状态和告警信息
|
|
|
- </el-tag>
|
|
|
- <el-tag class="question-tag" @click="inputMessage = '分析用户权限配置和角色分配情况'" type="success">
|
|
|
- 分析用户权限配置和角色分配情况
|
|
|
- </el-tag>
|
|
|
- <el-tag class="question-tag" @click="inputMessage = '检查系统性能和在线用户统计'" type="warning"> 检查系统性能和在线用户统计 </el-tag>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="messages-spacer"></div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="input-container">
|
|
|
- <!-- 工具和模型选择行 -->
|
|
|
- <div class="selection-row">
|
|
|
- <!-- 工具选择 -->
|
|
|
- <div class="tool-selector">
|
|
|
- <el-cascader
|
|
|
- v-model="selectedTool"
|
|
|
- :options="toolOptions"
|
|
|
- :show-all-levels="false"
|
|
|
- :props="{ multiple: true }"
|
|
|
- collapse-tags
|
|
|
- collapse-tags-tooltip
|
|
|
- clearable
|
|
|
- size="small"
|
|
|
- style="width: 200px"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 模型选择 -->
|
|
|
- <div class="model-selector">
|
|
|
- <el-select :loading="loadingModels" v-model="selectedModel" placeholder="选择模型" size="small" style="width: 200px">
|
|
|
- <el-option v-for="item in modelOptions" :key="item.id" :value="item.id" :label="item.modelName" />
|
|
|
- </el-select>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 提示词输入框 -->
|
|
|
- <div class="prompt-selector">
|
|
|
- <el-input v-model="prompt" placeholder="点击设置提示词..." size="small" style="width: 200px" readonly @click="openPromptDialog = true" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 输入框和按钮行 -->
|
|
|
- <div class="input-row">
|
|
|
- <!-- 输入框 -->
|
|
|
- <div class="message-input-wrapper">
|
|
|
- <el-input
|
|
|
- v-model="inputMessage"
|
|
|
- type="textarea"
|
|
|
- placeholder="请输入您的问题..."
|
|
|
- :rows="2"
|
|
|
- resize="none"
|
|
|
- @keydown.enter.ctrl="sendMessage"
|
|
|
- @keydown.enter.meta="sendMessage"
|
|
|
- :disabled="isConversationActive"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 按钮组 -->
|
|
|
- <div class="button-group">
|
|
|
- <el-button
|
|
|
- v-show="messages.length !== 0"
|
|
|
- type="warning"
|
|
|
- size="small"
|
|
|
- @click="clearMessage"
|
|
|
- style="margin-left: 12px"
|
|
|
- :loading="loadingClearMessage"
|
|
|
- >
|
|
|
- 清空
|
|
|
- </el-button>
|
|
|
- <el-button
|
|
|
- v-if="!isConversationActive"
|
|
|
- type="primary"
|
|
|
- size="small"
|
|
|
- @click="sendMessage"
|
|
|
- @keyup.ctrl.enter="sendMessage"
|
|
|
- :disabled="canSendMessage"
|
|
|
- >
|
|
|
- 发送
|
|
|
- </el-button>
|
|
|
- <el-button v-else type="danger" size="small" @click="stopConversation"> 终止</el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-main>
|
|
|
-
|
|
|
- <!-- 提示词设置对话框 -->
|
|
|
- <el-dialog
|
|
|
- v-model="openPromptDialog"
|
|
|
- title="设置提示词"
|
|
|
- width="600px"
|
|
|
- :before-close="
|
|
|
+ <el-container class="chat-container">
|
|
|
+ <!-- 左侧会话列表 -->
|
|
|
+ <el-aside width="300px" class="chat-sidebar">
|
|
|
+ <div class="sidebar-header">
|
|
|
+ <h3>对话历史</h3>
|
|
|
+ <el-dropdown v-model="showSettingsPanel" trigger="click" placement="bottom-end">
|
|
|
+ <el-button round :icon="EleSetting" size="small">
|
|
|
+ <el-icon class="el-icon--right">
|
|
|
+ <ArrowDown/>
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item class="settings-item">
|
|
|
+ <div class="settings-row">
|
|
|
+ <span class="settings-label">显示工具调用</span>
|
|
|
+ <el-switch v-model="showToolCalls" size="small"/>
|
|
|
+ </div>
|
|
|
+ </el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="redirectToModelManager">
|
|
|
+ <span class="settings-label">模型管理</span>
|
|
|
+ </el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </div>
|
|
|
+ <el-scrollbar class="conversation-list" v-loading="loadConversations">
|
|
|
+ <div
|
|
|
+ v-for="conv in conversations"
|
|
|
+ :key="conv.session_id"
|
|
|
+ @click="editingConversationId !== conv.session_id ? selectConversation(conv.session_id) : () => {}"
|
|
|
+ :class="['conversation-item', { active: activeConversationId === conv.session_id, editing: editingConversationId === conv.session_id }]"
|
|
|
+ >
|
|
|
+ <!-- 非编辑状态 -->
|
|
|
+ <div v-if="editingConversationId !== conv.session_id" class="conversation-content">
|
|
|
+ <span class="conversation-title">{{ conv.title }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 编辑状态 -->
|
|
|
+ <div v-else class="conversation-edit-content">
|
|
|
+ <el-input
|
|
|
+ v-model="editingTitle"
|
|
|
+ size="small"
|
|
|
+ @keydown.enter="confirmEdit(conv.session_id)"
|
|
|
+ @keydown.esc="cancelEdit"
|
|
|
+ class="edit-input"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <div class="conversation-actions" v-if="conv.session_id !== -1">
|
|
|
+ <!-- 非编辑状态的按钮 -->
|
|
|
+ <template v-if="editingConversationId !== conv.session_id">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :icon="Edit"
|
|
|
+ @click.stop="editSummary(conv.session_id)"
|
|
|
+ class="action-btn edit-btn"
|
|
|
+ title="编辑摘要"
|
|
|
+ plain
|
|
|
+ circle
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ size="small"
|
|
|
+ :icon="Delete"
|
|
|
+ :loading="loadingDeleteConversation && currentDeletingConversation == conv.session_id"
|
|
|
+ @click.stop="deleteConversation(conv.session_id)"
|
|
|
+ class="action-btn delete-btn"
|
|
|
+ title="删除对话"
|
|
|
+ plain
|
|
|
+ circle
|
|
|
+ :disabled="isConversationActive"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 编辑状态的按钮 -->
|
|
|
+ <template v-else>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ size="small"
|
|
|
+ :icon="Check"
|
|
|
+ :loading="loadingConfirmEdit"
|
|
|
+ @click.stop="confirmEdit(conv.session_id)"
|
|
|
+ class="action-btn confirm-btn"
|
|
|
+ title="确认修改"
|
|
|
+ plain
|
|
|
+ circle
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ type="info"
|
|
|
+ size="small"
|
|
|
+ @click.stop="cancelEdit(conv.session_id)"
|
|
|
+ class="action-btn cancel-btn"
|
|
|
+ title="取消编辑"
|
|
|
+ plain
|
|
|
+ circle
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <Close/>
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-scrollbar>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="large"
|
|
|
+ class="create-conversation-btn"
|
|
|
+ @click="createConversationAndSetItActive"
|
|
|
+ :loading="creatingConversation"
|
|
|
+ :disabled="isConversationActive"
|
|
|
+ >创建对话
|
|
|
+ </el-button>
|
|
|
+ </el-aside>
|
|
|
+
|
|
|
+ <!-- 右侧聊天区域 -->
|
|
|
+ <el-main class="chat-main">
|
|
|
+ <!-- 消息展示区域 -->
|
|
|
+ <div class="messages-container" ref="messagesContainer" v-loading="loadingMessage">
|
|
|
+ <div v-if="messages.length !== 0">
|
|
|
+ <div v-for="(message, idx) in messages" :key="message.id" :class="['message-wrapper', message.role]">
|
|
|
+ <!-- AI消息 -->
|
|
|
+ <div v-if="message.role === 'assistant'" class="ai-message-container">
|
|
|
+ <el-avatar class="message-avatar" :icon="ChatDotRound"/>
|
|
|
+ <div class="ai-message-content">
|
|
|
+ <div class="message-bubble ai-bubble">
|
|
|
+ <Markdown v-if="message.render_content !== ''" :content="message.render_content" :plugins="plugins"
|
|
|
+ class="markdown-content"/>
|
|
|
+ <div v-else class="loading-container">
|
|
|
+ <div class="loading-dots">
|
|
|
+ <span class="dot"></span>
|
|
|
+ <span class="dot"></span>
|
|
|
+ <span class="dot"></span>
|
|
|
+ </div>
|
|
|
+ <span class="loading-text">AI正在思考中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="ai-message-actions">
|
|
|
+ <el-button
|
|
|
+ :loading="favoriteMessageIdx === idx && loadingFavoriteMessage"
|
|
|
+ @click="toggleFavorite(idx)"
|
|
|
+ class="favorite-btn"
|
|
|
+ :class="{ 'favorited': message.like ?? false }"
|
|
|
+ :disabled="isConversationActive"
|
|
|
+ :icon="(message.like ?? false) ? StarFilled : Star"
|
|
|
+ plain
|
|
|
+ circle
|
|
|
+ >
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 用户消息 -->
|
|
|
+ <div v-if="message.role === 'user'" class="user-message-container">
|
|
|
+ <div class="user-message-content">
|
|
|
+ <div class="message-bubble user-bubble">
|
|
|
+ {{ message.render_content }}
|
|
|
+ </div>
|
|
|
+ <div class="user-message-actions">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ @click="replaceMessage(messages.indexOf(message))"
|
|
|
+ class="retry-btn"
|
|
|
+ plain
|
|
|
+ :disabled="isConversationActive"
|
|
|
+ >
|
|
|
+ 重试
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-avatar class="message-avatar" :src="getUserInfos.avatar" :icon="User"/>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 空状态页面 -->
|
|
|
+
|
|
|
+ <div v-else class="empty-content">
|
|
|
+ <!-- 主图标 -->
|
|
|
+ <div class="empty-icon">
|
|
|
+ <el-icon :size="80" color="#d1d5db">
|
|
|
+ <ChatDotRound/>
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 标题和描述 -->
|
|
|
+ <div class="empty-text">
|
|
|
+ <h2 class="empty-title">开始新的对话</h2>
|
|
|
+ </div>
|
|
|
+ <!-- 示例问题 -->
|
|
|
+ <div class="example-questions">
|
|
|
+ <h4>试试这些问题:</h4>
|
|
|
+ <div class="question-tags">
|
|
|
+ <el-tag class="question-tag" @click="inputMessage = '帮我查看设备运行状态和告警信息'" type="info">
|
|
|
+ 帮我查看设备运行状态和告警信息
|
|
|
+ </el-tag>
|
|
|
+ <el-tag class="question-tag" @click="inputMessage = '分析用户权限配置和角色分配情况'" type="success">
|
|
|
+ 分析用户权限配置和角色分配情况
|
|
|
+ </el-tag>
|
|
|
+ <el-tag class="question-tag" @click="inputMessage = '检查系统性能和在线用户统计'" type="warning">
|
|
|
+ 检查系统性能和在线用户统计
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="messages-spacer"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="input-container" v-show="activeConversationId !== -1">
|
|
|
+ <!-- 工具和模型选择行 -->
|
|
|
+ <div class="selection-row">
|
|
|
+ <!-- 工具选择 -->
|
|
|
+ <div class="tool-selector">
|
|
|
+ <el-cascader
|
|
|
+ v-model="selectedTool"
|
|
|
+ :options="toolOptions"
|
|
|
+ :show-all-levels="false"
|
|
|
+ :props="{ multiple: true }"
|
|
|
+ collapse-tags
|
|
|
+ collapse-tags-tooltip
|
|
|
+ clearable
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 模型选择 -->
|
|
|
+ <div class="model-selector">
|
|
|
+ <el-select :loading="loadingModels" v-model="selectedModel" placeholder="选择模型" size="small"
|
|
|
+ style="width: 200px">
|
|
|
+ <el-option v-for="item in modelOptions" :key="item.id" :value="item.id" :label="item.modelName"/>
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 提示词输入框 -->
|
|
|
+ <div class="prompt-selector">
|
|
|
+ <el-input v-model="prompt" placeholder="点击设置提示词..." size="small" style="width: 200px" readonly
|
|
|
+ @click="openPromptDialog = true"/>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 输入框和按钮行 -->
|
|
|
+ <div class="input-row">
|
|
|
+ <!-- 输入框 -->
|
|
|
+ <div class="message-input-wrapper">
|
|
|
+ <el-input
|
|
|
+ v-model="inputMessage"
|
|
|
+ type="textarea"
|
|
|
+ placeholder="请输入您的问题..."
|
|
|
+ :rows="2"
|
|
|
+ resize="none"
|
|
|
+ @keydown.enter.ctrl="sendMessage"
|
|
|
+ @keydown.enter.meta="sendMessage"
|
|
|
+ :disabled="isConversationActive"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 按钮组 -->
|
|
|
+ <div class="button-group">
|
|
|
+ <el-button
|
|
|
+ v-show="messages.length !== 0"
|
|
|
+ type="warning"
|
|
|
+ size="small"
|
|
|
+ @click="clearMessage"
|
|
|
+ style="margin-left: 12px"
|
|
|
+ :loading="loadingClearMessage"
|
|
|
+ >
|
|
|
+ 清空
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="!isConversationActive"
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ @click="sendMessage"
|
|
|
+ @keyup.ctrl.enter="sendMessage"
|
|
|
+ :disabled="canSendMessage"
|
|
|
+ >
|
|
|
+ 发送
|
|
|
+ </el-button>
|
|
|
+ <el-button v-else type="danger" size="small" @click="stopConversation"> 终止</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-main>
|
|
|
+
|
|
|
+ <!-- 提示词设置对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="openPromptDialog"
|
|
|
+ title="设置提示词"
|
|
|
+ width="600px"
|
|
|
+ :before-close="
|
|
|
() => {
|
|
|
openPromptDialog = false
|
|
|
}
|
|
|
"
|
|
|
- >
|
|
|
- <el-input v-model="prompt" type="textarea" placeholder="请输入提示词..." :rows="8" resize="none" />
|
|
|
- <template #footer>
|
|
|
- <div class="dialog-footer">
|
|
|
- <el-button @click="openPromptDialog = false">取消</el-button>
|
|
|
- <el-button type="primary" @click="openPromptDialog = false">确定</el-button>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
- </el-container>
|
|
|
+ >
|
|
|
+ <el-input v-model="prompt" type="textarea" placeholder="请输入提示词..." :rows="8" resize="none"/>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="openPromptDialog = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="openPromptDialog = false">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </el-container>
|
|
|
</template>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
:deep(.el-icon) {
|
|
|
- margin-right: 0 !important;
|
|
|
+ margin-right: 0 !important;
|
|
|
}
|
|
|
|
|
|
.chat-container {
|
|
|
- height: 100%;
|
|
|
- background: var(--el-bg-color-page);
|
|
|
+ height: 100%;
|
|
|
+ background: var(--el-bg-color-page);
|
|
|
}
|
|
|
|
|
|
.create-conversation-btn {
|
|
|
- margin: 16px;
|
|
|
+ margin: 16px;
|
|
|
}
|
|
|
|
|
|
/* 左侧边栏样式 */
|
|
|
.chat-sidebar {
|
|
|
- background: var(--el-bg-color);
|
|
|
- border-right: 1px solid var(--el-border-color-light);
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border-right: 1px solid var(--el-border-color-light);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.sidebar-header {
|
|
|
- padding: 20px;
|
|
|
- border-bottom: 1px solid var(--el-border-color-light);
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
+ padding: 20px;
|
|
|
+ border-bottom: 1px solid var(--el-border-color-light);
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
|
|
|
- h3 {
|
|
|
- margin: 0;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
- }
|
|
|
+ h3 {
|
|
|
+ margin: 0;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* 设置面板样式 */
|
|
|
:deep(.el-dropdown-menu) {
|
|
|
- .settings-item {
|
|
|
- padding: 0;
|
|
|
+ .settings-item {
|
|
|
+ padding: 0;
|
|
|
|
|
|
- &:hover {
|
|
|
- background: none;
|
|
|
- }
|
|
|
- }
|
|
|
+ &:hover {
|
|
|
+ background: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- .settings-row {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 8px 16px;
|
|
|
- min-width: 160px;
|
|
|
+ .settings-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px 16px;
|
|
|
+ min-width: 160px;
|
|
|
|
|
|
- &:hover {
|
|
|
- background: var(--el-fill-color-light);
|
|
|
- }
|
|
|
- }
|
|
|
+ &:hover {
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- .settings-label {
|
|
|
- font-size: 14px;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- flex: 1;
|
|
|
- }
|
|
|
+ .settings-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.conversation-list {
|
|
|
- flex: 1;
|
|
|
- padding: 10px;
|
|
|
+ flex: 1;
|
|
|
+ padding: 10px;
|
|
|
}
|
|
|
|
|
|
.conversation-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 12px 16px;
|
|
|
- margin-bottom: 8px;
|
|
|
- border-radius: 8px;
|
|
|
- transition: all 0.2s;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- border: 1px solid transparent;
|
|
|
- position: relative;
|
|
|
- cursor: pointer;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- background: var(--el-fill-color-light);
|
|
|
- color: var(--el-text-color-primary);
|
|
|
-
|
|
|
- .conversation-actions {
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- &.active {
|
|
|
- background: var(--el-color-primary-light-9);
|
|
|
- color: var(--el-color-primary);
|
|
|
- border-color: var(--el-color-primary-light-7);
|
|
|
-
|
|
|
- .conversation-actions {
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- &.editing {
|
|
|
- background: var(--el-color-warning-light-9);
|
|
|
- border-color: var(--el-color-warning-light-7);
|
|
|
-
|
|
|
- .conversation-actions {
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
- }
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 12px 16px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ border: 1px solid transparent;
|
|
|
+ position: relative;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+
|
|
|
+ .conversation-actions {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ background: var(--el-color-primary-light-9);
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ border-color: var(--el-color-primary-light-7);
|
|
|
+
|
|
|
+ .conversation-actions {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.editing {
|
|
|
+ background: var(--el-color-warning-light-9);
|
|
|
+ border-color: var(--el-color-warning-light-7);
|
|
|
+
|
|
|
+ .conversation-actions {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.conversation-content {
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
- cursor: pointer;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.conversation-edit-content {
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.edit-input {
|
|
|
- width: 100%;
|
|
|
+ width: 100%;
|
|
|
|
|
|
- :deep(.el-input__inner) {
|
|
|
- font-size: 14px;
|
|
|
- padding: 4px 8px;
|
|
|
- height: 28px;
|
|
|
- line-height: 20px;
|
|
|
- }
|
|
|
+ :deep(.el-input__inner) {
|
|
|
+ font-size: 14px;
|
|
|
+ padding: 4px 8px;
|
|
|
+ height: 28px;
|
|
|
+ line-height: 20px;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.conversation-title {
|
|
|
- display: block;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.conversation-actions {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
- opacity: 0;
|
|
|
- transition: opacity 0.2s;
|
|
|
- flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.action-btn {
|
|
|
- width: 28px !important;
|
|
|
- height: 28px !important;
|
|
|
- padding: 0 !important;
|
|
|
- margin: 0 2px;
|
|
|
+ width: 28px !important;
|
|
|
+ height: 28px !important;
|
|
|
+ padding: 0 !important;
|
|
|
+ margin: 0 2px;
|
|
|
}
|
|
|
|
|
|
/* 右侧聊天区域样式 */
|
|
|
.chat-main {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- padding: 0;
|
|
|
- background: var(--el-bg-color-page);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 0;
|
|
|
+ background: var(--el-bg-color-page);
|
|
|
|
|
|
- position: relative;
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
.messages-container {
|
|
|
- flex: 1;
|
|
|
- padding: 20px;
|
|
|
- overflow-y: auto;
|
|
|
+ flex: 1;
|
|
|
+ padding: 20px;
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
|
|
|
|
.messages-spacer {
|
|
|
- height: 160px;
|
|
|
+ height: 160px;
|
|
|
}
|
|
|
|
|
|
.message-wrapper {
|
|
|
- margin-bottom: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
|
- &:last-child {
|
|
|
- margin-bottom: 0;
|
|
|
- }
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* AI消息样式 */
|
|
|
.ai-message-container {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- gap: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
.ai-message-content {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-start;
|
|
|
- gap: 8px;
|
|
|
- width: 70%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 8px;
|
|
|
+ width: 70%;
|
|
|
}
|
|
|
|
|
|
.ai-message-actions {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-start;
|
|
|
- transition: opacity 0.2s ease;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-start;
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
}
|
|
|
|
|
|
.favorite-btn {
|
|
|
- width: 32px !important;
|
|
|
- height: 32px !important;
|
|
|
- padding: 0 !important;
|
|
|
- border: none !important;
|
|
|
- background: transparent !important;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- transition: all 0.2s ease;
|
|
|
+ width: 32px !important;
|
|
|
+ height: 32px !important;
|
|
|
+ padding: 0 !important;
|
|
|
+ border: none !important;
|
|
|
+ background: transparent !important;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
|
- &:hover {
|
|
|
- background: var(--el-fill-color-light) !important;
|
|
|
- }
|
|
|
+ &:hover {
|
|
|
+ background: var(--el-fill-color-light) !important;
|
|
|
+ }
|
|
|
|
|
|
- &.favorited {
|
|
|
- color: #ff4757;
|
|
|
- }
|
|
|
+ &.favorited {
|
|
|
+ color: #ff4757;
|
|
|
+ }
|
|
|
|
|
|
- .el-icon {
|
|
|
- font-size: 16px;
|
|
|
- }
|
|
|
+ .el-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.ai-bubble {
|
|
|
- background: var(--el-fill-color-light);
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- position: relative;
|
|
|
- max-width: 100%;
|
|
|
- border: 1px solid var(--el-border-color-lighter);
|
|
|
-
|
|
|
- &::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- left: -8px;
|
|
|
- top: 12px;
|
|
|
- width: 0;
|
|
|
- height: 0;
|
|
|
- border-right: 8px solid var(--el-fill-color-light);
|
|
|
- border-top: 8px solid transparent;
|
|
|
- border-bottom: 8px solid transparent;
|
|
|
- }
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ position: relative;
|
|
|
+ max-width: 100%;
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: -8px;
|
|
|
+ top: 12px;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-right: 8px solid var(--el-fill-color-light);
|
|
|
+ border-top: 8px solid transparent;
|
|
|
+ border-bottom: 8px solid transparent;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* 用户消息样式 */
|
|
|
.user-message-container {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- gap: 12px;
|
|
|
- justify-content: flex-end;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
.user-message-content {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-end;
|
|
|
- gap: 8px;
|
|
|
- width: 70%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+ width: 70%;
|
|
|
}
|
|
|
|
|
|
.user-message-actions {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
- opacity: 0;
|
|
|
- transition: opacity 0.2s ease;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
}
|
|
|
|
|
|
.user-message-container:hover .user-message-actions {
|
|
|
- opacity: 1;
|
|
|
+ opacity: 1;
|
|
|
}
|
|
|
|
|
|
.retry-btn {
|
|
|
- font-size: 12px;
|
|
|
- padding: 4px 12px;
|
|
|
- height: 24px;
|
|
|
- border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 4px 12px;
|
|
|
+ height: 24px;
|
|
|
+ border-radius: 12px;
|
|
|
}
|
|
|
|
|
|
.user-bubble {
|
|
|
- background: var(--el-color-primary);
|
|
|
- color: white;
|
|
|
- position: relative;
|
|
|
- max-width: 100%;
|
|
|
-
|
|
|
- &::after {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- right: -8px;
|
|
|
- top: 12px;
|
|
|
- width: 0;
|
|
|
- height: 0;
|
|
|
- border-left: 8px solid var(--el-color-primary);
|
|
|
- border-top: 8px solid transparent;
|
|
|
- border-bottom: 8px solid transparent;
|
|
|
- }
|
|
|
+ background: var(--el-color-primary);
|
|
|
+ color: white;
|
|
|
+ position: relative;
|
|
|
+ max-width: 100%;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ right: -8px;
|
|
|
+ top: 12px;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-left: 8px solid var(--el-color-primary);
|
|
|
+ border-top: 8px solid transparent;
|
|
|
+ border-bottom: 8px solid transparent;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* 消息气泡通用样式 */
|
|
|
.message-bubble {
|
|
|
- padding: 12px 16px;
|
|
|
- border-radius: 12px;
|
|
|
- line-height: 1.5;
|
|
|
- word-wrap: break-word;
|
|
|
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ word-wrap: break-word;
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
.message-avatar {
|
|
|
- flex-shrink: 0;
|
|
|
- margin-top: 2px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-top: 2px;
|
|
|
}
|
|
|
|
|
|
/* 加载动画样式 */
|
|
|
.loading-container {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- padding: 8px 0;
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 8px 0;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
}
|
|
|
|
|
|
.loading-dots {
|
|
|
- display: flex;
|
|
|
- gap: 4px;
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
}
|
|
|
|
|
|
.dot {
|
|
|
- width: 6px;
|
|
|
- height: 6px;
|
|
|
- border-radius: 50%;
|
|
|
- background-color: var(--el-color-primary);
|
|
|
- animation: loading-bounce 1.4s ease-in-out infinite both;
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: var(--el-color-primary);
|
|
|
+ animation: loading-bounce 1.4s ease-in-out infinite both;
|
|
|
}
|
|
|
|
|
|
.dot:nth-child(1) {
|
|
|
- animation-delay: -0.32s;
|
|
|
+ animation-delay: -0.32s;
|
|
|
}
|
|
|
|
|
|
.dot:nth-child(2) {
|
|
|
- animation-delay: -0.16s;
|
|
|
+ animation-delay: -0.16s;
|
|
|
}
|
|
|
|
|
|
.dot:nth-child(3) {
|
|
|
- animation-delay: 0s;
|
|
|
+ animation-delay: 0s;
|
|
|
}
|
|
|
|
|
|
@keyframes loading-bounce {
|
|
|
- 0%,
|
|
|
- 80%,
|
|
|
- 100% {
|
|
|
- transform: scale(0.8);
|
|
|
- opacity: 0.5;
|
|
|
- }
|
|
|
- 40% {
|
|
|
- transform: scale(1);
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
+ 0%,
|
|
|
+ 80%,
|
|
|
+ 100% {
|
|
|
+ transform: scale(0.8);
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
+ 40% {
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.loading-text {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.tools-button {
|
|
|
- margin-bottom: 10px;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
}
|
|
|
|
|
|
.message-input {
|
|
|
- margin-bottom: 12px;
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
|
- :deep(.el-textarea__inner) {
|
|
|
- border-radius: 8px;
|
|
|
- resize: none;
|
|
|
- }
|
|
|
+ :deep(.el-textarea__inner) {
|
|
|
+ border-radius: 8px;
|
|
|
+ resize: none;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.input-actions {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
- gap: 12px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
/* Markdown 内容样式优化 - 接入Element Plus颜色系统 */
|
|
|
:deep(.markdown-content) {
|
|
|
- /* 标题样式 */
|
|
|
- h1,
|
|
|
- h2,
|
|
|
- h3,
|
|
|
- h4,
|
|
|
- h5,
|
|
|
- h6 {
|
|
|
- margin-top: 24px;
|
|
|
- margin-bottom: 16px;
|
|
|
- font-weight: 600;
|
|
|
- line-height: 1.25;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- }
|
|
|
-
|
|
|
- h1 {
|
|
|
- font-size: 2em;
|
|
|
- border-bottom: 1px solid var(--el-border-color-light);
|
|
|
- padding-bottom: 8px;
|
|
|
- }
|
|
|
-
|
|
|
- h2 {
|
|
|
- font-size: 1.5em;
|
|
|
- border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
- padding-bottom: 6px;
|
|
|
- }
|
|
|
-
|
|
|
- h3 {
|
|
|
- font-size: 1.25em;
|
|
|
- }
|
|
|
-
|
|
|
- /* 段落样式 */
|
|
|
- p {
|
|
|
- margin-bottom: 16px;
|
|
|
- line-height: 1.6;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- }
|
|
|
-
|
|
|
- /* 代码块样式 */
|
|
|
- pre {
|
|
|
- background-color: var(--el-fill-color-light);
|
|
|
- border: 1px solid var(--el-border-color-light);
|
|
|
- border-radius: 6px;
|
|
|
- padding: 16px;
|
|
|
- overflow: auto;
|
|
|
- margin: 16px 0;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- }
|
|
|
-
|
|
|
- /* 行内代码样式 */
|
|
|
- code {
|
|
|
- background-color: var(--el-fill-color-light);
|
|
|
- color: var(--el-color-danger);
|
|
|
- padding: 2px 4px;
|
|
|
- border-radius: 3px;
|
|
|
- font-size: 85%;
|
|
|
- border: 1px solid var(--el-border-color-lighter);
|
|
|
- }
|
|
|
-
|
|
|
- /* 引用块样式 */
|
|
|
- blockquote {
|
|
|
- border-left: 4px solid var(--el-color-primary);
|
|
|
- padding-left: 16px;
|
|
|
- margin: 16px 0;
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
- background-color: var(--el-fill-color-extra-light);
|
|
|
- padding: 12px 16px;
|
|
|
- border-radius: 4px;
|
|
|
- }
|
|
|
-
|
|
|
- /* 列表样式 */
|
|
|
- ul,
|
|
|
- ol {
|
|
|
- margin: 16px 0;
|
|
|
- padding-left: 32px;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- }
|
|
|
-
|
|
|
- li {
|
|
|
- margin: 4px 0;
|
|
|
- line-height: 1.6;
|
|
|
- }
|
|
|
-
|
|
|
- /* 表格样式 */
|
|
|
- table {
|
|
|
- border-collapse: collapse;
|
|
|
- margin: 16px 0;
|
|
|
- width: 100%;
|
|
|
- border: 1px solid var(--el-border-color-light);
|
|
|
- }
|
|
|
-
|
|
|
- th,
|
|
|
- td {
|
|
|
- border: 1px solid var(--el-border-color-light);
|
|
|
- padding: 8px 12px;
|
|
|
- text-align: left;
|
|
|
- }
|
|
|
-
|
|
|
- th {
|
|
|
- background-color: var(--el-fill-color-light);
|
|
|
- font-weight: 600;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- }
|
|
|
-
|
|
|
- td {
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- }
|
|
|
-
|
|
|
- /* 分割线样式 */
|
|
|
- hr {
|
|
|
- border: none;
|
|
|
- border-top: 1px solid var(--el-border-color-light);
|
|
|
- margin: 24px 0;
|
|
|
- }
|
|
|
-
|
|
|
- /* 链接样式 */
|
|
|
- a {
|
|
|
- color: var(--el-color-primary);
|
|
|
- text-decoration: none;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- color: var(--el-color-primary-light-3);
|
|
|
- text-decoration: underline;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /* 强调文本样式 */
|
|
|
- strong {
|
|
|
- font-weight: 600;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- }
|
|
|
-
|
|
|
- em {
|
|
|
- font-style: italic;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- }
|
|
|
+ /* 标题样式 */
|
|
|
+ h1,
|
|
|
+ h2,
|
|
|
+ h3,
|
|
|
+ h4,
|
|
|
+ h5,
|
|
|
+ h6 {
|
|
|
+ margin-top: 24px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 1.25;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ h1 {
|
|
|
+ font-size: 2em;
|
|
|
+ border-bottom: 1px solid var(--el-border-color-light);
|
|
|
+ padding-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ h2 {
|
|
|
+ font-size: 1.5em;
|
|
|
+ border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
+ padding-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ font-size: 1.25em;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 段落样式 */
|
|
|
+ p {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 代码块样式 */
|
|
|
+ pre {
|
|
|
+ background-color: var(--el-fill-color-light);
|
|
|
+ border: 1px solid var(--el-border-color-light);
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 16px;
|
|
|
+ overflow: auto;
|
|
|
+ margin: 16px 0;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 行内代码样式 */
|
|
|
+ code {
|
|
|
+ background-color: var(--el-fill-color-light);
|
|
|
+ color: var(--el-color-danger);
|
|
|
+ padding: 2px 4px;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-size: 85%;
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 引用块样式 */
|
|
|
+ blockquote {
|
|
|
+ border-left: 4px solid var(--el-color-primary);
|
|
|
+ padding-left: 16px;
|
|
|
+ margin: 16px 0;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ background-color: var(--el-fill-color-extra-light);
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 列表样式 */
|
|
|
+ ul,
|
|
|
+ ol {
|
|
|
+ margin: 16px 0;
|
|
|
+ padding-left: 32px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+
|
|
|
+ li {
|
|
|
+ margin: 4px 0;
|
|
|
+ line-height: 1.6;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 表格样式 */
|
|
|
+ table {
|
|
|
+ border-collapse: collapse;
|
|
|
+ margin: 16px 0;
|
|
|
+ width: 100%;
|
|
|
+ border: 1px solid var(--el-border-color-light);
|
|
|
+ }
|
|
|
+
|
|
|
+ th,
|
|
|
+ td {
|
|
|
+ border: 1px solid var(--el-border-color-light);
|
|
|
+ padding: 8px 12px;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background-color: var(--el-fill-color-light);
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ td {
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 分割线样式 */
|
|
|
+ hr {
|
|
|
+ border: none;
|
|
|
+ border-top: 1px solid var(--el-border-color-light);
|
|
|
+ margin: 24px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 链接样式 */
|
|
|
+ a {
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ text-decoration: none;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: var(--el-color-primary-light-3);
|
|
|
+ text-decoration: underline;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 强调文本样式 */
|
|
|
+ strong {
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ em {
|
|
|
+ font-style: italic;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.input-container {
|
|
|
- position: absolute;
|
|
|
- width: 90%;
|
|
|
- left: 50%;
|
|
|
- bottom: 25px;
|
|
|
- transform: translate(-50%, 0);
|
|
|
- background: var(--el-bg-color);
|
|
|
- border: 1px solid var(--el-border-color-light);
|
|
|
- border-radius: 12px;
|
|
|
- padding: 16px;
|
|
|
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
+ position: absolute;
|
|
|
+ width: 90%;
|
|
|
+ left: 50%;
|
|
|
+ bottom: 25px;
|
|
|
+ transform: translate(-50%, 0);
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border: 1px solid var(--el-border-color-light);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
.selection-row {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- flex-wrap: wrap;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.tool-selector,
|
|
|
.model-selector,
|
|
|
.prompt-selector {
|
|
|
- flex-shrink: 0;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.input-row {
|
|
|
- display: flex;
|
|
|
- align-items: flex-end;
|
|
|
- gap: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
.message-input-wrapper {
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.message-input-wrapper :deep(.el-textarea__inner) {
|
|
|
- border-radius: 8px;
|
|
|
- resize: none;
|
|
|
- font-size: 14px;
|
|
|
- line-height: 1.4;
|
|
|
+ border-radius: 8px;
|
|
|
+ resize: none;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.4;
|
|
|
}
|
|
|
|
|
|
.button-group {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 6px;
|
|
|
- flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.button-group .el-button {
|
|
|
- min-width: 60px;
|
|
|
+ min-width: 60px;
|
|
|
}
|
|
|
|
|
|
/* 提示词输入框样式 */
|
|
|
.prompt-selector .el-input {
|
|
|
- cursor: pointer;
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.prompt-selector .el-input__inner {
|
|
|
- cursor: pointer;
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
|
|
|
|
/* 对话框样式 */
|
|
|
.dialog-footer {
|
|
|
- text-align: right;
|
|
|
+ text-align: right;
|
|
|
}
|
|
|
|
|
|
/* 响应式调整 */
|
|
|
/* 空状态页面样式 */
|
|
|
.empty-content {
|
|
|
- text-align: center;
|
|
|
- max-width: 600px;
|
|
|
- width: 100%;
|
|
|
+ text-align: center;
|
|
|
+ max-width: 600px;
|
|
|
+ width: 100%;
|
|
|
|
|
|
- position: absolute;
|
|
|
- left: 50%;
|
|
|
- top: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
}
|
|
|
|
|
|
.empty-icon {
|
|
|
- margin-bottom: 24px;
|
|
|
- opacity: 0.6;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ opacity: 0.6;
|
|
|
}
|
|
|
|
|
|
.empty-text {
|
|
|
- margin-bottom: 40px;
|
|
|
+ margin-bottom: 40px;
|
|
|
}
|
|
|
|
|
|
.empty-title {
|
|
|
- font-size: 28px;
|
|
|
- font-weight: 600;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- margin: 0 0 12px 0;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ margin: 0 0 12px 0;
|
|
|
}
|
|
|
|
|
|
.empty-description {
|
|
|
- font-size: 16px;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
- margin: 0;
|
|
|
- line-height: 1.5;
|
|
|
+ font-size: 16px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ margin: 0;
|
|
|
+ line-height: 1.5;
|
|
|
}
|
|
|
|
|
|
.quick-start {
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- gap: 40px;
|
|
|
- margin-bottom: 40px;
|
|
|
- flex-wrap: wrap;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 40px;
|
|
|
+ margin-bottom: 40px;
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.quick-start-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- color: var(--el-text-color-regular);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
}
|
|
|
|
|
|
.step-number {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- width: 24px;
|
|
|
- height: 24px;
|
|
|
- background: var(--el-color-primary);
|
|
|
- color: white;
|
|
|
- border-radius: 50%;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 600;
|
|
|
- flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ background: var(--el-color-primary);
|
|
|
+ color: white;
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.step-text {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.example-questions {
|
|
|
- h4 {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- margin: 0 0 16px 0;
|
|
|
- }
|
|
|
+ h4 {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ margin: 0 0 16px 0;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.question-tags {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 12px;
|
|
|
- justify-content: center;
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+ justify-content: center;
|
|
|
}
|
|
|
|
|
|
.question-tag {
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.2s ease;
|
|
|
- font-size: 13px;
|
|
|
- padding: 8px 16px;
|
|
|
- border-radius: 20px;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- transform: translateY(-1px);
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
- }
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-radius: 20px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|