Parcourir la source

fix: 修改对话

jiaxing.liao il y a 4 jours
Parent
commit
45a69c9fbd

+ 2 - 2
apps/web/src/components/Chat/ChatInput.vue

@@ -50,8 +50,8 @@
 			</template>
 
 			<!-- 自定义 底部插槽 -->
-			<template #footer>
-				<div v-if="attachments.length" class="attachment-preview">
+			<template v-if="attachments.length" #footer>
+				<div class="attachment-preview">
 					<div v-for="file in attachments" :key="file.id" class="attachment-preview__item">
 						<el-image
 							:src="getImageSrc(file)"

+ 163 - 9
apps/web/src/views/chat/index.vue

@@ -15,7 +15,12 @@
 		<!-- 右侧聊天区域 -->
 		<div class="chat-main">
 			<ChatHeader :title="activeConversationTitle">
-				<template #actions> </template>
+				<template #actions>
+					<el-button v-if="!isPresetMode" @click="shareConversation">
+						<Icon icon="lucide:share-2" />
+						<span>生成链接</span>
+					</el-button>
+				</template>
 			</ChatHeader>
 
 			<MessageList
@@ -49,7 +54,7 @@
 				@cancel="handleCancel"
 			>
 				<template #header>
-					<div class="px-8px py-12px flex items-center gap-12px">
+					<div v-if="!isPresetMode" class="px-8px py-12px flex items-center gap-12px">
 						<div class="flex items-center gap-4px">
 							<!-- <div class="title">知识库:</div> -->
 							<el-select
@@ -76,12 +81,21 @@
 				<template #prefix-extra>
 					<!-- 智能体 -->
 					<el-select
+						v-if="!isPresetMode"
 						class="w-180px"
 						:placeholder="t('pages.chat.selectAgentPlaceholder')"
 						v-model="settingsDraft.agentId"
 						:options="agentOptions"
 						@change="handleAgentSelectChange"
 					/>
+					<!-- 编排 -->
+					<!-- <el-select
+						v-if="!isPresetMode"
+						class="w-180px"
+						:placeholder="t('pages.chat.selectWorkflowPlaceholder')"
+						v-model="settingsDraft"
+						:options="agentOptions"
+					/> -->
 					<!-- 附件 -->
 					<el-badge :value="currentAttachments.length" :hidden="!currentAttachments.length">
 						<el-button
@@ -99,6 +113,7 @@
 				</template>
 				<template #action>
 					<el-select
+						v-if="!isPresetMode"
 						:placeholder="t('pages.chat.selectModelPlaceholder')"
 						placement="top"
 						v-model="settingsDraft.summaryModelId"
@@ -144,6 +159,19 @@
 				}}</el-button>
 			</template>
 		</el-dialog>
+
+		<el-dialog v-model="shareDialogVisible" title="生成访问链接" width="620px">
+			<div class="share-dialog">
+				<div class="share-dialog__tip">
+					通过该链接进入对话时会自动使用当前智能体、模型、知识库等配置,无需再次选择。
+				</div>
+				<el-input v-model="shareUrl" type="textarea" :rows="4" readonly />
+			</div>
+			<template #footer>
+				<el-button @click="shareDialogVisible = false">关闭</el-button>
+				<el-button type="primary" @click="copyShareUrl">复制链接</el-button>
+			</template>
+		</el-dialog>
 	</div>
 
 	<AddKbModal ref="addkbRef" />
@@ -151,6 +179,7 @@
 
 <script setup lang="ts">
 import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
+import { useRoute } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { PictureFilled } from '@element-plus/icons-vue'
 import { useI18n } from '@/composables/useI18n'
@@ -190,9 +219,11 @@ import type {
 } from './types'
 import type { PromptsItemsProps } from 'vue-element-plus-x/types/Prompts'
 import type { WorkflowUploadFile } from '@/features/fileUpload/shared'
+import { Icon } from '@repo/ui'
 
 const { t } = useI18n()
 const { isLoading, cancelRequest, streamChat } = useChatStream()
+const route = useRoute()
 
 const conversations = ref<Conversation[]>([])
 const sessionConfigMap = reactive<Record<string, ChatTargetConfig>>({})
@@ -219,6 +250,10 @@ const activeStreamToken = ref('')
 const chatInputRef = ref<InstanceType<typeof ChatInput>>()
 const agentPromptsItems = ref<PromptsItemsProps[]>()
 const addkbRef = ref<InstanceType<typeof AddKbModal>>()
