diff --git a/.env.development b/.env.development index 4214b50..2cde04e 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,5 @@ # VITE_APP_URL = http://192.168.31.82:8771 -VITE_APP_URL = http://18.167.251.121:10015 +# VITE_APP_URL = http://18.167.251.121:10015 # VITE_APP_URL = http://192.168.31.118:8080 +VITE_APP_URL = http://192.168.31.82:8755 VITE_GOOGLE_CLIENT_ID = 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com diff --git a/src/api/agent.ts b/src/api/agent.ts index d233a5f..ac409b4 100644 --- a/src/api/agent.ts +++ b/src/api/agent.ts @@ -3,8 +3,8 @@ import request from '@/utils/request' // 对话 export interface AgentParamsType { message: string // 消息 - projectID: string // - versionID?: string // + projectID: string // + versionID?: string // imageUrlList?: string[] // 图片URL列表 configParams: Record // 其他配置参数 token: string @@ -13,7 +13,17 @@ export const fetchAgentReply = (data: AgentParamsType): Promise = return request({ url: '/api/ai-design/chat', method: 'get', - data, + data, meta: { responseAll: true } }) } + +export interface CreateProjectParamsType { + type: string + region: string + style: string + temperature: number | string +} +export const createProject = (data: CreateProjectParamsType): Promise => { + return request({ url: '/api/project/init', method: 'post', data }) +} diff --git a/src/assets/images/sender.png b/src/assets/images/sender.png index 0579e40..5bc468f 100644 Binary files a/src/assets/images/sender.png and b/src/assets/images/sender.png differ diff --git a/src/stores/index.ts b/src/stores/index.ts index b5d3fe1..897bb4c 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -8,3 +8,4 @@ export default store export * from './global' export * from './userInfo' export * from './projectData' +export * from './agent' \ No newline at end of file diff --git a/src/utils/request.ts b/src/utils/request.ts index abd9872..5eac85f 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -5,25 +5,25 @@ import { ElMessage } from 'element-plus' // 扩展 AxiosRequestConfig 接口 declare module 'axios' { - interface AxiosRequestConfig { - loading?: boolean - loadingDom?: any - repeatRequest?: boolean - meta?: { - responseAll?: boolean - } - } + interface AxiosRequestConfig { + loading?: boolean + loadingDom?: any + repeatRequest?: boolean + meta?: { + responseAll?: boolean + } + } } // 创建axios实例 // console.log(import.meta.env,123) const service = axios.create({ - // baseURL: import.meta.env.VITE_APP_URL, // api的base_url - timeout: 60000 // 请求超时时间 + // baseURL: import.meta.env.VITE_APP_URL, // api的base_url + timeout: 60000 // 请求超时时间 }) if (import.meta.env.MODE != 'development') { - service.defaults.baseURL = import.meta.env.VITE_APP_URL + service.defaults.baseURL = import.meta.env.VITE_APP_URL } axios.defaults.headers.post['Content-Type'] = 'application/json' axios.defaults.headers.post['lang'] = 'en' //配置语言请求头 @@ -31,97 +31,98 @@ axios.defaults.withCredentials = true //跨域携带cookie // request拦截器 service.interceptors.request.use( - (config: any) => { - removePending(config) - // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求 - !config.repeatRequest && addPending(config) - // 打开loading - if (config.loading) { - LoadingInstance._count++ - if (LoadingInstance._count === 1) { - openLoading(config.loadingDom) - } - } - // 如果登录了,有token,则请求携带token - // Do something before request is sent - const token = useUserInfoStore().state.token - if (token) { - config.headers.Authorization = token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 - // config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 - } - return config - }, - (error) => { - // Do something with request error - console.log(error) // for debug - Promise.reject(error) - } + (config: any) => { + removePending(config) + // 如果repeatRequest不配置,那么默认该请求就取消重复接口请求 + !config.repeatRequest && addPending(config) + // 打开loading + if (config.loading) { + LoadingInstance._count++ + if (LoadingInstance._count === 1) { + openLoading(config.loadingDom) + } + } + // 如果登录了,有token,则请求携带token + // Do something before request is sent + const token = useUserInfoStore().state.token + if (token) { + config.headers.Authorization = 'Bearer ' + token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 + // config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 + } + return config + }, + (error) => { + // Do something with request error + console.log(error) // for debug + Promise.reject(error) + } ) // respone拦截器 service.interceptors.response.use( - // response => response, - /** - * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 - * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 - */ - (response: any) => { - // 如果是llm/streamChat这样的流式接口,不走这样的处理 - if (response.config.url.includes('llm/streamChat')) { - return response - } - - // 已完成请求的删除请求中数组 - removePending(response.config) - // 关闭loading - if (response.config.loading) { - closeLoading() - } - const res = response.data - // 处理异常的情况 - // console.log(res) - if (res.code != 200) { - ElMessage.error(res.message) - return Promise.reject(new Error(res.errMsg || res.message || 'error')) - } else { - // 默认只返回data,不返回状态码和message - // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data) - const isbackAll = response.config.meta && response.config.meta.responseAll - if (isbackAll) { - return res - } else { - return res.data - } - } - }, - (error) => { - if(error?.response){ - if (error.config?.loading) closeLoading() // 关闭loading - if(error?.response?.status === 401){//如果是记录浏览器页面就不跳转login - // showConfirmDialog({ - // title: '确定登出', - // message: '你已被登出,可以取消继续留在该页面,或者重新登录', - // confirmButtonText: '重新登录', - // cancelButtonText: '取消' - // }).then(() => { - // store.loginOut().then(() => { - // location.reload() // 为了重新实例化vue-router对象 避免bug - // }) - // }) - // showToast({ - // message: 'Please log in and try again.', - // duration: 5000 - // }) - // router.push('/login') - // useGenerateStore().clearGenerateData() - return Promise.reject(false) + // response => response, + /** + * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 + * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 + */ + (response: any) => { + // 如果是llm/streamChat这样的流式接口,不走这样的处理 + if (response.config.url.includes('llm/streamChat')) { + return response } - error.config && removePending(error.config) - console.log('err' + error) // for debug - ElMessage.error(error.message) + + // 已完成请求的删除请求中数组 + removePending(response.config) + // 关闭loading + if (response.config.loading) { + closeLoading() + } + const res = response.data + // 处理异常的情况 + // console.log(res) + if (res.code != 200) { + ElMessage.error(res.message) + return Promise.reject(new Error(res.errMsg || res.message || 'error')) + } else { + // 默认只返回data,不返回状态码和message + // 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data) + const isbackAll = response.config.meta && response.config.meta.responseAll + if (isbackAll) { + return res + } else { + return res.data + } + } + }, + (error) => { + if (error?.response) { + if (error.config?.loading) closeLoading() // 关闭loading + if (error?.response?.status === 401) { + //如果是记录浏览器页面就不跳转login + // showConfirmDialog({ + // title: '确定登出', + // message: '你已被登出,可以取消继续留在该页面,或者重新登录', + // confirmButtonText: '重新登录', + // cancelButtonText: '取消' + // }).then(() => { + // store.loginOut().then(() => { + // location.reload() // 为了重新实例化vue-router对象 避免bug + // }) + // }) + // showToast({ + // message: 'Please log in and try again.', + // duration: 5000 + // }) + // router.push('/login') + // useGenerateStore().clearGenerateData() + return Promise.reject(false) + } + error.config && removePending(error.config) + console.log('err' + error) // for debug + ElMessage.error(error.message) + } + return Promise.reject(error) } - return Promise.reject(error) - } ) // --------------------------------取消接口重复请求的函数----------------------------------- @@ -133,10 +134,10 @@ const pendingMap = new Map() * @returns string */ function getPendingKey(config: any) { - const { url, method, params } = config - let { data } = config - if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象 - return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&') + const { url, method, params } = config + let { data } = config + if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象 + return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&') } /** @@ -144,39 +145,39 @@ function getPendingKey(config: any) { * @param {*} config */ function addPending(config: any) { - const pendingKey = getPendingKey(config) - config.cancelToken = - config.cancelToken || - new axios.CancelToken((cancel) => { - if (!pendingMap.has(pendingKey)) { - pendingMap.set(pendingKey, cancel) - } - }) + const pendingKey = getPendingKey(config) + config.cancelToken = + config.cancelToken || + new axios.CancelToken((cancel) => { + if (!pendingMap.has(pendingKey)) { + pendingMap.set(pendingKey, cancel) + } + }) } /** * 删除重复的请求 * @param {*} config */ function removePending(config: any) { - const pendingKey = getPendingKey(config) - if (pendingMap.has(pendingKey)) { - const cancelToken = pendingMap.get(pendingKey) - cancelToken(pendingKey) - pendingMap.delete(pendingKey) - } + const pendingKey = getPendingKey(config) + if (pendingMap.has(pendingKey)) { + const cancelToken = pendingMap.get(pendingKey) + cancelToken(pendingKey) + pendingMap.delete(pendingKey) + } } // ----------------------------------loading的函数------------------------------- const LoadingInstance: { _count: number } = { - _count: 0 + _count: 0 } function openLoading(loadingDom: any) { - useGlobalStore().setLoading(true) + useGlobalStore().setLoading(true) } function closeLoading() { - if (LoadingInstance._count > 0) LoadingInstance._count-- - if (LoadingInstance._count === 0) { - useGlobalStore().setLoading(false) - } + if (LoadingInstance._count > 0) LoadingInstance._count-- + if (LoadingInstance._count === 0) { + useGlobalStore().setLoading(false) + } } export default service diff --git a/src/views/home/agent/components/Agent.vue b/src/views/home/agent/components/Agent.vue index 55518a4..32a7f7b 100644 --- a/src/views/home/agent/components/Agent.vue +++ b/src/views/home/agent/components/Agent.vue @@ -8,8 +8,13 @@
- - + +
@@ -20,7 +25,7 @@ import Input from '../../components/Input.vue' import { fetchAgentReply } from '@/api/agent' import type { AgentParamsType } from '@/api/agent' - import { useUserInfoStore,useProjectStore } from '@/stores' + import { useUserInfoStore, useProjectStore } from '@/stores' import { useAgentStore } from '@/stores/agent' const userStore = useUserInfoStore() @@ -37,8 +42,10 @@ const messageList = ref([]) const listRef = ref() + const isGenerating = ref(false) + const isPaused = ref(false) // 标记是否为主动暂停 const params = reactive({ - projectID: '1', + projectID: projectStore.state.id, message: '', token: userStore.state.token, versionID: '', @@ -51,22 +58,32 @@ imageUrlList: [] }) - const abort = new AbortController() + // 每次请求时创建新的 AbortController + let abort: AbortController + + const createAbortController = () => { + // 如果已有未完成的中止,先中止它 + if (abort) { + abort.abort() + } + abort = new AbortController() + return abort + } onUnmounted(() => { - abort.abort() + abort?.abort() }) onMounted(() => { // 检查 store 中是否有初始项目数据 - projectStore.setId('1') // 临时设置项目ID为1,实际应用中应根据上下文动态设置 + // projectStore.setId('1') // 临时设置项目ID为1,实际应用中应根据上下文动态设置 const initialData = agentStore.getInitialProjectData if (initialData) { // 等待页面渲染完成后自动发送初始消息 params.configParams = { - type: initialData.type || 'Chair', - region: initialData.area || 'China', - style: initialData.style || 'Transitional', + type: initialData.type , + region: initialData.area, + style: initialData.style , temperature: 0.7 } handleSendMessage({ @@ -83,15 +100,21 @@ const handleSendMessage = async (message: { text: string images: Array<{ url: string; name: string }> - }) => { + }, skipUserMessage = false) => { console.log('Message sent:', message) + isPaused.value = false + isGenerating.value = true params.message = message.text params.imageUrlList = message.images || [] - messageList.value.push({ - id: messageList.value.length + 1, - text: message.text, - isUser: true - }) + + // 如果不是重新生成模式,则添加用户消息到列表 + if (!skipUserMessage) { + messageList.value.push({ + id: messageList.value.length + 1, + text: message.text, + isUser: true + }) + } // Add AI loading message const aiMessage = reactive({ @@ -106,6 +129,9 @@ }) messageList.value.push(aiMessage) + // 创建新的 AbortController + const abortController = createAbortController() + // console.log('token---', params.token, '参数---', params) try { @@ -116,7 +142,7 @@ const response = await fetch(`/api/ai-design/chat?${urlParams.toString()}`, { method: 'GET', - signal: abort.signal + signal: abortController.signal }) // 检查响应内容类型,判断是否为流式响应 @@ -128,9 +154,12 @@ // 非流式错误响应,使用 text() 读取错误信息 const errorText = await response.text() console.error('请求错误:', errorText) - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false return } @@ -143,9 +172,12 @@ } catch (e) { console.error('非流式响应文本:', text) } - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false return } @@ -181,26 +213,26 @@ // 过滤掉 id: 等字段,只取 data: let isNodeIdEvent = false - if(event.startsWith('event:')){ + if (event.startsWith('event:')) { isNodeIdEvent = true // continue } - + const dataLines = event - .split(/\n/) - .filter((line) => line.startsWith('data:')) - .map((line) => line.replace(/^data:\s*/, '').trim()) - console.log('dataLInes',dataLines); - if(isNodeIdEvent){ + .split(/\n/) + .filter((line) => line.startsWith('data:')) + .map((line) => line.replace(/^data:\s*/, '').trim()) + console.log('dataLInes', dataLines) + if (isNodeIdEvent) { params.versionID = dataLines[0] } - + if (dataLines.length === 0) continue const jsonText = dataLines.join('\n') try { const jsonData = JSON.parse(jsonText) - + // 赋值 project_id 和 version_id if (jsonData.project_id) params.projectID = jsonData.project_id if (jsonData.version_id) params.versionID = jsonData.version_id @@ -215,6 +247,7 @@ if (jsonData.type === 'end') { aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false flag = false break } @@ -224,6 +257,7 @@ console.log('结束-----------------------') aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false flag = false break } @@ -239,19 +273,59 @@ } } catch (error) { console.error('流式传输错误:', error) - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false } finally { reader.releaseLock() } } catch (error) { console.error('fetch请求失败:', error) - aiMessage.text = '发送失败,请重试' + if (!isPaused.value) { + aiMessage.text = '发送失败,请重试' + } aiMessage.streaming = false aiMessage.loading = false + isGenerating.value = false } } + + const handlePause = () => { + isPaused.value = true + isGenerating.value = false + abort?.abort() + } + + const handleRegenerate = async (aiMessage: any) => { + // 找到当前 AI 消息在列表中的索引 + const aiIndex = messageList.value.findIndex((msg) => msg.id === aiMessage.id) + if (aiIndex === -1) return + + // 找到对应的用户消息(AI 消息前面的最近一条用户消息) + let userMessage = null + for (let i = aiIndex - 1; i >= 0; i--) { + if (messageList.value[i].isUser) { + userMessage = messageList.value[i] + break + } + } + if (!userMessage) return + + // 删除当前的 AI 回复消息 + messageList.value.splice(aiIndex, 1) + + // 重新调用 API(跳过用户消息添加,因为用户消息已存在) + await handleSendMessage( + { + text: userMessage.text, + images: userMessage.images || [] + }, + true + ) + }