|
|
@@ -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)
|