+const shareDialogVisible = ref(false)
+const shareUrl = ref('')
+const presetConfig = ref<ChatTargetConfig | null>(null)
+const isPresetMode = computed(() => !!presetConfig.value)
 let scrollToBottomPending = false
 
 const scrollToBottom = async () => {
@@ -282,10 +317,17 @@ const getDefaultAgentId = () => {
 }
 
 onMounted(async () => {
+	presetConfig.value = parsePresetConfigFromRoute()
 	await loadChatOptions()
+	if (presetConfig.value) {
+		await initializePresetConversation(presetConfig.value)
+		return
+	}
 	await loadConversations()
 	if (conversations.value.length > 0 && !activeConversationId.value) {
-		await handleSelectConversation(conversations.value?.[0]?.id!)
+		activeConversationId.value = conversations.value?.[0]?.id!
+		await loadConversationMessages(activeConversationId.value)
+		await scrollToBottom()
 	}
 })
 
@@ -331,6 +373,79 @@ const cloneConfig = (config: ChatTargetConfig): ChatTargetConfig => {
 	}
 }
 
+const normalizeConfig = (config: Partial<ChatTargetConfig>): ChatTargetConfig => {
+	const base = createDefaultConfig(config.type || 'agent')
+	return {
+		...base,
+		...config,
+		type: config.type || base.type,
+		knowledgeBaseIds: Array.isArray(config.knowledgeBaseIds) ? config.knowledgeBaseIds : [],
+		knowledgeIds: Array.isArray(config.knowledgeIds) ? config.knowledgeIds : [],
+		summaryModelId: config.summaryModelId || '',
+		disableTitle: config.disableTitle ?? base.disableTitle,
+		enableMemory: config.enableMemory ?? base.enableMemory,
+		agentId: config.agentId || '',
+		agentEnabled: config.agentEnabled ?? base.agentEnabled,
+		webSearchEnabled: config.webSearchEnabled ?? base.webSearchEnabled,
+		images: []
+	}
+}
+
+const parsePresetConfigFromRoute = () => {
+	if (route.query.preset !== '1' || typeof route.query.chatConfig !== 'string') return null
+	try {
+		const rawConfig = JSON.parse(decodeURIComponent(route.query.chatConfig))
+		return normalizeConfig(rawConfig)
+	} catch (error) {
+		console.error('Failed to parse chat preset config', error)
+		ElMessage.error('访问链接参数无效')
+		return null
+	}
+}
+
+const buildShareUrl = () => {
+	const config = normalizeConfig(currentChatConfig.value)
+	const url = new URL(window.location.href)
+	const hashPath = route.path || '/chat'
+	const params = new URLSearchParams()
+	params.set('preset', '1')
+	params.set('chatConfig', encodeURIComponent(JSON.stringify(config)))
+	url.hash = `${hashPath}?${params.toString()}`
+	return url.toString()
+}
+
+const shareConversation = () => {
+	shareUrl.value = buildShareUrl()
+	shareDialogVisible.value = true
+}
+
+const copyShareUrl = async () => {
+	if (!shareUrl.value) return
+	try {
+		await navigator.clipboard.writeText(shareUrl.value)
+		ElMessage.success('链接已复制')
+	} catch {
+		ElMessage.warning('复制失败,请手动复制链接')
+	}
+}
+
+const initializePresetConversation = async (config: ChatTargetConfig) => {
+	syncSettingsDraft(config)
+	await fetchKnowledgeOptions(config.knowledgeBaseIds)
+	await refreshAgentUploadCapability(config.agentId)
+	await loadConversations(true)
+
+	const res = await createSession()
+	if (!res.isSuccess || !res.result) return
+
+	await loadConversations(true)
+	const targetConv = conversations.value.find((c) => c.id === res.result)
+	if (targetConv) {
+		sessionConfigMap[targetConv.id] = cloneConfig(config)
+		await handleSelectConversation(targetConv.id)
+	}
+}
+
 /**
  * 获取指定会话的配置
  * 优先从缓存中获取,其次从会话历史中恢复,最后使用默认配置
@@ -338,6 +453,8 @@ const cloneConfig = (config: ChatTargetConfig): ChatTargetConfig => {
  * @returns 聊天目标配置对象
  */
 const getConversationConfig = (conversationId: string) => {
+	if (presetConfig.value) return cloneConfig(presetConfig.value)
+
 	const cachedConfig = sessionConfigMap[conversationId]
 	if (cachedConfig) return cachedConfig
 
@@ -961,13 +1078,43 @@ const handleSelectConversation = async (bizId: string) => {
 		return
 	}
 
-	const config = getConversationConfig(targetConv.id)
-	syncSettingsDraft(config)
-	await refreshAgentUploadCapability(config.agentId)
+	if (presetConfig.value) {
+		sessionConfigMap[targetConv.id] = cloneConfig(presetConfig.value)
+	}
 	await loadConversationMessages(targetConv.id)
 	await scrollToBottom()
 }
 
