|  | @@ -0,0 +1,2530 @@
 | 
	
		
			
				|  |  | +<script setup lang="ts">
 | 
	
		
			
				|  |  | +import { computed, isReactive, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
 | 
	
		
			
				|  |  | +import { useI18n } from 'vue-i18n'
 | 
	
		
			
				|  |  | +import { Local } from '/@/utils/storage'
 | 
	
		
			
				|  |  | +import {
 | 
	
		
			
				|  |  | +	ArrowDown,
 | 
	
		
			
				|  |  | +	ChatDotRound,
 | 
	
		
			
				|  |  | +	Check,
 | 
	
		
			
				|  |  | +	Close,
 | 
	
		
			
				|  |  | +	CopyDocument,
 | 
	
		
			
				|  |  | +	Delete,
 | 
	
		
			
				|  |  | +	Document,
 | 
	
		
			
				|  |  | +	Download,
 | 
	
		
			
				|  |  | +	Edit,
 | 
	
		
			
				|  |  | +	Files,
 | 
	
		
			
				|  |  | +	Headset,
 | 
	
		
			
				|  |  | +	Loading,
 | 
	
		
			
				|  |  | +	MoreFilled,
 | 
	
		
			
				|  |  | +	Picture,
 | 
	
		
			
				|  |  | +	Plus,
 | 
	
		
			
				|  |  | +	Promotion,
 | 
	
		
			
				|  |  | +	Search,
 | 
	
		
			
				|  |  | +	Setting,
 | 
	
		
			
				|  |  | +	Setting as EleSetting,
 | 
	
		
			
				|  |  | +	Star,
 | 
	
		
			
				|  |  | +	StarFilled,
 | 
	
		
			
				|  |  | +	User,
 | 
	
		
			
				|  |  | +	VideoPause,
 | 
	
		
			
				|  |  | +	VideoPlay,
 | 
	
		
			
				|  |  | +} 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, Prompt } from '/@/api/assist/type'
 | 
	
		
			
				|  |  | +import { useLoading } from '/@/utils/loading-util'
 | 
	
		
			
				|  |  | +import { useRouter } from 'vue-router'
 | 
	
		
			
				|  |  | +import { ElMessage, ElMessageBox } from 'element-plus'
 | 
	
		
			
				|  |  | +import StructDataPlugin from '/@/components/markdown/plugins/struct-data'
 | 
	
		
			
				|  |  | +import { useDropZone, useFileDialog } from '@vueuse/core'
 | 
	
		
			
				|  |  | +import download from 'downloadjs'
 | 
	
		
			
				|  |  | +import common from '/@/api/common'
 | 
	
		
			
				|  |  | +import { UploadFile } from '/@/api/common/type'
 | 
	
		
			
				|  |  | +import system from '/@/api/system'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const plugins: Array<MarkdownPlugin<any>> = [EChartsPlugin(), ToolsLoadingPlugin(), TablePlugin(), StructDataPlugin()]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 国际化
 | 
	
		
			
				|  |  | +const { t } = useI18n()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +//聊天管理接口
 | 
	
		
			
				|  |  | +// 消息列表
 | 
	
		
			
				|  |  | +const messages = ref<Message[]>([])
 | 
	
		
			
				|  |  | +const messagesContainer = ref<HTMLElement>()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 输入框内容
 | 
	
		
			
				|  |  | +const inputMessage = ref('')
 | 
	
		
			
				|  |  | +const {
 | 
	
		
			
				|  |  | +	open,
 | 
	
		
			
				|  |  | +	reset,
 | 
	
		
			
				|  |  | +	files: attachments,
 | 
	
		
			
				|  |  | +} = useFileDialog({
 | 
	
		
			
				|  |  | +	multiple: true,
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 附件管理:使用本地列表以支持单个移除
 | 
	
		
			
				|  |  | +const dropToUploadZone = ref<HTMLElement>()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const { isOverDropZone } = useDropZone(dropToUploadZone, async (files: File[] | null) => {
 | 
	
		
			
				|  |  | +	if (loadingUpload.value) {
 | 
	
		
			
				|  |  | +		ElMessage.warning(t('message.assistant.status.uploading'))
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	if (files === null || files.length === 0) return
 | 
	
		
			
				|  |  | +	const data = await doUpload(files).catch(() => undefined)
 | 
	
		
			
				|  |  | +	if (data === undefined) {
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	selectedFiles.value = [...selectedFiles.value, ...data]
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const selectedFiles = ref<UploadFile[]>([])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const uploadProgress = ref<number>(-1)
 | 
	
		
			
				|  |  | +const { loading: loadingUpload, doLoading: doUpload } = useLoading(async (file: File[]) => {
 | 
	
		
			
				|  |  | +	uploadProgress.value = -1
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return (await common.upload
 | 
	
		
			
				|  |  | +		.multi(file, (progress) => (uploadProgress.value = progress * 100))
 | 
	
		
			
				|  |  | +		.finally(() => (uploadProgress.value = -1))) as UploadFile[]
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +watch(
 | 
	
		
			
				|  |  | +	attachments,
 | 
	
		
			
				|  |  | +	async (newFiles) => {
 | 
	
		
			
				|  |  | +		if (newFiles === null || newFiles.length === 0) return
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		const data = await doUpload(Array.from(newFiles)).catch(() => undefined)
 | 
	
		
			
				|  |  | +		if (data === undefined) {
 | 
	
		
			
				|  |  | +			reset()
 | 
	
		
			
				|  |  | +			return
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		selectedFiles.value = [...selectedFiles.value, ...data]
 | 
	
		
			
				|  |  | +		reset()
 | 
	
		
			
				|  |  | +	},
 | 
	
		
			
				|  |  | +	{ immediate: true }
 | 
	
		
			
				|  |  | +)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const removeAttachment = (index: number) => {
 | 
	
		
			
				|  |  | +	if (index < 0 || index >= selectedFiles.value.length) return
 | 
	
		
			
				|  |  | +	selectedFiles.value.splice(index, 1)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// // 选中的工具和模型
 | 
	
		
			
				|  |  | +// 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: '进程启动',
 | 
	
		
			
				|  |  | +// 			},
 | 
	
		
			
				|  |  | +// 		],
 | 
	
		
			
				|  |  | +// 	},
 | 
	
		
			
				|  |  | +// ])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 模型选择
 | 
	
		
			
				|  |  | +const modelOptions = ref<LmConfigInfo[]>([])
 | 
	
		
			
				|  |  | +const modelLabel = computed(() => {
 | 
	
		
			
				|  |  | +	if (!loadingModels.value && selectedModel.value === undefined) {
 | 
	
		
			
				|  |  | +		return t('message.assistant.status.noModelConfigured')
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	const select = modelOptions.value.filter((i) => i.id === selectedModel.value)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if (select.length === 0) {
 | 
	
		
			
				|  |  | +		return t('message.assistant.status.loading')
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return select[0].modelName
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const { loading: loadingModels, doLoading: loadModel } = useLoading(async () => {
 | 
	
		
			
				|  |  | +	const data: { list: LmConfigInfo[]; total: number } = await assist.model.getList({ modelType: 'chat' }).catch(() => {
 | 
	
		
			
				|  |  | +		return {
 | 
	
		
			
				|  |  | +			list: [],
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	modelOptions.value = data.list ?? []
 | 
	
		
			
				|  |  | +	selectedModel.value = modelOptions.value[0]?.id ?? undefined
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +onMounted(loadModel)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const selectEmbeddingId = ref<number | undefined>(undefined)
 | 
	
		
			
				|  |  | +const { loading: loadingEmbedding, doLoading: loadEmbedding } = useLoading(async () => {
 | 
	
		
			
				|  |  | +	const embedding_key = await system.getInfoByKey('assistant.embedding.default').then((res: any) => res.data.configValue)
 | 
	
		
			
				|  |  | +  const embedding_model = await assist.model.getList({ modelType: 'embedding',keyWord: embedding_key }) as LmConfigInfo[]
 | 
	
		
			
				|  |  | +  selectEmbeddingId.value = embedding_model[0]?.id
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +onMounted(loadEmbedding)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 提示词列表相关状态
 | 
	
		
			
				|  |  | +const customPrompt = ref('')
 | 
	
		
			
				|  |  | +const promptList = ref<Prompt[]>([])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const displayPromptList = computed(() => {
 | 
	
		
			
				|  |  | +	//有提示词展示自定义提示词
 | 
	
		
			
				|  |  | +	let r = [...promptList.value]
 | 
	
		
			
				|  |  | +	if (customPrompt.value.trim() !== '') {
 | 
	
		
			
				|  |  | +		r.splice(1, 0, {
 | 
	
		
			
				|  |  | +			id: -2,
 | 
	
		
			
				|  |  | +			title: t('message.assistant.prompt.customPromptWithCount', { count: customPrompt.value.length }),
 | 
	
		
			
				|  |  | +			prompt: customPrompt.value,
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	return r
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +const selectPromptId = ref<number | undefined>(undefined)
 | 
	
		
			
				|  |  | +watch(selectPromptId, (newVal) => {
 | 
	
		
			
				|  |  | +	const placeholder = displayPromptList.value.find((i) => i.id === newVal)?.placeholder
 | 
	
		
			
				|  |  | +	if (placeholder === undefined) {
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	messages.value.push({
 | 
	
		
			
				|  |  | +		id: messages.value.length,
 | 
	
		
			
				|  |  | +		render_content: placeholder,
 | 
	
		
			
				|  |  | +		timestamp: Date.now(),
 | 
	
		
			
				|  |  | +		role: 'assistant',
 | 
	
		
			
				|  |  | +		content: placeholder,
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +	// inputMessage.value = displayPromptList.value.find((i) => i.id === newVal)?.placeholder ?? ''
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +const promptLabel = computed(() => {
 | 
	
		
			
				|  |  | +	if (!loadingPromptList.value && selectPromptId.value === undefined) {
 | 
	
		
			
				|  |  | +		return t('message.assistant.status.noPromptConfigured')
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	const select = displayPromptList.value.filter((i) => i.id === selectPromptId.value)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if (select.length === 0) {
 | 
	
		
			
				|  |  | +		return t('message.assistant.status.loading')
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return select[0].title
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +const { loading: loadingPromptList, doLoading: loadPromptList } = useLoading(async () => {
 | 
	
		
			
				|  |  | +	const data: { list: Prompt[]; total: number } = await assist.chat.prompt.list({ pageSize: 10, pageNum: 1, keyWord: '' }).catch(() => {
 | 
	
		
			
				|  |  | +		return {
 | 
	
		
			
				|  |  | +			list: [],
 | 
	
		
			
				|  |  | +			total: 0,
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	promptList.value = data.list ?? []
 | 
	
		
			
				|  |  | +	if (promptList.value.length !== 0) {
 | 
	
		
			
				|  |  | +		promptList.value.unshift({
 | 
	
		
			
				|  |  | +			id: -1,
 | 
	
		
			
				|  |  | +			title: t('message.assistant.prompt.noPrompt'),
 | 
	
		
			
				|  |  | +			prompt: '',
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	selectPromptId.value = promptList.value[0]?.id ?? undefined
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +onMounted(loadPromptList)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const selectedModel = ref<number | undefined>(undefined)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 提示词管理对话框可见性
 | 
	
		
			
				|  |  | +const promptDialogVisible = ref(false)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const chatInstance = ref<(() => void) | undefined>(undefined)
 | 
	
		
			
				|  |  | +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 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(),
 | 
	
		
			
				|  |  | +		files: selectedFiles.value,
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +	selectedFiles.value = []
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	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 = async (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 (aiMessage?.like === true) {
 | 
	
		
			
				|  |  | +			const confirm = await ElMessageBox({
 | 
	
		
			
				|  |  | +				title: t('message.assistant.messages.prompt'),
 | 
	
		
			
				|  |  | +				message: t('message.assistant.messages.replaceMessageWarning'),
 | 
	
		
			
				|  |  | +				type: 'warning',
 | 
	
		
			
				|  |  | +				confirmButtonText: t('message.assistant.buttons.confirmDialog'),
 | 
	
		
			
				|  |  | +				cancelButtonText: t('message.assistant.buttons.cancelDialog'),
 | 
	
		
			
				|  |  | +			})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			if (!confirm) {
 | 
	
		
			
				|  |  | +				return
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		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) => {
 | 
	
		
			
				|  |  | +	let r = [...context]
 | 
	
		
			
				|  |  | +	if (selectPromptId.value != undefined && selectPromptId.value != -1) {
 | 
	
		
			
				|  |  | +		const prompt = displayPromptList.value.find((i) => i.id === selectPromptId.value)?.prompt ?? ''
 | 
	
		
			
				|  |  | +		//insert element on top
 | 
	
		
			
				|  |  | +		r.unshift({
 | 
	
		
			
				|  |  | +			id: messages.value.length,
 | 
	
		
			
				|  |  | +			role: 'system',
 | 
	
		
			
				|  |  | +			render_content: prompt,
 | 
	
		
			
				|  |  | +			content: prompt,
 | 
	
		
			
				|  |  | +			timestamp: Date.now(),
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	chatInstance.value = assist.chat.sse({
 | 
	
		
			
				|  |  | +		chatRequest: {
 | 
	
		
			
				|  |  | +			session_id: activeConversationId.value!,
 | 
	
		
			
				|  |  | +			message: r,
 | 
	
		
			
				|  |  | +			modelClassId: selectedModel.value,
 | 
	
		
			
				|  |  | +			modelEmbeddingId: selectEmbeddingId.value,
 | 
	
		
			
				|  |  | +		},
 | 
	
		
			
				|  |  | +		onReceive: (resp: ChatResponse) => {
 | 
	
		
			
				|  |  | +			switch (resp.type) {
 | 
	
		
			
				|  |  | +				case 'structdata':
 | 
	
		
			
				|  |  | +					rtn.render_content += `
 | 
	
		
			
				|  |  | +\`\`\`structdata
 | 
	
		
			
				|  |  | +${resp.uuid}
 | 
	
		
			
				|  |  | +\`\`\`
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +`
 | 
	
		
			
				|  |  | +					break
 | 
	
		
			
				|  |  | +				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}
 | 
	
		
			
				|  |  | +${resp.response.data.replace('\n', '')}
 | 
	
		
			
				|  |  | +\`\`\`
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +`
 | 
	
		
			
				|  |  | +					} else {
 | 
	
		
			
				|  |  | +						if (rtn.render_content.at(-1) !== '\n') {
 | 
	
		
			
				|  |  | +							rtn.render_content += '\n\n'
 | 
	
		
			
				|  |  | +						}
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +					//防止重试时底部有聊天记录无脑插入到底部出现后端无法识别的问题
 | 
	
		
			
				|  |  | +					messages.value.splice(messages.value.indexOf(rtn) + 1, 0, {
 | 
	
		
			
				|  |  | +						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}
 | 
	
		
			
				|  |  | +${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 += `
 | 
	
		
			
				|  |  | +> ### 系统报错
 | 
	
		
			
				|  |  | +>
 | 
	
		
			
				|  |  | +>
 | 
	
		
			
				|  |  | +> ${resp.error.split('\n').join('\n> ')}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +`
 | 
	
		
			
				|  |  | +					//过滤ai消息下的所有tool防止报错,鬼知道后端怎么想的
 | 
	
		
			
				|  |  | +					messages.value = messages.value.slice(0, messages.value.indexOf(rtn) + 1)
 | 
	
		
			
				|  |  | +					break
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		},
 | 
	
		
			
				|  |  | +		onComplete: async (e) => {
 | 
	
		
			
				|  |  | +			if (e !== undefined) {
 | 
	
		
			
				|  |  | +				rtn.content += `
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				`
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			rtn.render_content += '\n'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			const save_status = await assist.session.message
 | 
	
		
			
				|  |  | +				.save({
 | 
	
		
			
				|  |  | +					sessionId: activeConversationId.value!,
 | 
	
		
			
				|  |  | +					messages: messages.value,
 | 
	
		
			
				|  |  | +				})
 | 
	
		
			
				|  |  | +				.then(() => true)
 | 
	
		
			
				|  |  | +				.catch(() => false)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			if (!save_status) {
 | 
	
		
			
				|  |  | +				ElMessage.warning(t('message.assistant.messages.messageSaveFailed'))
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			chatInstance.value = undefined
 | 
	
		
			
				|  |  | +		},
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 终止对话
 | 
	
		
			
				|  |  | +const stopConversation = () => {
 | 
	
		
			
				|  |  | +	chatInstance.value?.()
 | 
	
		
			
				|  |  | +	chatInstance.value = undefined
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 滚动到底部
 | 
	
		
			
				|  |  | +const scrollToBottom = () => {
 | 
	
		
			
				|  |  | +	nextTick(() => {
 | 
	
		
			
				|  |  | +		if (messagesContainer.value) {
 | 
	
		
			
				|  |  | +			messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 会话管理模块
 | 
	
		
			
				|  |  | +// 所有会话
 | 
	
		
			
				|  |  | +const conversations = ref<LmSession[]>([])
 | 
	
		
			
				|  |  | +const displayConversations = computed(() => {
 | 
	
		
			
				|  |  | +	return [{ session_id: -1, title: t('message.assistant.sidebar.bookmark') }, ...conversations.value]
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +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 ?? []
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +onMounted(doLoadConversations)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 当前活跃会话
 | 
	
		
			
				|  |  | +const activeConversationId = ref<number | undefined>(undefined)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// watch(activeConversationId, async (newVal) => {
 | 
	
		
			
				|  |  | +// 	if (newVal === undefined) {
 | 
	
		
			
				|  |  | +// 		return
 | 
	
		
			
				|  |  | +// 	}
 | 
	
		
			
				|  |  | +// 	await doLoadingMessage(newVal)
 | 
	
		
			
				|  |  | +// })
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const bookmarkOptions = ref({
 | 
	
		
			
				|  |  | +	pageSize: 30,
 | 
	
		
			
				|  |  | +	pageNum: 1,
 | 
	
		
			
				|  |  | +	keyWord: '',
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 收藏消息总数
 | 
	
		
			
				|  |  | +const bookmarkTotal = ref(0)
 | 
	
		
			
				|  |  | +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(bookmarkOptions.value).catch(() => {
 | 
	
		
			
				|  |  | +			return {
 | 
	
		
			
				|  |  | +				list: [],
 | 
	
		
			
				|  |  | +				total: 0,
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +		messages.value = data.messages ?? []
 | 
	
		
			
				|  |  | +		bookmarkTotal.value = data.total ?? 0
 | 
	
		
			
				|  |  | +		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)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 删除会话
 | 
	
		
			
				|  |  | +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(t('message.assistant.messages.deleteSuccess'))
 | 
	
		
			
				|  |  | +	activeConversationId.value = undefined
 | 
	
		
			
				|  |  | +	messages.value = []
 | 
	
		
			
				|  |  | +	conversations.value = conversations.value.filter((item) => item.session_id !== id)
 | 
	
		
			
				|  |  | +	// await nextTick()
 | 
	
		
			
				|  |  | +	// await doLoadConversations()
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const multiDeleteConversationModel = ref({
 | 
	
		
			
				|  |  | +	visible: false,
 | 
	
		
			
				|  |  | +	selectedConversations: [] as number[],
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const multiDeleteConversationClear = () => {
 | 
	
		
			
				|  |  | +	multiDeleteConversationModel.value.selectedConversations = []
 | 
	
		
			
				|  |  | +	multiDeleteConversationModel.value.visible = false
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 多选删除会话
 | 
	
		
			
				|  |  | +const startMultidelete = async () => {
 | 
	
		
			
				|  |  | +	if (multiDeleteConversationModel.value.selectedConversations.length === 0) {
 | 
	
		
			
				|  |  | +		ElMessage.warning(t('message.assistant.messages.selectConversationsToDelete'))
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	const confirm = await ElMessageBox.confirm(
 | 
	
		
			
				|  |  | +		t('message.assistant.messages.deleteConfirm', { count: multiDeleteConversationModel.value.selectedConversations.length }),
 | 
	
		
			
				|  |  | +		t('message.assistant.messages.warning'),
 | 
	
		
			
				|  |  | +		{
 | 
	
		
			
				|  |  | +			confirmButtonText: t('message.assistant.buttons.confirmDialog'),
 | 
	
		
			
				|  |  | +			cancelButtonText: t('message.assistant.buttons.cancelDialog'),
 | 
	
		
			
				|  |  | +			type: 'error',
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if (confirm !== 'confirm') {
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	const res = await assist.session
 | 
	
		
			
				|  |  | +		.del(multiDeleteConversationModel.value.selectedConversations)
 | 
	
		
			
				|  |  | +		.then(() => true)
 | 
	
		
			
				|  |  | +		.catch(() => false)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if (!res) {
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	ElMessage.success(t('message.assistant.messages.deleteSuccess'))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	await nextTick()
 | 
	
		
			
				|  |  | +	conversations.value = conversations.value.filter((item) => !multiDeleteConversationModel.value.selectedConversations.includes(item.session_id))
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 创建新对话
 | 
	
		
			
				|  |  | +const { loading: creatingConversation, doLoading: createConversationAndSetItActive } = useLoading(async () => {
 | 
	
		
			
				|  |  | +	// 调用API创建新对话,默认标题为"新对话"
 | 
	
		
			
				|  |  | +	const { id } = await assist.session.add(t('message.assistant.sidebar.createConversation'))
 | 
	
		
			
				|  |  | +	// 刷新对话列表
 | 
	
		
			
				|  |  | +	await doLoadConversations()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	await nextTick()
 | 
	
		
			
				|  |  | +	activeConversationId.value = id
 | 
	
		
			
				|  |  | +	messages.value = []
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 编辑会话状态管理
 | 
	
		
			
				|  |  | +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 { 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 = ''
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +onMounted(() => {
 | 
	
		
			
				|  |  | +	scrollToBottom()
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +//杂项
 | 
	
		
			
				|  |  | +const getUserInfos = ref<{
 | 
	
		
			
				|  |  | +	avatar: string
 | 
	
		
			
				|  |  | +	userName: string
 | 
	
		
			
				|  |  | +}>(Local.get('userInfo') || {})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const canSendMessage = computed(() => {
 | 
	
		
			
				|  |  | +	return (
 | 
	
		
			
				|  |  | +		!inputMessage.value.trim() ||
 | 
	
		
			
				|  |  | +		loadingModels.value ||
 | 
	
		
			
				|  |  | +		loadingPromptList.value ||
 | 
	
		
			
				|  |  | +		loadConversations.value ||
 | 
	
		
			
				|  |  | +		loadingMessage.value ||
 | 
	
		
			
				|  |  | +		loadingUpload.value
 | 
	
		
			
				|  |  | +	)
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const router = useRouter()
 | 
	
		
			
				|  |  | +const redirectToModelManager = () => router.push('model')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 设置面板相关状态
 | 
	
		
			
				|  |  | +const showSettingsPanel = ref(false)
 | 
	
		
			
				|  |  | +const showToolCalls = ref(false)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 收藏消息功能
 | 
	
		
			
				|  |  | +const favoriteMessageIdx = ref(-1)
 | 
	
		
			
				|  |  | +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),
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 收藏面板功能
 | 
	
		
			
				|  |  | +// 搜索功能
 | 
	
		
			
				|  |  | +const handleBookmarkSearch = async () => {
 | 
	
		
			
				|  |  | +	bookmarkOptions.value.pageNum = 1
 | 
	
		
			
				|  |  | +	await doLoadingMessage(-1)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 分页变化
 | 
	
		
			
				|  |  | +const handleBookmarkPageChange = async (page: number) => {
 | 
	
		
			
				|  |  | +	bookmarkOptions.value.pageNum = page
 | 
	
		
			
				|  |  | +	await doLoadingMessage(-1)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 每页数目变化
 | 
	
		
			
				|  |  | +const handleBookmarkPageSizeChange = async (pageSize: number) => {
 | 
	
		
			
				|  |  | +	bookmarkOptions.value.pageSize = pageSize
 | 
	
		
			
				|  |  | +	bookmarkOptions.value.pageNum = 1 // 重置到第一页
 | 
	
		
			
				|  |  | +	await doLoadingMessage(-1)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 重置功能
 | 
	
		
			
				|  |  | +const handleBookmarkReset = async () => {
 | 
	
		
			
				|  |  | +	bookmarkOptions.value.keyWord = ''
 | 
	
		
			
				|  |  | +	bookmarkOptions.value.pageNum = 1
 | 
	
		
			
				|  |  | +	await doLoadingMessage(-1)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 每页数目选项
 | 
	
		
			
				|  |  | +const pageSizeOptions = [3, 10, 20, 30, 50, 100]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 最大页数限制
 | 
	
		
			
				|  |  | +const maxPages = 100
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const exportToMarkDown = (message: Message) => {
 | 
	
		
			
				|  |  | +	// 创建CSV内容
 | 
	
		
			
				|  |  | +	const csvContent = message.render_content
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	download(csvContent, `export_${new Date().getTime()}.md`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 导出对话历史
 | 
	
		
			
				|  |  | +const exportId = ref<number>(-1)
 | 
	
		
			
				|  |  | +const { loading: exportConversationLoading, doLoading: exportConversation } = useLoading(async (id: number) => {
 | 
	
		
			
				|  |  | +	exportId.value = id
 | 
	
		
			
				|  |  | +	const result: {
 | 
	
		
			
				|  |  | +		messages: Message[]
 | 
	
		
			
				|  |  | +		total: number
 | 
	
		
			
				|  |  | +	} = await assist.session.message.list({ sessionId: id }).catch(() => {
 | 
	
		
			
				|  |  | +		return {
 | 
	
		
			
				|  |  | +			list: [],
 | 
	
		
			
				|  |  | +			total: 0,
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	let md = ''
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	for (const resultElement of result.messages) {
 | 
	
		
			
				|  |  | +		switch (resultElement.role) {
 | 
	
		
			
				|  |  | +			case 'user': {
 | 
	
		
			
				|  |  | +				md += '\n\n>' + resultElement.render_content + '\n\n'
 | 
	
		
			
				|  |  | +				continue
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			case 'assistant': {
 | 
	
		
			
				|  |  | +				md += '\n\n' + resultElement.render_content + '\n\n'
 | 
	
		
			
				|  |  | +				continue
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	exportToMarkDown({
 | 
	
		
			
				|  |  | +		id: 0,
 | 
	
		
			
				|  |  | +		role: 'assistant',
 | 
	
		
			
				|  |  | +		render_content: md,
 | 
	
		
			
				|  |  | +		content: md,
 | 
	
		
			
				|  |  | +		timestamp: Date.now(),
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	exportId.value = -1
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const isBlank = (str: string) => {
 | 
	
		
			
				|  |  | +	return str == null || str.trim().length === 0
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 打开文件
 | 
	
		
			
				|  |  | +const openFile = (fullPath: string) => {
 | 
	
		
			
				|  |  | +	window.open(fullPath, '_blank')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 格式化文件大小
 | 
	
		
			
				|  |  | +const formatFileSize = (bytes: number): string => {
 | 
	
		
			
				|  |  | +	if (bytes === 0) return '0 B'
 | 
	
		
			
				|  |  | +	const k = 1024
 | 
	
		
			
				|  |  | +	const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
 | 
	
		
			
				|  |  | +	const i = Math.floor(Math.log(bytes) / Math.log(k))
 | 
	
		
			
				|  |  | +	return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const isSystemLoading = computed(()=> {
 | 
	
		
			
				|  |  | +  return loadConversations.value ||
 | 
	
		
			
				|  |  | +      loadingModels.value ||
 | 
	
		
			
				|  |  | +      loadingPromptList.value ||
 | 
	
		
			
				|  |  | +      loadingEmbedding.value
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +</script>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<template>
 | 
	
		
			
				|  |  | +	<el-container class="chat-container" ref="dropToUploadZone" v-loading="isSystemLoading">
 | 
	
		
			
				|  |  | +		<!-- 左侧会话列表 -->
 | 
	
		
			
				|  |  | +		<el-aside width="300px" class="chat-sidebar">
 | 
	
		
			
				|  |  | +			<div class="sidebar-header">
 | 
	
		
			
				|  |  | +				<h3>{{ t('message.assistant.sidebar.conversationHistory') }}</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 v-if="!multiDeleteConversationModel.visible">
 | 
	
		
			
				|  |  | +							<el-dropdown-item class="settings-item">
 | 
	
		
			
				|  |  | +								<div class="settings-row">
 | 
	
		
			
				|  |  | +									<span class="settings-label">{{ t('message.assistant.settings.autoRecordToolCalls') }}</span>
 | 
	
		
			
				|  |  | +									<el-switch v-model="showToolCalls" size="small" />
 | 
	
		
			
				|  |  | +								</div>
 | 
	
		
			
				|  |  | +							</el-dropdown-item>
 | 
	
		
			
				|  |  | +							<el-dropdown-item @click="redirectToModelManager">
 | 
	
		
			
				|  |  | +								<span class="settings-label">{{ t('message.assistant.settings.modelManagement') }}</span>
 | 
	
		
			
				|  |  | +							</el-dropdown-item>
 | 
	
		
			
				|  |  | +							<el-dropdown-item @click="promptDialogVisible = true">
 | 
	
		
			
				|  |  | +								<span class="settings-label">{{ t('message.assistant.settings.promptManagement') }}</span>
 | 
	
		
			
				|  |  | +							</el-dropdown-item>
 | 
	
		
			
				|  |  | +							<el-dropdown-item @click="multiDeleteConversationModel.visible = true">
 | 
	
		
			
				|  |  | +								<span class="settings-label">{{ t('message.assistant.settings.conversationManagement') }}</span>
 | 
	
		
			
				|  |  | +							</el-dropdown-item>
 | 
	
		
			
				|  |  | +						</el-dropdown-menu>
 | 
	
		
			
				|  |  | +						<el-dropdown-menu v-else>
 | 
	
		
			
				|  |  | +							<el-dropdown-item @click="multiDeleteConversationClear">
 | 
	
		
			
				|  |  | +								<el-icon>
 | 
	
		
			
				|  |  | +									<Close />
 | 
	
		
			
				|  |  | +								</el-icon>
 | 
	
		
			
				|  |  | +								<span>{{ t('message.assistant.settings.cancelSelection') }}</span>
 | 
	
		
			
				|  |  | +							</el-dropdown-item>
 | 
	
		
			
				|  |  | +							<el-dropdown-item @click="startMultidelete">
 | 
	
		
			
				|  |  | +								<el-icon>
 | 
	
		
			
				|  |  | +									<Delete />
 | 
	
		
			
				|  |  | +								</el-icon>
 | 
	
		
			
				|  |  | +								<span>{{ t('message.assistant.settings.deleteSelected') }}</span>
 | 
	
		
			
				|  |  | +							</el-dropdown-item>
 | 
	
		
			
				|  |  | +						</el-dropdown-menu>
 | 
	
		
			
				|  |  | +					</template>
 | 
	
		
			
				|  |  | +				</el-dropdown>
 | 
	
		
			
				|  |  | +			</div>
 | 
	
		
			
				|  |  | +			<el-scrollbar class="conversation-list" v-loading="loadConversations">
 | 
	
		
			
				|  |  | +				<div
 | 
	
		
			
				|  |  | +					v-for="conv in displayConversations"
 | 
	
		
			
				|  |  | +					:key="conv.session_id"
 | 
	
		
			
				|  |  | +					@click="editingConversationId !== conv.session_id && !multiDeleteConversationModel.visible ? 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">
 | 
	
		
			
				|  |  | +						<el-checkbox
 | 
	
		
			
				|  |  | +							v-if="multiDeleteConversationModel.visible && conv.session_id !== -1 && conv.session_id !== activeConversationId"
 | 
	
		
			
				|  |  | +							:model-value="multiDeleteConversationModel.selectedConversations.includes(conv.session_id)"
 | 
	
		
			
				|  |  | +							@change="(enable: boolean) => enable ? multiDeleteConversationModel.selectedConversations.push(conv.session_id) : multiDeleteConversationModel.selectedConversations = multiDeleteConversationModel.selectedConversations.filter((id: number) => id !== conv.session_id)"
 | 
	
		
			
				|  |  | +							:disabled="isConversationActive && activeConversationId === conv.session_id"
 | 
	
		
			
				|  |  | +							@click.stop
 | 
	
		
			
				|  |  | +						/>
 | 
	
		
			
				|  |  | +						<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 && !multiDeleteConversationModel.visible">
 | 
	
		
			
				|  |  | +						<!-- 非编辑状态的三点菜单 -->
 | 
	
		
			
				|  |  | +						<template v-if="editingConversationId !== conv.session_id">
 | 
	
		
			
				|  |  | +							<el-dropdown trigger="click" placement="bottom-end" @click.stop>
 | 
	
		
			
				|  |  | +								<el-button type="primary" size="small" :icon="MoreFilled" class="action-btn more-btn" :title="t('message.assistant.buttons.more')" plain circle @click.stop />
 | 
	
		
			
				|  |  | +								<template #dropdown>
 | 
	
		
			
				|  |  | +									<el-dropdown-menu>
 | 
	
		
			
				|  |  | +										<el-dropdown-item @click="exportConversation(conv.session_id)">
 | 
	
		
			
				|  |  | +											<el-button v-if="conv.session_id === exportId && exportConversationLoading" size="small" loading circle plain />
 | 
	
		
			
				|  |  | +											<el-icon v-else>
 | 
	
		
			
				|  |  | +												<Download />
 | 
	
		
			
				|  |  | +											</el-icon>
 | 
	
		
			
				|  |  | +											<span>{{ t('message.assistant.buttons.export') }}</span>
 | 
	
		
			
				|  |  | +										</el-dropdown-item>
 | 
	
		
			
				|  |  | +										<el-dropdown-item @click="editSummary(conv.session_id)">
 | 
	
		
			
				|  |  | +											<el-icon>
 | 
	
		
			
				|  |  | +												<Edit />
 | 
	
		
			
				|  |  | +											</el-icon>
 | 
	
		
			
				|  |  | +											<span>{{ t('message.assistant.buttons.edit') }}</span>
 | 
	
		
			
				|  |  | +										</el-dropdown-item>
 | 
	
		
			
				|  |  | +										<el-dropdown-item
 | 
	
		
			
				|  |  | +											@click="deleteConversation(conv.session_id)"
 | 
	
		
			
				|  |  | +											:disabled="isConversationActive || (loadingDeleteConversation && currentDeletingConversation == conv.session_id)"
 | 
	
		
			
				|  |  | +										>
 | 
	
		
			
				|  |  | +											<el-icon>
 | 
	
		
			
				|  |  | +												<Delete />
 | 
	
		
			
				|  |  | +											</el-icon>
 | 
	
		
			
				|  |  | +											<span>{{ t('message.assistant.buttons.delete') }}</span>
 | 
	
		
			
				|  |  | +										</el-dropdown-item>
 | 
	
		
			
				|  |  | +									</el-dropdown-menu>
 | 
	
		
			
				|  |  | +								</template>
 | 
	
		
			
				|  |  | +							</el-dropdown>
 | 
	
		
			
				|  |  | +						</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="t('message.assistant.buttons.confirm')"
 | 
	
		
			
				|  |  | +								plain
 | 
	
		
			
				|  |  | +								circle
 | 
	
		
			
				|  |  | +							/>
 | 
	
		
			
				|  |  | +							<el-button
 | 
	
		
			
				|  |  | +								type="info"
 | 
	
		
			
				|  |  | +								size="small"
 | 
	
		
			
				|  |  | +								@click.stop="cancelEdit(conv.session_id)"
 | 
	
		
			
				|  |  | +								class="action-btn cancel-btn"
 | 
	
		
			
				|  |  | +								:title="t('message.assistant.buttons.cancel')"
 | 
	
		
			
				|  |  | +								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"
 | 
	
		
			
				|  |  | +				>{{ t('message.assistant.sidebar.createConversation') }}
 | 
	
		
			
				|  |  | +			</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="!isBlank(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">{{ t('message.assistant.status.aiThinking') }}</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
 | 
	
		
			
				|  |  | +										:loading="favoriteMessageIdx === idx && loadingFavoriteMessage"
 | 
	
		
			
				|  |  | +										@click="exportToMarkDown(message)"
 | 
	
		
			
				|  |  | +										class="favorite-btn"
 | 
	
		
			
				|  |  | +										:disabled="isConversationActive"
 | 
	
		
			
				|  |  | +										:icon="Download"
 | 
	
		
			
				|  |  | +										plain
 | 
	
		
			
				|  |  | +										circle
 | 
	
		
			
				|  |  | +									/>
 | 
	
		
			
				|  |  | +								</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 v-if="message.files !== undefined && message.files.length > 0" class="message-files">
 | 
	
		
			
				|  |  | +										<div
 | 
	
		
			
				|  |  | +											v-for="file in message.files"
 | 
	
		
			
				|  |  | +											:key="file.path"
 | 
	
		
			
				|  |  | +											class="file-item"
 | 
	
		
			
				|  |  | +											@click="openFile(file.full_path)"
 | 
	
		
			
				|  |  | +											:title="t('message.assistant.file.clickToOpen', { name: file.name })"
 | 
	
		
			
				|  |  | +										>
 | 
	
		
			
				|  |  | +											<el-icon class="file-icon">
 | 
	
		
			
				|  |  | +												<Document v-if="file.type.includes('text') || file.type.includes('document')" />
 | 
	
		
			
				|  |  | +												<Picture v-else-if="file.type.includes('image')" />
 | 
	
		
			
				|  |  | +												<VideoPlay v-else-if="file.type.includes('video')" />
 | 
	
		
			
				|  |  | +												<Headset v-else-if="file.type.includes('audio')" />
 | 
	
		
			
				|  |  | +												<Files v-else />
 | 
	
		
			
				|  |  | +											</el-icon>
 | 
	
		
			
				|  |  | +											<span class="file-name">{{ file.name }}</span>
 | 
	
		
			
				|  |  | +											<span class="file-size">{{ formatFileSize(file.size) }}</span>
 | 
	
		
			
				|  |  | +										</div>
 | 
	
		
			
				|  |  | +									</div>
 | 
	
		
			
				|  |  | +								</div>
 | 
	
		
			
				|  |  | +								<div class="user-message-actions">
 | 
	
		
			
				|  |  | +									<el-button
 | 
	
		
			
				|  |  | +										type="primary"
 | 
	
		
			
				|  |  | +										size="small"
 | 
	
		
			
				|  |  | +										@click="replaceMessage(messages.indexOf(message))"
 | 
	
		
			
				|  |  | +										class="retry-btn"
 | 
	
		
			
				|  |  | +										plain
 | 
	
		
			
				|  |  | +										:disabled="isConversationActive"
 | 
	
		
			
				|  |  | +									>
 | 
	
		
			
				|  |  | +										{{ t('message.assistant.buttons.retry') }}
 | 
	
		
			
				|  |  | +									</el-button>
 | 
	
		
			
				|  |  | +								</div>
 | 
	
		
			
				|  |  | +							</div>
 | 
	
		
			
				|  |  | +							<el-avatar class="message-avatar" :src="getUserInfos.avatar" :icon="User" />
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +					</div>
 | 
	
		
			
				|  |  | +				</div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				<!-- 空状态页面 -->
 | 
	
		
			
				|  |  | +				<div v-else class="empty-content">
 | 
	
		
			
				|  |  | +					<!-- 收藏页面空状态 -->
 | 
	
		
			
				|  |  | +					<div v-if="activeConversationId === -1" class="bookmark-empty">
 | 
	
		
			
				|  |  | +						<div class="empty-icon">
 | 
	
		
			
				|  |  | +							<el-icon :size="80" color="#f59e0b">
 | 
	
		
			
				|  |  | +								<StarFilled />
 | 
	
		
			
				|  |  | +							</el-icon>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +						<div class="empty-text">
 | 
	
		
			
				|  |  | +							<h2 class="empty-title">{{ t('message.assistant.empty.noBookmarks') }}</h2>
 | 
	
		
			
				|  |  | +							<p class="empty-description">{{ t('message.assistant.empty.noBookmarksDescription') }}</p>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +						<div class="empty-tips">
 | 
	
		
			
				|  |  | +							<div class="tip-item">
 | 
	
		
			
				|  |  | +								<el-icon color="#409eff">
 | 
	
		
			
				|  |  | +									<InfoFilled />
 | 
	
		
			
				|  |  | +								</el-icon>
 | 
	
		
			
				|  |  | +								<span>{{ t('message.assistant.empty.bookmarkTip1') }}</span>
 | 
	
		
			
				|  |  | +							</div>
 | 
	
		
			
				|  |  | +							<div class="tip-item">
 | 
	
		
			
				|  |  | +								<el-icon color="#67c23a">
 | 
	
		
			
				|  |  | +									<Search />
 | 
	
		
			
				|  |  | +								</el-icon>
 | 
	
		
			
				|  |  | +								<span>{{ t('message.assistant.empty.bookmarkTip2') }}</span>
 | 
	
		
			
				|  |  | +							</div>
 | 
	
		
			
				|  |  | +							<div class="tip-item">
 | 
	
		
			
				|  |  | +								<el-icon color="#e6a23c">
 | 
	
		
			
				|  |  | +									<Collection />
 | 
	
		
			
				|  |  | +								</el-icon>
 | 
	
		
			
				|  |  | +								<span>{{ t('message.assistant.empty.bookmarkTip3') }}</span>
 | 
	
		
			
				|  |  | +							</div>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +					</div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +					<!-- 普通对话空状态 -->
 | 
	
		
			
				|  |  | +					<div v-else class="chat-empty">
 | 
	
		
			
				|  |  | +						<div class="empty-icon">
 | 
	
		
			
				|  |  | +							<el-icon :size="80" color="#d1d5db">
 | 
	
		
			
				|  |  | +								<ChatDotRound />
 | 
	
		
			
				|  |  | +							</el-icon>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +						<div class="empty-text">
 | 
	
		
			
				|  |  | +							<h2 class="empty-title">{{ t('message.assistant.empty.startNewConversation') }}</h2>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +						<div class="example-questions">
 | 
	
		
			
				|  |  | +							<h4>{{ t('message.assistant.empty.tryTheseQuestions') }}</h4>
 | 
	
		
			
				|  |  | +							<div class="question-tags">
 | 
	
		
			
				|  |  | +								<el-tag class="question-tag" @click="inputMessage = t('message.assistant.examples.deviceStatus')" type="info">
 | 
	
		
			
				|  |  | +									{{ t('message.assistant.examples.deviceStatus') }}
 | 
	
		
			
				|  |  | +								</el-tag>
 | 
	
		
			
				|  |  | +								<el-tag class="question-tag" @click="inputMessage = t('message.assistant.examples.userPermissions')" type="success">
 | 
	
		
			
				|  |  | +									{{ t('message.assistant.examples.userPermissions') }}
 | 
	
		
			
				|  |  | +								</el-tag>
 | 
	
		
			
				|  |  | +								<el-tag class="question-tag" @click="inputMessage = t('message.assistant.examples.systemPerformance')" type="warning"> {{ t('message.assistant.examples.systemPerformance') }} </el-tag>
 | 
	
		
			
				|  |  | +							</div>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +					</div>
 | 
	
		
			
				|  |  | +				</div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				<div class="messages-spacer"></div>
 | 
	
		
			
				|  |  | +			</div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			<!-- 附件栏(内嵌到输入框上方) -->
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			<div class="input-container" v-if="activeConversationId !== -1">
 | 
	
		
			
				|  |  | +				<!-- 大输入框容器 -->
 | 
	
		
			
				|  |  | +				<div class="large-input-container">
 | 
	
		
			
				|  |  | +					<!-- 附件栏:紧贴输入框上方,位于容器内部 -->
 | 
	
		
			
				|  |  | +					<div class="attachments-inline">
 | 
	
		
			
				|  |  | +						<el-scrollbar>
 | 
	
		
			
				|  |  | +							<div class="attachments-inline-scroll">
 | 
	
		
			
				|  |  | +								<div
 | 
	
		
			
				|  |  | +									v-for="(file, fIdx) in selectedFiles"
 | 
	
		
			
				|  |  | +									:key="file.name + '_' + file.size"
 | 
	
		
			
				|  |  | +									class="attachment-card"
 | 
	
		
			
				|  |  | +									@click="openFile(file.full_path)"
 | 
	
		
			
				|  |  | +									title=""
 | 
	
		
			
				|  |  | +								>
 | 
	
		
			
				|  |  | +									<button class="control-btn attachment-item" :title="`${file.name}`">
 | 
	
		
			
				|  |  | +										<el-icon :size="10"><CopyDocument /></el-icon>
 | 
	
		
			
				|  |  | +										<span class="attachment-name">{{ file.name }}</span>
 | 
	
		
			
				|  |  | +									</button>
 | 
	
		
			
				|  |  | +									<button class="remove-attachment-icon" @click.stop="removeAttachment(fIdx)">
 | 
	
		
			
				|  |  | +										<el-icon><Close /></el-icon>
 | 
	
		
			
				|  |  | +									</button>
 | 
	
		
			
				|  |  | +								</div>
 | 
	
		
			
				|  |  | +								<!-- 无附件时的提示信息 -->
 | 
	
		
			
				|  |  | +								<div v-if="selectedFiles.length === 0" class="attachment-hint">
 | 
	
		
			
				|  |  | +									<span v-if="!isOverDropZone">{{ t('message.assistant.status.clickToUpload') }}</span>
 | 
	
		
			
				|  |  | +									<span v-else>{{ t('message.assistant.status.uploadProgress') }}</span>
 | 
	
		
			
				|  |  | +								</div>
 | 
	
		
			
				|  |  | +							</div>
 | 
	
		
			
				|  |  | +						</el-scrollbar>
 | 
	
		
			
				|  |  | +						<button class="control-btn add-attachment-btn" @click="open" :disabled="loadingUpload">
 | 
	
		
			
				|  |  | +							<el-icon :size="10">
 | 
	
		
			
				|  |  | +								<Plus v-if="!loadingUpload" />
 | 
	
		
			
				|  |  | +								<div v-else>{{ uploadProgress.toFixed(2) }}</div>
 | 
	
		
			
				|  |  | +							</el-icon>
 | 
	
		
			
				|  |  | +						</button>
 | 
	
		
			
				|  |  | +					</div>
 | 
	
		
			
				|  |  | +					<!-- 输入框 -->
 | 
	
		
			
				|  |  | +					<textarea
 | 
	
		
			
				|  |  | +						v-model="inputMessage"
 | 
	
		
			
				|  |  | +						class="large-textarea"
 | 
	
		
			
				|  |  | +						:placeholder="t('message.assistant.placeholders.inputQuestion')"
 | 
	
		
			
				|  |  | +						@keydown.enter.ctrl="sendMessage"
 | 
	
		
			
				|  |  | +						@keydown.enter.meta="sendMessage"
 | 
	
		
			
				|  |  | +						:disabled="isConversationActive"
 | 
	
		
			
				|  |  | +						rows="3"
 | 
	
		
			
				|  |  | +					></textarea>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +					<!-- 内嵌按钮区域 -->
 | 
	
		
			
				|  |  | +					<div class="embedded-controls">
 | 
	
		
			
				|  |  | +						<!-- 左下角按钮组 -->
 | 
	
		
			
				|  |  | +						<div class="left-controls">
 | 
	
		
			
				|  |  | +							<!-- 模型选择按钮 -->
 | 
	
		
			
				|  |  | +							<el-dropdown trigger="click" placement="top-start" :disabled="loadingModels || modelLabel == t('message.assistant.status.noModelConfigured')">
 | 
	
		
			
				|  |  | +								<button class="control-btn model-btn">
 | 
	
		
			
				|  |  | +									<el-icon>
 | 
	
		
			
				|  |  | +										<Setting v-if="!loadingModels" />
 | 
	
		
			
				|  |  | +										<Loading v-else class="spin" />
 | 
	
		
			
				|  |  | +									</el-icon>
 | 
	
		
			
				|  |  | +									<span>{{ modelLabel }}</span>
 | 
	
		
			
				|  |  | +								</button>
 | 
	
		
			
				|  |  | +								<template #dropdown>
 | 
	
		
			
				|  |  | +									<el-dropdown-menu>
 | 
	
		
			
				|  |  | +										<el-dropdown-item
 | 
	
		
			
				|  |  | +											v-for="item in modelOptions"
 | 
	
		
			
				|  |  | +											:key="item.id"
 | 
	
		
			
				|  |  | +											@click="selectedModel = item.id"
 | 
	
		
			
				|  |  | +											:class="{ 'is-selected': selectedModel === item.id }"
 | 
	
		
			
				|  |  | +										>
 | 
	
		
			
				|  |  | +											{{ item.modelName }}
 | 
	
		
			
				|  |  | +										</el-dropdown-item>
 | 
	
		
			
				|  |  | +									</el-dropdown-menu>
 | 
	
		
			
				|  |  | +								</template>
 | 
	
		
			
				|  |  | +							</el-dropdown>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +							<!-- 词嵌入模型选择按钮 -->
 | 
	
		
			
				|  |  | +							<el-dropdown trigger="click" placement="top-start" :disabled="loadingModels || promptLabel == t('message.assistant.status.noPromptConfigured')">
 | 
	
		
			
				|  |  | +								<button class="control-btn embedding-model-btn">
 | 
	
		
			
				|  |  | +									<el-icon>
 | 
	
		
			
				|  |  | +										<CopyDocument v-if="!loadingPromptList" />
 | 
	
		
			
				|  |  | +										<Loading v-else class="spin" />
 | 
	
		
			
				|  |  | +									</el-icon>
 | 
	
		
			
				|  |  | +									<span>{{ promptLabel }}</span>
 | 
	
		
			
				|  |  | +								</button>
 | 
	
		
			
				|  |  | +								<template #dropdown>
 | 
	
		
			
				|  |  | +									<el-dropdown-menu>
 | 
	
		
			
				|  |  | +										<el-dropdown-item
 | 
	
		
			
				|  |  | +											v-for="item in displayPromptList"
 | 
	
		
			
				|  |  | +											:key="item.id"
 | 
	
		
			
				|  |  | +											@click="selectPromptId = item.id"
 | 
	
		
			
				|  |  | +											:class="{ 'is-selected': selectPromptId === item.id }"
 | 
	
		
			
				|  |  | +										>
 | 
	
		
			
				|  |  | +											{{ item.title }}
 | 
	
		
			
				|  |  | +										</el-dropdown-item>
 | 
	
		
			
				|  |  | +									</el-dropdown-menu>
 | 
	
		
			
				|  |  | +								</template>
 | 
	
		
			
				|  |  | +							</el-dropdown>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +						<!-- 右下角按钮组 -->
 | 
	
		
			
				|  |  | +						<div class="right-controls">
 | 
	
		
			
				|  |  | +							<!-- 清空按钮 -->
 | 
	
		
			
				|  |  | +							<button v-show="messages.length !== 0" class="control-btn clear-btn" @click="clearMessage" :disabled="loadingClearMessage">
 | 
	
		
			
				|  |  | +								<el-icon v-if="loadingClearMessage"><Loading /></el-icon>
 | 
	
		
			
				|  |  | +								<el-icon v-else><Delete /></el-icon>
 | 
	
		
			
				|  |  | +							</button>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +							<!-- 发送按钮 -->
 | 
	
		
			
				|  |  | +							<button v-if="!isConversationActive" class="control-btn send-btn" @click="sendMessage" :disabled="canSendMessage">
 | 
	
		
			
				|  |  | +								<el-icon><Promotion /></el-icon>
 | 
	
		
			
				|  |  | +							</button>
 | 
	
		
			
				|  |  | +							<button v-else class="control-btn stop-btn" @click="stopConversation">
 | 
	
		
			
				|  |  | +								<el-icon><VideoPause /></el-icon>
 | 
	
		
			
				|  |  | +							</button>
 | 
	
		
			
				|  |  | +						</div>
 | 
	
		
			
				|  |  | +					</div>
 | 
	
		
			
				|  |  | +				</div>
 | 
	
		
			
				|  |  | +			</div>
 | 
	
		
			
				|  |  | +			<!-- 收藏面板底部栏 -->
 | 
	
		
			
				|  |  | +			<div class="input-container bookmark-footer" v-else>
 | 
	
		
			
				|  |  | +				<div class="bookmark-controls">
 | 
	
		
			
				|  |  | +					<!-- 搜索框 -->
 | 
	
		
			
				|  |  | +					<div class="bookmark-search">
 | 
	
		
			
				|  |  | +						<el-input
 | 
	
		
			
				|  |  | +							v-model="bookmarkOptions.keyWord"
 | 
	
		
			
				|  |  | +							:placeholder="t('message.assistant.placeholders.searchBookmarks')"
 | 
	
		
			
				|  |  | +							:prefix-icon="Search"
 | 
	
		
			
				|  |  | +							clearable
 | 
	
		
			
				|  |  | +							@keydown.enter="handleBookmarkSearch"
 | 
	
		
			
				|  |  | +							@clear="handleBookmarkReset"
 | 
	
		
			
				|  |  | +							style="width: 300px"
 | 
	
		
			
				|  |  | +						/>
 | 
	
		
			
				|  |  | +						<el-button type="primary" :icon="Search" @click="handleBookmarkSearch" :loading="loadingMessage"> {{ t('message.assistant.buttons.search') }} </el-button>
 | 
	
		
			
				|  |  | +					</div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +					<!-- 分页和重置 -->
 | 
	
		
			
				|  |  | +					<div class="bookmark-pagination">
 | 
	
		
			
				|  |  | +						<el-button @click="handleBookmarkReset" :loading="loadingMessage"> {{ t('message.assistant.buttons.reset') }} </el-button>
 | 
	
		
			
				|  |  | +						<el-pagination
 | 
	
		
			
				|  |  | +							v-model:current-page="bookmarkOptions.pageNum"
 | 
	
		
			
				|  |  | +							v-model:page-size="bookmarkOptions.pageSize"
 | 
	
		
			
				|  |  | +							:total="bookmarkTotal"
 | 
	
		
			
				|  |  | +							:page-sizes="pageSizeOptions"
 | 
	
		
			
				|  |  | +							:pager-count="7"
 | 
	
		
			
				|  |  | +							:max-page="maxPages"
 | 
	
		
			
				|  |  | +							layout="total, sizes, prev, pager, next, jumper"
 | 
	
		
			
				|  |  | +							@current-change="handleBookmarkPageChange"
 | 
	
		
			
				|  |  | +							@size-change="handleBookmarkPageSizeChange"
 | 
	
		
			
				|  |  | +							:disabled="loadingMessage"
 | 
	
		
			
				|  |  | +							small
 | 
	
		
			
				|  |  | +						/>
 | 
	
		
			
				|  |  | +					</div>
 | 
	
		
			
				|  |  | +				</div>
 | 
	
		
			
				|  |  | +			</div>
 | 
	
		
			
				|  |  | +		</el-main>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		<!-- 提示词管理对话框 -->
 | 
	
		
			
				|  |  | +		<el-dialog v-model="promptDialogVisible" :title="t('message.assistant.prompt.management')" width="60%" append-to-body>
 | 
	
		
			
				|  |  | +			<div class="prompt-dialog-content">
 | 
	
		
			
				|  |  | +				<div class="prompt-input-section">
 | 
	
		
			
				|  |  | +					<h4>{{ t('message.assistant.prompt.customPrompt') }}</h4>
 | 
	
		
			
				|  |  | +					<el-input
 | 
	
		
			
				|  |  | +						v-model="customPrompt"
 | 
	
		
			
				|  |  | +						type="textarea"
 | 
	
		
			
				|  |  | +						:autosize="{ minRows: 12, maxRows: 24 }"
 | 
	
		
			
				|  |  | +						:placeholder="t('message.assistant.placeholders.customPrompt')"
 | 
	
		
			
				|  |  | +					/>
 | 
	
		
			
				|  |  | +				</div>
 | 
	
		
			
				|  |  | +			</div>
 | 
	
		
			
				|  |  | +			<template #footer>
 | 
	
		
			
				|  |  | +				<div class="dialog-footer">
 | 
	
		
			
				|  |  | +					<el-button @click="promptDialogVisible = false">{{ t('message.assistant.buttons.cancelDialog') }}</el-button>
 | 
	
		
			
				|  |  | +					<el-button type="primary" @click="promptDialogVisible = false">{{ t('message.assistant.buttons.confirmDialog') }}</el-button>
 | 
	
		
			
				|  |  | +				</div>
 | 
	
		
			
				|  |  | +			</template>
 | 
	
		
			
				|  |  | +		</el-dialog>
 | 
	
		
			
				|  |  | +	</el-container>
 | 
	
		
			
				|  |  | +</template>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<style scoped lang="scss">
 | 
	
		
			
				|  |  | +:deep(.el-icon) {
 | 
	
		
			
				|  |  | +	margin-right: 0 !important;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.chat-container {
 | 
	
		
			
				|  |  | +	height: 100%;
 | 
	
		
			
				|  |  | +	background: var(--el-bg-color-page);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.create-conversation-btn {
 | 
	
		
			
				|  |  | +	margin: 16px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 左侧边栏样式 */
 | 
	
		
			
				|  |  | +.chat-sidebar {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	h3 {
 | 
	
		
			
				|  |  | +		margin: 0;
 | 
	
		
			
				|  |  | +		color: var(--el-text-color-primary);
 | 
	
		
			
				|  |  | +		font-size: 16px;
 | 
	
		
			
				|  |  | +		font-weight: 600;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 设置面板样式 */
 | 
	
		
			
				|  |  | +:deep(.el-dropdown-menu) {
 | 
	
		
			
				|  |  | +	.settings-item {
 | 
	
		
			
				|  |  | +		padding: 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		&:hover {
 | 
	
		
			
				|  |  | +			background: none;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.settings-row {
 | 
	
		
			
				|  |  | +		display: flex;
 | 
	
		
			
				|  |  | +		align-items: center;
 | 
	
		
			
				|  |  | +		justify-content: space-between;
 | 
	
		
			
				|  |  | +		padding: 8px 16px;
 | 
	
		
			
				|  |  | +		min-width: 160px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		&:hover {
 | 
	
		
			
				|  |  | +			background: var(--el-fill-color-light);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.settings-label {
 | 
	
		
			
				|  |  | +		font-size: 14px;
 | 
	
		
			
				|  |  | +		color: var(--el-text-color-primary);
 | 
	
		
			
				|  |  | +		flex: 1;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.conversation-list {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.conversation-content {
 | 
	
		
			
				|  |  | +	flex: 1;
 | 
	
		
			
				|  |  | +	min-width: 0;
 | 
	
		
			
				|  |  | +	cursor: pointer;
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.el-checkbox {
 | 
	
		
			
				|  |  | +		margin-right: 8px;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.conversation-edit-content {
 | 
	
		
			
				|  |  | +	flex: 1;
 | 
	
		
			
				|  |  | +	min-width: 0;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.edit-input {
 | 
	
		
			
				|  |  | +	width: 100%;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	: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;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.conversation-actions {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.more-btn {
 | 
	
		
			
				|  |  | +	&:hover {
 | 
	
		
			
				|  |  | +		background: var(--el-color-primary-light-9) !important;
 | 
	
		
			
				|  |  | +		border-color: var(--el-color-primary-light-7) !important;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 对话历史三点菜单样式 */
 | 
	
		
			
				|  |  | +.conversation-actions {
 | 
	
		
			
				|  |  | +	:deep(.el-dropdown-menu) {
 | 
	
		
			
				|  |  | +		.el-dropdown-menu__item {
 | 
	
		
			
				|  |  | +			display: flex;
 | 
	
		
			
				|  |  | +			align-items: center;
 | 
	
		
			
				|  |  | +			gap: 8px;
 | 
	
		
			
				|  |  | +			padding: 8px 16px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			.el-icon {
 | 
	
		
			
				|  |  | +				font-size: 14px;
 | 
	
		
			
				|  |  | +				color: var(--el-text-color-regular);
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			span {
 | 
	
		
			
				|  |  | +				font-size: 14px;
 | 
	
		
			
				|  |  | +				color: var(--el-text-color-primary);
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			&:hover {
 | 
	
		
			
				|  |  | +				background: var(--el-fill-color-light);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				.el-icon {
 | 
	
		
			
				|  |  | +					color: var(--el-color-primary);
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			&.is-disabled {
 | 
	
		
			
				|  |  | +				.el-icon,
 | 
	
		
			
				|  |  | +				span {
 | 
	
		
			
				|  |  | +					color: var(--el-text-color-disabled);
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 右侧聊天区域样式 */
 | 
	
		
			
				|  |  | +.chat-main {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	flex-direction: column;
 | 
	
		
			
				|  |  | +	padding: 0;
 | 
	
		
			
				|  |  | +	background: var(--el-bg-color-page);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	position: relative;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.messages-container {
 | 
	
		
			
				|  |  | +	flex: 1;
 | 
	
		
			
				|  |  | +	padding: 20px;
 | 
	
		
			
				|  |  | +	overflow-y: auto;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.messages-spacer {
 | 
	
		
			
				|  |  | +	height: 203px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.message-wrapper {
 | 
	
		
			
				|  |  | +	margin-bottom: 20px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	&:last-child {
 | 
	
		
			
				|  |  | +		margin-bottom: 0;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* AI消息样式 */
 | 
	
		
			
				|  |  | +.ai-message-container {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: flex-start;
 | 
	
		
			
				|  |  | +	gap: 12px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.ai-message-content {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.favorite-btn {
 | 
	
		
			
				|  |  | +	width: 16px !important;
 | 
	
		
			
				|  |  | +	height: 16px !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;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	&.favorited {
 | 
	
		
			
				|  |  | +		color: #ff4757;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.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;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 用户消息样式 */
 | 
	
		
			
				|  |  | +.user-message-container {
 | 
	
		
			
				|  |  | +	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%;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.user-message-actions {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	justify-content: flex-end;
 | 
	
		
			
				|  |  | +	opacity: 0;
 | 
	
		
			
				|  |  | +	transition: opacity 0.2s ease;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.user-message-container:hover .user-message-actions {
 | 
	
		
			
				|  |  | +	opacity: 1;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.retry-btn {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 消息气泡通用样式 */
 | 
	
		
			
				|  |  | +.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);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 消息文件样式 */
 | 
	
		
			
				|  |  | +.message-files {
 | 
	
		
			
				|  |  | +	margin-top: 8px;
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	flex-direction: row;
 | 
	
		
			
				|  |  | +	gap: 6px;
 | 
	
		
			
				|  |  | +	flex-wrap: wrap;
 | 
	
		
			
				|  |  | +	overflow: visible;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.file-item {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	gap: 4px;
 | 
	
		
			
				|  |  | +	padding: 4px 6px;
 | 
	
		
			
				|  |  | +	background: rgba(255, 255, 255, 0.1);
 | 
	
		
			
				|  |  | +	border-radius: 4px;
 | 
	
		
			
				|  |  | +	cursor: pointer;
 | 
	
		
			
				|  |  | +	transition: all 0.2s ease;
 | 
	
		
			
				|  |  | +	font-size: 11px;
 | 
	
		
			
				|  |  | +	flex-shrink: 0;
 | 
	
		
			
				|  |  | +	min-width: 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	&:hover {
 | 
	
		
			
				|  |  | +		background: rgba(255, 255, 255, 0.2);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.file-icon {
 | 
	
		
			
				|  |  | +		flex-shrink: 0;
 | 
	
		
			
				|  |  | +		font-size: 12px;
 | 
	
		
			
				|  |  | +		color: rgba(255, 255, 255, 0.8);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.file-name {
 | 
	
		
			
				|  |  | +		max-width: 80px;
 | 
	
		
			
				|  |  | +		overflow: hidden;
 | 
	
		
			
				|  |  | +		text-overflow: ellipsis;
 | 
	
		
			
				|  |  | +		white-space: nowrap;
 | 
	
		
			
				|  |  | +		color: rgba(255, 255, 255, 0.9);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.file-size {
 | 
	
		
			
				|  |  | +		flex-shrink: 0;
 | 
	
		
			
				|  |  | +		color: rgba(255, 255, 255, 0.6);
 | 
	
		
			
				|  |  | +		font-size: 10px;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.message-avatar {
 | 
	
		
			
				|  |  | +	flex-shrink: 0;
 | 
	
		
			
				|  |  | +	margin-top: 2px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 加载动画样式 */
 | 
	
		
			
				|  |  | +.loading-container {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	gap: 12px;
 | 
	
		
			
				|  |  | +	padding: 8px 0;
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-secondary);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.loading-dots {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.dot:nth-child(1) {
 | 
	
		
			
				|  |  | +	animation-delay: -0.32s;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.dot:nth-child(2) {
 | 
	
		
			
				|  |  | +	animation-delay: -0.16s;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.dot:nth-child(3) {
 | 
	
		
			
				|  |  | +	animation-delay: 0s;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +@keyframes loading-bounce {
 | 
	
		
			
				|  |  | +	0%,
 | 
	
		
			
				|  |  | +	80%,
 | 
	
		
			
				|  |  | +	100% {
 | 
	
		
			
				|  |  | +		transform: scale(0.8);
 | 
	
		
			
				|  |  | +		opacity: 0.5;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	40% {
 | 
	
		
			
				|  |  | +		transform: scale(1);
 | 
	
		
			
				|  |  | +		opacity: 1;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.loading-text {
 | 
	
		
			
				|  |  | +	font-size: 14px;
 | 
	
		
			
				|  |  | +	font-weight: 500;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.tools-button {
 | 
	
		
			
				|  |  | +	margin-bottom: 10px;
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-regular);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.message-input {
 | 
	
		
			
				|  |  | +	margin-bottom: 12px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	:deep(.el-textarea__inner) {
 | 
	
		
			
				|  |  | +		border-radius: 8px;
 | 
	
		
			
				|  |  | +		resize: none;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.input-actions {
 | 
	
		
			
				|  |  | +	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);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 新的输入容器样式 */
 | 
	
		
			
				|  |  | +.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: 8px;
 | 
	
		
			
				|  |  | +	padding: 0;
 | 
	
		
			
				|  |  | +	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 附件栏(内嵌)样式 */
 | 
	
		
			
				|  |  | +.attachments-inline {
 | 
	
		
			
				|  |  | +	width: 100%;
 | 
	
		
			
				|  |  | +	padding: 4px;
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	gap: 8px;
 | 
	
		
			
				|  |  | +	background: var(--el-fill-color-extra-light);
 | 
	
		
			
				|  |  | +	border-radius: 8px 8px 0 0;
 | 
	
		
			
				|  |  | +	border-bottom: 1px solid var(--el-border-color-lighter);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.attachments-inline-scroll {
 | 
	
		
			
				|  |  | +	flex: 1;
 | 
	
		
			
				|  |  | +	//overflow-x: auto;
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	gap: 8px;
 | 
	
		
			
				|  |  | +	background: var(--el-fill-color-extra-light);
 | 
	
		
			
				|  |  | +	padding: 6px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.attachment-card {
 | 
	
		
			
				|  |  | +	position: relative;
 | 
	
		
			
				|  |  | +	display: inline-flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	gap: 0;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.attachment-item {
 | 
	
		
			
				|  |  | +	/* 对齐下方控制按钮的尺寸与风格 */
 | 
	
		
			
				|  |  | +	max-width: 260px;
 | 
	
		
			
				|  |  | +	white-space: nowrap;
 | 
	
		
			
				|  |  | +	overflow: hidden;
 | 
	
		
			
				|  |  | +	text-overflow: ellipsis;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	font-size: 10px !important;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	padding: 4px 8px !important;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.attachment-name {
 | 
	
		
			
				|  |  | +	max-width: 200px;
 | 
	
		
			
				|  |  | +	white-space: nowrap;
 | 
	
		
			
				|  |  | +	overflow: hidden;
 | 
	
		
			
				|  |  | +	text-overflow: ellipsis;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.add-attachment-btn {
 | 
	
		
			
				|  |  | +	flex-shrink: 0;
 | 
	
		
			
				|  |  | +	font-size: 10px !important;
 | 
	
		
			
				|  |  | +	margin: 5px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.attachment-hint {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	justify-content: center;
 | 
	
		
			
				|  |  | +	padding: 4px;
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-placeholder);
 | 
	
		
			
				|  |  | +	font-size: 12px;
 | 
	
		
			
				|  |  | +	opacity: 0.8;
 | 
	
		
			
				|  |  | +	height: 21.5px;
 | 
	
		
			
				|  |  | +	line-height: 1;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.remove-attachment-icon {
 | 
	
		
			
				|  |  | +	position: absolute;
 | 
	
		
			
				|  |  | +	top: -5px;
 | 
	
		
			
				|  |  | +	right: -5px;
 | 
	
		
			
				|  |  | +	width: 15px;
 | 
	
		
			
				|  |  | +	height: 15px;
 | 
	
		
			
				|  |  | +	border-radius: 50%;
 | 
	
		
			
				|  |  | +	border: 1px solid var(--el-border-color-light);
 | 
	
		
			
				|  |  | +	background: var(--el-bg-color);
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	justify-content: center;
 | 
	
		
			
				|  |  | +	cursor: pointer;
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-regular);
 | 
	
		
			
				|  |  | +	padding: 0;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.large-input-container {
 | 
	
		
			
				|  |  | +	position: relative;
 | 
	
		
			
				|  |  | +	width: 100%;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.large-textarea {
 | 
	
		
			
				|  |  | +	width: 100%;
 | 
	
		
			
				|  |  | +	min-height: 120px;
 | 
	
		
			
				|  |  | +	padding: 16px 16px 60px 16px;
 | 
	
		
			
				|  |  | +	border: none;
 | 
	
		
			
				|  |  | +	outline: none;
 | 
	
		
			
				|  |  | +	resize: none;
 | 
	
		
			
				|  |  | +	font-size: 14px;
 | 
	
		
			
				|  |  | +	line-height: 1.5;
 | 
	
		
			
				|  |  | +	background: transparent;
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-primary);
 | 
	
		
			
				|  |  | +	border-radius: 8px;
 | 
	
		
			
				|  |  | +	font-family: inherit;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.large-textarea::placeholder {
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-placeholder);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.large-textarea:disabled {
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-disabled);
 | 
	
		
			
				|  |  | +	cursor: not-allowed;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.embedded-controls {
 | 
	
		
			
				|  |  | +	position: absolute;
 | 
	
		
			
				|  |  | +	bottom: 0;
 | 
	
		
			
				|  |  | +	left: 0;
 | 
	
		
			
				|  |  | +	right: 0;
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	justify-content: space-between;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	padding: 12px 16px;
 | 
	
		
			
				|  |  | +	border-top: 1px solid var(--el-border-color-lighter);
 | 
	
		
			
				|  |  | +	background: var(--el-fill-color-extra-light);
 | 
	
		
			
				|  |  | +	border-radius: 0 0 8px 8px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.left-controls,
 | 
	
		
			
				|  |  | +.right-controls {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	gap: 8px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.control-btn {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	gap: 6px;
 | 
	
		
			
				|  |  | +	padding: 6px 12px;
 | 
	
		
			
				|  |  | +	border: 1px solid var(--el-border-color-light);
 | 
	
		
			
				|  |  | +	border-radius: 6px;
 | 
	
		
			
				|  |  | +	background: var(--el-bg-color);
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-regular);
 | 
	
		
			
				|  |  | +	font-size: 12px;
 | 
	
		
			
				|  |  | +	cursor: pointer;
 | 
	
		
			
				|  |  | +	transition: all 0.2s ease;
 | 
	
		
			
				|  |  | +	white-space: nowrap;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.control-btn:hover:not(:disabled) {
 | 
	
		
			
				|  |  | +	background: var(--el-fill-color-light);
 | 
	
		
			
				|  |  | +	border-color: var(--el-color-primary-light-7);
 | 
	
		
			
				|  |  | +	color: var(--el-color-primary);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.control-btn:disabled {
 | 
	
		
			
				|  |  | +	opacity: 0.5;
 | 
	
		
			
				|  |  | +	cursor: not-allowed;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.control-btn .el-icon {
 | 
	
		
			
				|  |  | +	font-size: 14px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 特定按钮样式 */
 | 
	
		
			
				|  |  | +.send-btn {
 | 
	
		
			
				|  |  | +	background: var(--el-color-primary);
 | 
	
		
			
				|  |  | +	color: white;
 | 
	
		
			
				|  |  | +	border-color: var(--el-color-primary);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.send-btn:hover:not(:disabled) {
 | 
	
		
			
				|  |  | +	background: var(--el-color-primary-light-3);
 | 
	
		
			
				|  |  | +	border-color: var(--el-color-primary-light-3);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.stop-btn {
 | 
	
		
			
				|  |  | +	background: var(--el-color-danger);
 | 
	
		
			
				|  |  | +	color: white;
 | 
	
		
			
				|  |  | +	border-color: var(--el-color-danger);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.stop-btn:hover:not(:disabled) {
 | 
	
		
			
				|  |  | +	background: var(--el-color-danger-light-3);
 | 
	
		
			
				|  |  | +	border-color: var(--el-color-danger-light-3);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.clear-btn:hover:not(:disabled) {
 | 
	
		
			
				|  |  | +	background: var(--el-color-warning-light-9);
 | 
	
		
			
				|  |  | +	border-color: var(--el-color-warning-light-7);
 | 
	
		
			
				|  |  | +	color: var(--el-color-warning);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 下拉菜单样式 */
 | 
	
		
			
				|  |  | +:deep(.el-dropdown-menu) {
 | 
	
		
			
				|  |  | +	.el-dropdown-menu__item.is-selected {
 | 
	
		
			
				|  |  | +		background: var(--el-color-primary-light-9);
 | 
	
		
			
				|  |  | +		color: var(--el-color-primary);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 对话框样式 */
 | 
	
		
			
				|  |  | +.dialog-footer {
 | 
	
		
			
				|  |  | +	text-align: right;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 提示词对话框样式 */
 | 
	
		
			
				|  |  | +.prompt-dialog-content {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	flex-direction: column;
 | 
	
		
			
				|  |  | +	gap: 24px;
 | 
	
		
			
				|  |  | +	max-height: 600px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-input-section {
 | 
	
		
			
				|  |  | +	h4 {
 | 
	
		
			
				|  |  | +		margin: 0 0 12px 0;
 | 
	
		
			
				|  |  | +		color: var(--el-text-color-primary);
 | 
	
		
			
				|  |  | +		font-size: 14px;
 | 
	
		
			
				|  |  | +		font-weight: 600;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-list-section {
 | 
	
		
			
				|  |  | +	border-top: 1px solid var(--el-border-color-lighter);
 | 
	
		
			
				|  |  | +	padding-top: 20px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	h4 {
 | 
	
		
			
				|  |  | +		margin: 0 0 16px 0;
 | 
	
		
			
				|  |  | +		color: var(--el-text-color-primary);
 | 
	
		
			
				|  |  | +		font-size: 14px;
 | 
	
		
			
				|  |  | +		font-weight: 600;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-list-header {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	justify-content: space-between;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	margin-bottom: 16px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.prompt-search {
 | 
	
		
			
				|  |  | +		display: flex;
 | 
	
		
			
				|  |  | +		align-items: center;
 | 
	
		
			
				|  |  | +		gap: 8px;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-list-container {
 | 
	
		
			
				|  |  | +	min-height: 200px;
 | 
	
		
			
				|  |  | +	max-height: 300px;
 | 
	
		
			
				|  |  | +	overflow-y: auto;
 | 
	
		
			
				|  |  | +	border: 1px solid var(--el-border-color-light);
 | 
	
		
			
				|  |  | +	border-radius: 6px;
 | 
	
		
			
				|  |  | +	background: var(--el-fill-color-extra-light);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-list-empty {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	justify-content: center;
 | 
	
		
			
				|  |  | +	height: 200px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-list {
 | 
	
		
			
				|  |  | +	padding: 8px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-item {
 | 
	
		
			
				|  |  | +	padding: 12px;
 | 
	
		
			
				|  |  | +	margin-bottom: 8px;
 | 
	
		
			
				|  |  | +	border: 1px solid var(--el-border-color-lighter);
 | 
	
		
			
				|  |  | +	border-radius: 6px;
 | 
	
		
			
				|  |  | +	background: var(--el-bg-color);
 | 
	
		
			
				|  |  | +	cursor: pointer;
 | 
	
		
			
				|  |  | +	transition: all 0.2s ease;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	&:hover {
 | 
	
		
			
				|  |  | +		border-color: var(--el-color-primary-light-7);
 | 
	
		
			
				|  |  | +		background: var(--el-color-primary-light-9);
 | 
	
		
			
				|  |  | +		transform: translateY(-1px);
 | 
	
		
			
				|  |  | +		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	&:last-child {
 | 
	
		
			
				|  |  | +		margin-bottom: 0;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-item-header {
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	justify-content: space-between;
 | 
	
		
			
				|  |  | +	align-items: center;
 | 
	
		
			
				|  |  | +	margin-bottom: 8px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-title {
 | 
	
		
			
				|  |  | +	font-weight: 600;
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-primary);
 | 
	
		
			
				|  |  | +	font-size: 14px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-preview {
 | 
	
		
			
				|  |  | +	color: var(--el-text-color-regular);
 | 
	
		
			
				|  |  | +	font-size: 12px;
 | 
	
		
			
				|  |  | +	line-height: 1.4;
 | 
	
		
			
				|  |  | +	word-break: break-all;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.prompt-pagination {
 | 
	
		
			
				|  |  | +	margin-top: 16px;
 | 
	
		
			
				|  |  | +	display: flex;
 | 
	
		
			
				|  |  | +	justify-content: center;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 响应式调整 */
 | 
	
		
			
				|  |  | +/* 空状态页面样式 */
 | 
	
		
			
				|  |  | +.empty-content {
 | 
	
		
			
				|  |  | +	text-align: center;
 | 
	
		
			
				|  |  | +	max-width: 600px;
 | 
	
		
			
				|  |  | +	width: 100%;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	position: absolute;
 | 
	
		
			
				|  |  | +	left: 50%;
 | 
	
		
			
				|  |  | +	top: 50%;
 | 
	
		
			
				|  |  | +	transform: translate(-50%, -50%);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.empty-icon {
 | 
	
		
			
				|  |  | +	margin-bottom: 24px;
 | 
	
		
			
				|  |  | +	opacity: 0.6;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.empty-text {
 | 
	
		
			
				|  |  | +	margin-bottom: 40px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.empty-title {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.quick-start {
 | 
	
		
			
				|  |  | +	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);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.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;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.step-text {
 | 
	
		
			
				|  |  | +	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;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.question-tags {
 | 
	
		
			
				|  |  | +	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);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 收藏面板底部栏样式 */
 | 
	
		
			
				|  |  | +.bookmark-footer {
 | 
	
		
			
				|  |  | +	border-radius: 8px;
 | 
	
		
			
				|  |  | +	padding: 16px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.bookmark-controls {
 | 
	
		
			
				|  |  | +		display: flex;
 | 
	
		
			
				|  |  | +		justify-content: space-between;
 | 
	
		
			
				|  |  | +		align-items: center;
 | 
	
		
			
				|  |  | +		gap: 16px;
 | 
	
		
			
				|  |  | +		width: 100%;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.bookmark-search {
 | 
	
		
			
				|  |  | +		display: flex;
 | 
	
		
			
				|  |  | +		align-items: center;
 | 
	
		
			
				|  |  | +		gap: 12px;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	.bookmark-pagination {
 | 
	
		
			
				|  |  | +		display: flex;
 | 
	
		
			
				|  |  | +		align-items: center;
 | 
	
		
			
				|  |  | +		gap: 16px;
 | 
	
		
			
				|  |  | +		flex-wrap: wrap;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 响应式调整 */
 | 
	
		
			
				|  |  | +@media (max-width: 768px) {
 | 
	
		
			
				|  |  | +	.bookmark-footer {
 | 
	
		
			
				|  |  | +		.bookmark-controls {
 | 
	
		
			
				|  |  | +			flex-direction: column;
 | 
	
		
			
				|  |  | +			gap: 12px;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		.bookmark-search {
 | 
	
		
			
				|  |  | +			width: 100%;
 | 
	
		
			
				|  |  | +			justify-content: center;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			.el-input {
 | 
	
		
			
				|  |  | +				width: 100% !important;
 | 
	
		
			
				|  |  | +				max-width: 300px;
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		.bookmark-pagination {
 | 
	
		
			
				|  |  | +			width: 100%;
 | 
	
		
			
				|  |  | +			justify-content: center;
 | 
	
		
			
				|  |  | +			flex-direction: column;
 | 
	
		
			
				|  |  | +			gap: 8px;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			.el-pagination {
 | 
	
		
			
				|  |  | +				justify-content: center;
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 无限旋转动画
 | 
	
		
			
				|  |  | +@keyframes spin {
 | 
	
		
			
				|  |  | +	to {
 | 
	
		
			
				|  |  | +		transform: rotate(360deg);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 基础旋转类
 | 
	
		
			
				|  |  | +.spin {
 | 
	
		
			
				|  |  | +	display: inline-block;
 | 
	
		
			
				|  |  | +	animation: spin 1s linear infinite;
 | 
	
		
			
				|  |  | +	transform-origin: center center;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	// 变体:慢速
 | 
	
		
			
				|  |  | +	&-slow {
 | 
	
		
			
				|  |  | +		animation-duration: 3s;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	// 变体:快速
 | 
	
		
			
				|  |  | +	&-fast {
 | 
	
		
			
				|  |  | +		animation-duration: 0.6s;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	// 变体:反向
 | 
	
		
			
				|  |  | +	&-rev {
 | 
	
		
			
				|  |  | +		animation-direction: reverse;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	// 变体:悬停暂停
 | 
	
		
			
				|  |  | +	&-pause:hover {
 | 
	
		
			
				|  |  | +		animation-play-state: paused;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +</style>
 |