|
@@ -0,0 +1,989 @@
|
|
|
+<template>
|
|
|
+ <div class="chat-container">
|
|
|
+ <!-- 左侧对话列表 -->
|
|
|
+ <div class="sidebar">
|
|
|
+ <div class="logo">
|
|
|
+ <img src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
|
|
|
+ alt="logo" class="logo-img" />
|
|
|
+ <span class="logo-text">Ant Design X Vue</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Button @click="onAddConversation" type="link" class="new-chat-btn">
|
|
|
+ <PlusOutlined />
|
|
|
+ New Conversation
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Conversations :items="conversationItems" :defaultActiveKey="activeConversation" :style="sidebarStyle"
|
|
|
+ :menu="menuConfig" @activeChange="handleConversationChange" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧聊天区域 -->
|
|
|
+ <div class="chat-content">
|
|
|
+ <div v-if="messages.length > 0" class="messages-container" ref="messageContainer">
|
|
|
+ <Bubble.List
|
|
|
+ :items="bubbleItems"
|
|
|
+ :roles="roles"
|
|
|
+ class="messages-list"
|
|
|
+ key="message-list"
|
|
|
+ >
|
|
|
+ <template #footer v-if="false">
|
|
|
+ <Space :size="token.paddingXXS">
|
|
|
+ <Button type="text" size="small" :icon="h(SyncOutlined)"></Button>
|
|
|
+ <Button type="text" size="small" :icon="h(CopyOutlined)" />
|
|
|
+ </Space>
|
|
|
+ </template>
|
|
|
+ </Bubble.List>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else>
|
|
|
+ <Welcome key="welcome" variant="borderless"
|
|
|
+ icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
|
|
|
+ :title="welcomeTitle.content" :description="welcomeDescription">
|
|
|
+ <template #extra>
|
|
|
+ <Space>
|
|
|
+ <Button>
|
|
|
+ <template #icon>
|
|
|
+ <ShareAltOutlined />
|
|
|
+ </template>
|
|
|
+ </Button>
|
|
|
+ <Button>
|
|
|
+ <template #icon>
|
|
|
+ <EllipsisOutlined />
|
|
|
+ </template>
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </template>
|
|
|
+ </Welcome>
|
|
|
+
|
|
|
+ <Prompts v-if="placeholderPromptsItems.length" title="Do you want?" :items="placeholderPromptsItems"
|
|
|
+ :styles="promptsStyles" @itemClick="onPromptsItemClick" />
|
|
|
+ </div>
|
|
|
+ <!-- 底部输入区域 -->
|
|
|
+ <div class="input-section">
|
|
|
+ <Flex align="center" justify="space-between" style="margin-bottom: 8px;">
|
|
|
+ <Prompts :items="senderPromptsItems" @itemClick="onPromptsItemClick" />
|
|
|
+ <a-select
|
|
|
+ ref="selectRef"
|
|
|
+ v-model:value="selectedModel"
|
|
|
+ style="width: 180px;"
|
|
|
+ :loading="modelLoading"
|
|
|
+ placeholder="选择模型"
|
|
|
+ @change="handleModelChange"
|
|
|
+ @focus="handleFocus"
|
|
|
+ >
|
|
|
+ <a-select-option
|
|
|
+ v-for="option in modelOptions"
|
|
|
+ :key="option.id"
|
|
|
+ :value="option.id"
|
|
|
+ >
|
|
|
+ {{ option.name }}
|
|
|
+ </a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </Flex>
|
|
|
+ <Flex style="max-height: 250px;margin-top: 10px;" align="flex-end">
|
|
|
+ <Sender ref="senderRef" :header="senderHeader" :prefix="badgeNode" :value="inputContent" :loading="loading"
|
|
|
+ @change="handleInputChange" @submit="handleSubmit" />
|
|
|
+ </Flex>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, markRaw, h, onMounted, nextTick, watch, CSSProperties } from 'vue'
|
|
|
+import {
|
|
|
+ Attachments,
|
|
|
+ Bubble,
|
|
|
+ Conversations,
|
|
|
+ Prompts,
|
|
|
+ Sender,
|
|
|
+ Welcome,
|
|
|
+ useXAgent,
|
|
|
+ useXChat,
|
|
|
+ ConversationsProps
|
|
|
+} from 'ant-design-x-vue'
|
|
|
+import {
|
|
|
+ CloudUploadOutlined,
|
|
|
+ CommentOutlined,
|
|
|
+ EllipsisOutlined,
|
|
|
+ FireOutlined,
|
|
|
+ HeartOutlined,
|
|
|
+ PaperClipOutlined,
|
|
|
+ PlusOutlined,
|
|
|
+ ReadOutlined,
|
|
|
+ ShareAltOutlined,
|
|
|
+ SmileOutlined,
|
|
|
+ LinkOutlined,
|
|
|
+ EditOutlined,
|
|
|
+ StopOutlined,
|
|
|
+ DeleteOutlined
|
|
|
+} from '@ant-design/icons-vue'
|
|
|
+import { UserOutlined } from '@ant-design/icons-vue';
|
|
|
+import { Badge, Button, Space, theme, Flex, Typography, message as messageAnt, Avatar, Select } from 'ant-design-vue'
|
|
|
+import MarkdownIt from 'markdown-it'
|
|
|
+import type { SelectProps } from 'ant-design-vue'
|
|
|
+
|
|
|
+const { token } = theme.useToken()
|
|
|
+const openValue = ref(true);
|
|
|
+// 状态管理
|
|
|
+const loading = ref(false)
|
|
|
+const headerOpen = ref(false)
|
|
|
+const inputContent = ref('')
|
|
|
+const attachedFiles = ref([])
|
|
|
+const activeConversation = ref('')
|
|
|
+const API_BASE_URL = 'http://192.168.66.187:8082'
|
|
|
+// 对话列表数据
|
|
|
+const conversationItems = ref([
|
|
|
+ {
|
|
|
+ key: 'chat1',
|
|
|
+ label: 'What is Ant Design X?',
|
|
|
+ }
|
|
|
+])
|
|
|
+/* 对话列表菜单 */
|
|
|
+const menuConfig: ConversationsProps['menu'] = (conversation) => ({
|
|
|
+ items: [
|
|
|
+ {
|
|
|
+ label: 'Operation 1',
|
|
|
+ key: 'operation1',
|
|
|
+ icon: h(EditOutlined),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Operation 2',
|
|
|
+ key: 'operation2',
|
|
|
+ icon: h(StopOutlined),
|
|
|
+ disabled: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Operation 3',
|
|
|
+ key: 'operation3',
|
|
|
+ icon: h(DeleteOutlined),
|
|
|
+ danger: true,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+})
|
|
|
+
|
|
|
+// 聊天相关配置
|
|
|
+const roles = {
|
|
|
+ ai: {
|
|
|
+ placement: 'start',
|
|
|
+ typing: { step: 5, interval: 20 },
|
|
|
+ styles: {
|
|
|
+ content: {
|
|
|
+ borderRadius: '16px',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ local: {
|
|
|
+ placement: 'end',
|
|
|
+ variant: 'shadow',
|
|
|
+ },
|
|
|
+}
|
|
|
+
|
|
|
+// 修改提示词配置,使用 defineComponent 包装
|
|
|
+const placeholderPromptsItems = ref([
|
|
|
+ {
|
|
|
+ key: '1',
|
|
|
+ label: 'Hot Topics',
|
|
|
+ icon: '',//markRaw(FireOutlined),
|
|
|
+ description: 'What are you interested in?',
|
|
|
+ children: [
|
|
|
+ { key: '1-1', description: `What's new in X?` },
|
|
|
+ { key: '1-2', description: `What's AGI?` },
|
|
|
+ { key: '1-3', description: `Where is the doc?` },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '2',
|
|
|
+ label: 'Design Guide',
|
|
|
+ icon: '',//markRaw(ReadOutlined),
|
|
|
+ description: 'How to design a good product?',
|
|
|
+ children: [
|
|
|
+ { key: '2-1', icon: '', description: 'Know the well' },//markRaw(HeartOutlined)
|
|
|
+ { key: '2-2', icon: '', description: 'Set the AI role' },//markRaw(SmileOutlined)
|
|
|
+ { key: '2-3', icon: '', description: 'Express the feeling' },//markRaw(CommentOutlined)
|
|
|
+ ],
|
|
|
+ },
|
|
|
+])
|
|
|
+
|
|
|
+const senderPromptsItems = ref([
|
|
|
+ {
|
|
|
+ key: '1',
|
|
|
+ description: 'Hot Topics',
|
|
|
+ icon: ''//markRaw(FireOutlined),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '2',
|
|
|
+ description: 'Design Guide',
|
|
|
+ icon: '',//markRaw(ReadOutlined),
|
|
|
+ },
|
|
|
+])
|
|
|
+
|
|
|
+// 添加文件类型分类存储
|
|
|
+const imageUrls = ref<string[]>([]);
|
|
|
+const documentUrls = ref<string[]>([]);
|
|
|
+
|
|
|
+const fooAvatar: CSSProperties = {
|
|
|
+ color: '#f56a00',
|
|
|
+ backgroundColor: '#fde3cf',
|
|
|
+};
|
|
|
+
|
|
|
+const barAvatar: CSSProperties = {
|
|
|
+ color: '#fff',
|
|
|
+ backgroundColor: '#87d068',
|
|
|
+};
|
|
|
+
|
|
|
+const hideAvatar: CSSProperties = {
|
|
|
+ visibility: 'hidden',
|
|
|
+};
|
|
|
+
|
|
|
+// 消息处理
|
|
|
+const { messages, onRequest, setMessages } = useXChat({
|
|
|
+ agent: useXAgent({
|
|
|
+ request: async ({ message, attachments }, { onSuccess, onError }) => {
|
|
|
+ try {
|
|
|
+ loading.value = true
|
|
|
+ console.log('messages', messages.value)
|
|
|
+ // 保存当前消息状态
|
|
|
+ const currentMessages = [...messages.value]
|
|
|
+
|
|
|
+ // 构建请求数据
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('message', message)
|
|
|
+
|
|
|
+ // 添加附件
|
|
|
+ if (attachments?.length) {
|
|
|
+ attachments.forEach(file => {
|
|
|
+ formData.append('files', file)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送请求到后端API
|
|
|
+ const response = await fetch('/api/chat', {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('请求失败')
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json()
|
|
|
+ // 确保在成功回调前不会清空消息
|
|
|
+ setMessages(currentMessages)
|
|
|
+ onSuccess(data.message)
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ onError(error)
|
|
|
+ messageAnt.error('发送失败,请重试!')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })[0],
|
|
|
+ initialMessages: [], // 添加初始空数组
|
|
|
+ // 添加配置以防止自动清空消息
|
|
|
+ preserveMessages: true
|
|
|
+})
|
|
|
+
|
|
|
+// 添加消息状态监听
|
|
|
+watch(messages, (newMessages) => {
|
|
|
+ if (newMessages.length === 0 && activeConversation.value) {
|
|
|
+ // 如果消息被清空且存在活动会话,恢复上一次的消息
|
|
|
+ const lastMessages = [...messages.value]
|
|
|
+ if (lastMessages.length > 0) {
|
|
|
+ nextTick(() => {
|
|
|
+ setMessages(lastMessages)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
+// 监听会话切换
|
|
|
+watch(activeConversation, () => {
|
|
|
+ if (activeConversation.value !== undefined) {
|
|
|
+ // 切换会话时清空消息
|
|
|
+ setMessages([])
|
|
|
+ }
|
|
|
+}, { immediate: true })
|
|
|
+
|
|
|
+// 新增会话处理
|
|
|
+const onAddConversation = () => {
|
|
|
+ const newKey = `chat${conversationItems.value.length + 1}`
|
|
|
+ conversationItems.value = [
|
|
|
+ ...conversationItems.value,
|
|
|
+ {
|
|
|
+ key: newKey,
|
|
|
+ label: `新会话 ${conversationItems.value.length + 1}`,
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ activeConversation.value = newKey
|
|
|
+ // 清空当前会话消息
|
|
|
+ setMessages([])
|
|
|
+}
|
|
|
+
|
|
|
+// 创建 markdown-it 实例
|
|
|
+const md = new MarkdownIt({
|
|
|
+ html: true,
|
|
|
+ linkify: true,
|
|
|
+ typographer: true,
|
|
|
+ breaks: true
|
|
|
+})
|
|
|
+
|
|
|
+// 修改消息渲染相关的代码
|
|
|
+const bubbleItems = computed(() => {
|
|
|
+ if (!messages.value?.length) return []
|
|
|
+
|
|
|
+ return messages.value.map((msg, index) => {
|
|
|
+ const baseItem = {
|
|
|
+ key: msg.id || `msg-${index}`,
|
|
|
+ role: msg.role || 'local',
|
|
|
+ status: msg.status || 'success',
|
|
|
+ }
|
|
|
+
|
|
|
+ if (msg.message) {
|
|
|
+ return {
|
|
|
+ ...baseItem,
|
|
|
+ content: h('div', {
|
|
|
+ class: 'markdown-content',
|
|
|
+ innerHTML: md.render(msg.message)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return baseItem
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// 修改模型选择相关的响应式数据
|
|
|
+const selectedModel = ref('')
|
|
|
+const selectedModelInfo = ref({ name: '', type: '' }) // 新增:存储选中模型的完整信息
|
|
|
+const modelOptions = ref([])
|
|
|
+const modelLoading = ref(false)
|
|
|
+
|
|
|
+// 添加获取用户模型偏好的函数
|
|
|
+const fetchUserModelPreference = async () => {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE_URL}/api/user/model_preference`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ user_id: "4",
|
|
|
+ merchant_id: "3",
|
|
|
+ action: "get"
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to fetch user model preference')
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json()
|
|
|
+ if (data.code === 2000 && data.data?.id) {
|
|
|
+ // 设置用户已配置的模型
|
|
|
+
|
|
|
+ console.log('selectedModel', selectedModel.value)
|
|
|
+ // 从 modelOptions 中找到对应的模型信息并设置
|
|
|
+ const selectedOption = modelOptions.value.find(opt => opt.name === data.data.model_name)
|
|
|
+ console.log('selectedOption', selectedOption)
|
|
|
+ if (selectedOption) {
|
|
|
+ selectedModel.value = selectedOption.id
|
|
|
+ selectedModelInfo.value = {
|
|
|
+ name: selectedOption.name,
|
|
|
+ type: selectedOption.type
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果找不到对应的模型,使用第一个可用的模型
|
|
|
+ if (modelOptions.value.length > 0) {
|
|
|
+ const defaultModel = modelOptions.value[0]
|
|
|
+ selectedModel.value = defaultModel.id
|
|
|
+ selectedModelInfo.value = {
|
|
|
+ name: defaultModel.name,
|
|
|
+ type: defaultModel.type
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果没有用户偏好设置,使用第一个模型作为默认值
|
|
|
+ if (modelOptions.value.length > 0) {
|
|
|
+ const defaultModel = modelOptions.value[0]
|
|
|
+ selectedModel.value = defaultModel.id
|
|
|
+ selectedModelInfo.value = {
|
|
|
+ name: defaultModel.name,
|
|
|
+ type: defaultModel.type
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error fetching user model preference:', error)
|
|
|
+ // 如果获取偏好失败,使用第一个可用的模型
|
|
|
+ if (modelOptions.value.length > 0) {
|
|
|
+ const defaultModel = modelOptions.value[0]
|
|
|
+ selectedModel.value = defaultModel.id
|
|
|
+ selectedModelInfo.value = {
|
|
|
+ name: defaultModel.name,
|
|
|
+ type: defaultModel.type
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 修改获取模型列表的函数,在获取完列表后获取用户偏好
|
|
|
+const fetchModelOptions = async () => {
|
|
|
+ try {
|
|
|
+ modelLoading.value = true
|
|
|
+ const response = await fetch(`${API_BASE_URL}/api/models`)
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to fetch models')
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json()
|
|
|
+ if (data.code === 2000) {
|
|
|
+ modelOptions.value = data.data.models
|
|
|
+ // .map(model => ({
|
|
|
+ // value: model.model_name,
|
|
|
+ // label: model.display_name || model.model_name
|
|
|
+ // }))
|
|
|
+
|
|
|
+ // 获取模型列表后,获取用户偏好设置
|
|
|
+ await fetchUserModelPreference()
|
|
|
+
|
|
|
+ // 如果没有用户偏好设置,使用第一个模型作为默认值
|
|
|
+ if (!selectedModel.value && modelOptions.value.length > 0) {
|
|
|
+ selectedModel.value = modelOptions.value[0].value
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error(data.message || 'Failed to fetch models')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error fetching models:', error)
|
|
|
+ messageAnt.error('获取模型列表失败')
|
|
|
+ } finally {
|
|
|
+ modelLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 在组件挂载时获取模型列表
|
|
|
+onMounted(async () => {
|
|
|
+ console.log('Component mounted') // 添加日志
|
|
|
+ await fetchModelOptions()
|
|
|
+})
|
|
|
+
|
|
|
+// 修改模型切换处理函数
|
|
|
+const handleModelChange = async (value: string) => {
|
|
|
+ try {
|
|
|
+ console.log('Model changed to:', value)
|
|
|
+
|
|
|
+ // 从选项中获取完整的模型信息
|
|
|
+ const selectedOption = modelOptions.value.find(opt => opt.id === value)
|
|
|
+ if (selectedOption) {
|
|
|
+ selectedModel.value = value
|
|
|
+ selectedModelInfo.value = {
|
|
|
+ name: selectedOption.name,
|
|
|
+ type: selectedOption.type
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(`${API_BASE_URL}/api/user/model_preference`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ user_id: "4",
|
|
|
+ merchant_id: "3",
|
|
|
+ model_type: selectedModelInfo.value.type,
|
|
|
+ model_name: selectedModelInfo.value.name,
|
|
|
+ action: "set"
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to update model preference')
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json()
|
|
|
+ if (data.code === 2000) {
|
|
|
+ messageAnt.success('模型偏好设置已更新')
|
|
|
+ } else {
|
|
|
+ throw new Error(data.message || 'Failed to update model preference')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error updating model preference:', error)
|
|
|
+ messageAnt.error('更新模型偏好设置失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理焦点事件
|
|
|
+const handleFocus = () => {
|
|
|
+ console.log('Select focused')
|
|
|
+}
|
|
|
+
|
|
|
+// 修改消息处理部分
|
|
|
+const handleSubmit = async () => {
|
|
|
+ const content = inputContent.value.trim()
|
|
|
+ if (!content && !items.value.length) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ loading.value = true
|
|
|
+ const userMessageId = `user-${Date.now()}`
|
|
|
+
|
|
|
+ // 保存当前消息状态
|
|
|
+ const currentMessages = [...messages.value]
|
|
|
+
|
|
|
+ // 添加用户消息
|
|
|
+ const newMessages = [...currentMessages, {
|
|
|
+ id: userMessageId,
|
|
|
+ role: 'local',
|
|
|
+ message: content,
|
|
|
+ status: 'success'
|
|
|
+ }]
|
|
|
+
|
|
|
+ // 更新消息数组
|
|
|
+ setMessages(newMessages)
|
|
|
+
|
|
|
+ // 清理输入
|
|
|
+ await nextTick(() => {
|
|
|
+ inputContent.value = ''
|
|
|
+ items.value = []
|
|
|
+ imageUrls.value = []
|
|
|
+ documentUrls.value = []
|
|
|
+ })
|
|
|
+
|
|
|
+ // API 请求
|
|
|
+ const response = await fetch(`${API_BASE_URL}/api/chat/online/multimodal`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ message: content,
|
|
|
+ chat_config_id: "17",
|
|
|
+ user_id: 4,
|
|
|
+ session_id: activeConversation.value,
|
|
|
+ source: "pc",
|
|
|
+ image_urls: imageUrls.value,
|
|
|
+ documents: documentUrls.value,
|
|
|
+ merchant_id: "3",
|
|
|
+ model_type: selectedModelInfo.value.type,
|
|
|
+ model_name: selectedModelInfo.value.name
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`API request failed: ${response.status}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await response.json()
|
|
|
+ if (result.code !== 2000) {
|
|
|
+ throw new Error(`API returned error code: ${result.code}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ activeConversation.value = result.session_id
|
|
|
+ if (result.choices?.[0]?.message?.content) {
|
|
|
+ const aiMessageId = `ai-${Date.now()}`
|
|
|
+ const fullMessage = result.choices[0].message.content
|
|
|
+
|
|
|
+ // 使用最新的消息数组
|
|
|
+ const latestMessages = [...messages.value]
|
|
|
+ const updatedMessages = [...latestMessages, {
|
|
|
+ id: aiMessageId,
|
|
|
+ role: 'ai',
|
|
|
+ message: '',
|
|
|
+ status: 'loading'
|
|
|
+ }]
|
|
|
+ setMessages(updatedMessages)
|
|
|
+
|
|
|
+ // 逐字显示
|
|
|
+ let currentText = ''
|
|
|
+ const chars = fullMessage.split('')
|
|
|
+
|
|
|
+ for (let i = 0; i < chars.length; i++) {
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 50))
|
|
|
+ currentText += chars[i]
|
|
|
+
|
|
|
+ const progressMessages = messages.value.map(msg =>
|
|
|
+ msg.id === aiMessageId
|
|
|
+ ? { ...msg, message: currentText }
|
|
|
+ : msg
|
|
|
+ )
|
|
|
+ setMessages(progressMessages)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保最终状态使用最新的消息数组
|
|
|
+ const finalMessages = messages.value.map(msg =>
|
|
|
+ msg.id === aiMessageId
|
|
|
+ ? { ...msg, message: fullMessage, status: 'success' }
|
|
|
+ : msg
|
|
|
+ )
|
|
|
+ setMessages(finalMessages)
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error sending message:', error)
|
|
|
+ messageAnt.error('发送失败,请重试!')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 添加文件上传函数
|
|
|
+const uploadFile = async (file: File) => {
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('file', file)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch('http://your-api/upload/file', {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Upload failed')
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await response.json()
|
|
|
+
|
|
|
+ // 根据文件类型分类存储URL
|
|
|
+ const fileUrl = result.data.fileUrl
|
|
|
+ if (file.type.startsWith('image/')) {
|
|
|
+ imageUrls.value.push(fileUrl)
|
|
|
+ } else {
|
|
|
+ documentUrls.value.push(fileUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ return fileUrl
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Upload error:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 修改会话切换处理函数
|
|
|
+const handleConversationChange = (key: string) => {
|
|
|
+ if (key) {
|
|
|
+ activeConversation.value = key
|
|
|
+ // 只在必要时清空消息
|
|
|
+ if (!messages.value.length) {
|
|
|
+ setMessages([])
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 方法定义
|
|
|
+const onPromptsItemClick = async (info: any) => {
|
|
|
+ if (info?.data?.description) {
|
|
|
+ // 设置输入内容为选中的提示词
|
|
|
+ inputContent.value = info.data.description
|
|
|
+ // 调用 handleSubmit 发送请求
|
|
|
+ await handleSubmit()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 添加文件处理函数
|
|
|
+const handleFileChange = (info: any) => {
|
|
|
+ const { fileList } = info
|
|
|
+ attachedFiles.value = fileList.map((file: any) => ({
|
|
|
+ ...file,
|
|
|
+ status: file.status,
|
|
|
+ url: file.response?.url || file.url
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 根据文件类型分类存储URL
|
|
|
+ fileList.forEach((file: any) => {
|
|
|
+ const fileUrl = file.response?.url || file.url
|
|
|
+ if (fileUrl) {
|
|
|
+ if (file.type?.startsWith('image/')) {
|
|
|
+ if (!imageUrls.value.includes(fileUrl)) {
|
|
|
+ imageUrls.value.push(fileUrl)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (!documentUrls.value.includes(fileUrl)) {
|
|
|
+ documentUrls.value.push(fileUrl)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const sidebarStyle = computed(() => ({
|
|
|
+ width: '255px',
|
|
|
+ background: token.value.colorBgContainer,
|
|
|
+ borderRadius: token.value.borderRadius,
|
|
|
+}))
|
|
|
+
|
|
|
+// 修改 attachmentsNode 计算属性
|
|
|
+const attachmentsNode = computed(() =>
|
|
|
+ h(Badge,
|
|
|
+ { dot: attachedFiles.value.length > 0 && !headerOpen.value },
|
|
|
+ {
|
|
|
+ default: () => h(Button, {
|
|
|
+ type: 'text',
|
|
|
+ icon: '',//markRaw(PaperClipOutlined),
|
|
|
+ onClick: () => headerOpen.value = !headerOpen.value
|
|
|
+ })
|
|
|
+ }
|
|
|
+ )
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+// 添加欢迎页面的计算属性
|
|
|
+const welcomeTitle = computed(() => ({
|
|
|
+ content: 'Hello, I\'m Ant Design X',
|
|
|
+ style: {
|
|
|
+ fontSize: '24px',
|
|
|
+ fontWeight: 'bold',
|
|
|
+ marginBottom: '8px'
|
|
|
+ }
|
|
|
+}))
|
|
|
+
|
|
|
+const welcomeDescription = ref('Base on Ant Design, AGI product interface solution, create a better intelligent vision~')
|
|
|
+
|
|
|
+const [message, contextHolder] = messageAnt.useMessage();
|
|
|
+
|
|
|
+// 在组件挂载后再进行相关操作
|
|
|
+onMounted(() => {
|
|
|
+ // 如果需要在挂载后执行一些初始化逻辑
|
|
|
+ nextTick(() => {
|
|
|
+ // 可以在这里进行一些初始化
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// 处理函数
|
|
|
+const openChange = (value: boolean) => {
|
|
|
+ nextTick(() => { // 使用 nextTick 确保状态更新后再触发渲染
|
|
|
+ openValue.value = value
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const triggerOpen = () => {
|
|
|
+ nextTick(() => { // 使用 nextTick 确保状态更新后再触发渲染
|
|
|
+ openValue.value = false
|
|
|
+ })
|
|
|
+ console.log(openValue.value)
|
|
|
+}
|
|
|
+
|
|
|
+const senderSubmit = () => {
|
|
|
+ message.success('Send message successfully!');
|
|
|
+}
|
|
|
+
|
|
|
+const selectFileClick = () => {
|
|
|
+ message.info('Mock select file');
|
|
|
+}
|
|
|
+
|
|
|
+// 确保所有响应式数据都有初始值
|
|
|
+const promptsStyles = ref({})
|
|
|
+const open = ref(false);
|
|
|
+const items = ref([]);
|
|
|
+const text = ref('');
|
|
|
+
|
|
|
+const senderRef = ref<InstanceType<typeof Sender>>(null);
|
|
|
+
|
|
|
+const selectRef = ref()
|
|
|
+
|
|
|
+const senderHeader = computed(() =>
|
|
|
+ h(Sender.Header, {
|
|
|
+ title: "Attachments",
|
|
|
+ styles: {
|
|
|
+ content: {
|
|
|
+ padding: 0,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ open: open.value,
|
|
|
+ onOpenChange: (v: boolean) => open.value = v,
|
|
|
+ forceRender: true
|
|
|
+ }, {
|
|
|
+ default: () => h(Attachments, {
|
|
|
+ action: `${API_BASE_URL}/chatbot/upload/file`,
|
|
|
+ name: 'file',
|
|
|
+ items: attachedFiles.value,
|
|
|
+ onChange: handleFileChange,
|
|
|
+ customRequest: ({ file, onSuccess, onError }) => {
|
|
|
+ // 确保文件对象存在
|
|
|
+ const fileObj = file.originFileObj || file
|
|
|
+ if (fileObj) {
|
|
|
+ uploadFile(fileObj)
|
|
|
+ .then(fileUrl => {
|
|
|
+ console.log('Upload successful:', fileUrl)
|
|
|
+ onSuccess({ url: fileUrl }, new XMLHttpRequest())
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ console.error('Upload failed:', err)
|
|
|
+ onError(err)
|
|
|
+ messageAnt.error('文件上传失败,请重试!')
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ placeholder: (type: string) => type === 'drop'
|
|
|
+ ? { title: 'Drop file here' }
|
|
|
+ : {
|
|
|
+ icon: h(CloudUploadOutlined),
|
|
|
+ title: 'Upload files',
|
|
|
+ description: 'Click or drag files to this area to upload',
|
|
|
+ },
|
|
|
+ getDropContainer: () => senderRef.value?.nativeElement
|
|
|
+ })
|
|
|
+ })
|
|
|
+)
|
|
|
+
|
|
|
+const badgeNode = computed(() =>
|
|
|
+ h(Badge, { dot: items.value.length > 0 && !open.value }, {
|
|
|
+ default: () => h(Button, {
|
|
|
+ onClick: () => open.value = !open.value,
|
|
|
+ icon: h(LinkOutlined)
|
|
|
+ })
|
|
|
+ })
|
|
|
+);
|
|
|
+
|
|
|
+// 处理输入变化
|
|
|
+const handleInputChange = (value: string) => {
|
|
|
+ inputContent.value = value
|
|
|
+}
|
|
|
+
|
|
|
+// 添加清理文件的函数
|
|
|
+const clearFiles = () => {
|
|
|
+ attachedFiles.value = []
|
|
|
+ imageUrls.value = []
|
|
|
+ documentUrls.value = []
|
|
|
+}
|
|
|
+
|
|
|
+// 在 script setup 中添加滚动逻辑
|
|
|
+const scrollToBottom = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ const container = document.querySelector('.messages-container')
|
|
|
+ if (container) {
|
|
|
+ container.scrollTop = container.scrollHeight
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 监听消息变化,自动滚动到底部
|
|
|
+watch(messages, () => {
|
|
|
+ scrollToBottom()
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.chat-container {
|
|
|
+ display: flex;
|
|
|
+ height: calc(100vh - 200px);
|
|
|
+ min-width: 1000px;
|
|
|
+ background: var(--ant-color-bg-container);
|
|
|
+ font-family: AlibabaPuHuiTi, -apple-system, sans-serif;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar {
|
|
|
+ width: 280px;
|
|
|
+ background: var(--ant-color-bg-layout-80);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border-right: 1px solid rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+.logo {
|
|
|
+ display: flex;
|
|
|
+ height: 72px;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.logo-img {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.logo-text {
|
|
|
+ margin-left: 8px;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.new-chat-btn {
|
|
|
+ margin: 0 12px 24px;
|
|
|
+ background: #1677ff0f;
|
|
|
+ border: 1px solid #1677ff34;
|
|
|
+}
|
|
|
+
|
|
|
+.chat-content {
|
|
|
+ flex: 1;
|
|
|
+ max-width: 700px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 24px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height:calc(100vh - 50px);
|
|
|
+ overflow: hidden; /* 防止出现双滚动条 */
|
|
|
+}
|
|
|
+
|
|
|
+.messages-container {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 24px 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: calc(100vh - 200px); /* 调整高度,减去其他元素的高度 */
|
|
|
+ scroll-behavior: smooth; /* 添加平滑滚动效果 */
|
|
|
+}
|
|
|
+
|
|
|
+.messages-list {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ margin-bottom: auto; /* 确保内容从顶部开始 */
|
|
|
+}
|
|
|
+
|
|
|
+.input-section {
|
|
|
+ position: sticky;
|
|
|
+ bottom: 0;
|
|
|
+ background: var(--ant-color-bg-container);
|
|
|
+ padding: 16px 0;
|
|
|
+ margin-top: auto;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.welcome-container {
|
|
|
+ padding-top: 32px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加 Markdown 样式 */
|
|
|
+.markdown-content {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
+}
|
|
|
+
|
|
|
+.markdown-content h1,
|
|
|
+.markdown-content h2,
|
|
|
+.markdown-content h3,
|
|
|
+.markdown-content h4 {
|
|
|
+ margin-top: 1em;
|
|
|
+ margin-bottom: 0.5em;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.markdown-content code {
|
|
|
+ background-color: rgba(0, 0, 0, 0.05);
|
|
|
+ padding: 0.2em 0.4em;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.markdown-content pre {
|
|
|
+ background-color: rgba(0, 0, 0, 0.05);
|
|
|
+ padding: 1em;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow-x: auto;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .markdown-content p {
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加模型选择器样式 */
|
|
|
+.model-selector {
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+</style>
|