+const ensureActiveConversation = async () => {
+	if (activeConversationId.value) {
+		sessionConfigMap[activeConversationId.value] = cloneConfig(currentChatConfig.value)
+		return true
+	}
+
+	try {
+		const config = cloneConfig(currentChatConfig.value)
+		const res = await createSession()
+		if (!res.isSuccess || !res.result) {
+			ElMessage.error(t('common.error.network'))
+			return false
+		}
+		await loadConversations(true)
+		const targetConv = conversations.value.find((c) => c.id === res.result)
+		if (targetConv) {
+			sessionConfigMap[targetConv.id] = config
+			activeConversationId.value = targetConv.id
+			messages.value = []
+			return true
+		}
+		ElMessage.error(t('common.error.network'))
+		return false
+	} catch (error) {
+		console.error(error)
+		ElMessage.error(t('common.error.network'))
+		return false
+	}
+}
+
 /**
  * 处理新建会话事件
  */
@@ -979,7 +1126,9 @@ const handleNewChat = async () => {
 			await loadConversations(true)
 			const targetConv = conversations.value.find((c) => c.id === res.result)
 			if (targetConv) {
-				sessionConfigMap[targetConv.id] = createDefaultConfig(activeTargetType.value)
+				sessionConfigMap[targetConv.id] = presetConfig.value
+					? cloneConfig(presetConfig.value)
+					: cloneConfig(currentChatConfig.value)
 				await handleSelectConversation(targetConv.id)
 				ElMessage.success(t('pages.chat.createSuccess'))
 			} else if (conversations.value.length > 0) {
@@ -1023,7 +1172,9 @@ const handleConvCommand = (command: string | number, bizId: string) => {
 					}
 					await loadConversations(true)
 					if (conversations.value.length > 0 && !activeConversationId.value) {
-						await handleSelectConversation(conversations.value?.[0]?.id!)
+						activeConversationId.value = conversations.value?.[0]?.id!
+						await loadConversationMessages(activeConversationId.value)
+						await scrollToBottom()
 					}
 				} catch (error) {
 					ElMessage.error(t('common.error.network'))
@@ -1092,7 +1243,7 @@ const handleRename = async () => {
  * @param content - 可选的消息内容,若不提供则使用 senderValue
  */
 const handleSend = async (content?: string) => {
-	if (!content || !content.trim() || !activeConversationId.value) {
+	if (!content || !content.trim()) {
 		ElMessage.warning('请输入对话内容')
 		return
 	}
@@ -1102,6 +1253,9 @@ const handleSend = async (content?: string) => {
 		return
 	}
 
+	const hasConversation = await ensureActiveConversation()
+	if (!hasConversation) return
+
 	senderValue.value = ''
 
 	const userMsg = createUserMessageWithAttachments(content)

+ 9 - 3
apps/web/src/views/model/index.vue

@@ -96,7 +96,9 @@
 											<template #dropdown>
 												<el-dropdown-menu>
 													<el-dropdown-item @click="openDetailModel(row.id)">详情</el-dropdown-item>
-													<el-dropdown-item v-permission="'edit'" @click="openEditModel(row.id)">编辑</el-dropdown-item>
+													<el-dropdown-item v-permission="'edit'" @click="openEditModel(row.id)"
+														>编辑</el-dropdown-item
+													>
 													<el-dropdown-item
 														v-if="row.source === 'remote'"
 														v-permission="'edit'"
@@ -111,7 +113,11 @@
 													>
 														<span class="danger-text">删除凭证</span>
 													</el-dropdown-item>
-													<el-dropdown-item v-permission="'del'" @click="deleteModelConfirm(row.id)" divided>
+													<el-dropdown-item
+														v-permission="'del'"
+														@click="deleteModelConfirm(row.id)"
+														divided
+													>
 														<span class="danger-text">删除</span>
 													</el-dropdown-item>
 												</el-dropdown-menu>
@@ -408,7 +414,7 @@ async function runModelCheck(
 async function openDetailModel(id: string) {
 	const res = await aiModel.postModelInfo({ id })
 	if (res.isSuccess) {
-		currentDetailModel.value = res.result
+		currentDetailModel.value = res.result as ModelDetail
 		resetDetailCheckResult()
 		showDetailDialog.value = true
 	}