diff --git a/.gitignore b/.gitignore index 6609edf..e4b31a1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ coverage *.njsproj *.sln *.sw? + +.assistant-rules.md \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index c595bc0..72ed79d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,13 +1,13 @@ + + diff --git a/src/lang/en.ts b/src/lang/en.ts index f270781..1e9fd99 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -4,7 +4,7 @@ export default { login: 'Log in', register: 'Register', loginTo: 'Log on to FiDA', - LoginTitle: 'A multi-agent canvas for rapid, trend driven design iteration.', + loginTitle: 'A multi-agent canvas for rapid, trend driven design iteration.', name: 'Name', email: 'Email', password: 'Password', diff --git a/src/lang/index.ts b/src/lang/index.ts index d35810c..00e5230 100644 --- a/src/lang/index.ts +++ b/src/lang/index.ts @@ -27,7 +27,7 @@ const i18n = createI18n({ warnHtmlMessage: false, legacy: false, globalInjection:true, // 全局模式,可以直接使用 $t - locale: 'ENGLISH', + locale: localStorage.getItem('language') || 'ENGLISH', messages: messages }) diff --git a/src/stores/userInfo.ts b/src/stores/userInfo.ts index ef7d558..835d8ae 100644 --- a/src/stores/userInfo.ts +++ b/src/stores/userInfo.ts @@ -1,70 +1,46 @@ // 每一个存储的模块,命名规则use开头,store结尾 +import router from '@/router' import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { removeLocal, setLocal } from '@/utils/local' import MyEvent from '@/utils/myEvent' export const useUserInfoStore = defineStore('userInfo', () => { - const state = ref({ + const state = ref({ userInfo: {}, token: '', - generateParams: { - stylist: '', - sex: '', - stylistImage: '' - } - }) - // getters - const getUserInfo = computed(() => state.value.userInfo) + }) - // actions - const setUserInfo = (data: any) => { - state.value.userInfo = data - } + // getters + const getUserInfo = computed(() => state.value.userInfo) - const setToken = (data: string) => { - state.value.token = data - // setLocal(data, 'token') - } + // actions + const setUserInfo = (data: any) => { + state.value.userInfo = data + } - const getGenerateParams = () => { - return state.value.generateParams - } + const setToken = (data: string) => { + state.value.token = data + // setLocal(data, 'token') + } - const setGenerateParams = (data: any) => { - state.value.generateParams = data - } + const logOut = async () => { + // 处理退出登录的一些逻辑 + state.value.token = '' + state.value.userInfo = {} + // removeLocal('token') + // MyEvent.emit('clear-generate-state') + // MyEvent.emit('clear-client-state') + // MyEvent.emit('clearAllCache') + router.push({ name: 'login' }) + return "" + } - const resetGenerateParams = () => { - state.value.generateParams = { - stylist: '', - sex: '', - stylistImage: '' - } - } - - const logOut = () => { - // 处理退出登录的一些逻辑 - return new Promise((resolve) => { - state.value.token = '' - state.value.userInfo = {} - removeLocal('token') - resetGenerateParams() - MyEvent.emit('clear-generate-state') - MyEvent.emit('clear-client-state') - MyEvent.emit('clearAllCache') - resolve('') - }) - } - - return { - state, - getUserInfo, - setToken, - setUserInfo, - setGenerateParams, - getGenerateParams, - resetGenerateParams, - logOut - } + return { + state, + getUserInfo, + setToken, + setUserInfo, + logOut + } }) diff --git a/src/utils/request.ts b/src/utils/request.ts index 5bb61f0..abd9872 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,6 +1,7 @@ import axios from 'axios' import router from '@/router/index' import { useGlobalStore, useUserInfoStore } from '@/stores' +import { ElMessage } from 'element-plus' // 扩展 AxiosRequestConfig 接口 declare module 'axios' { @@ -80,13 +81,7 @@ service.interceptors.response.use( // 处理异常的情况 // console.log(res) if (res.code != 200) { - // showToast({ - // message: res.errMsg || res.message, - // // type: 'fail', - // duration: 5000, - // position: 'top', - // icon: 'none' - // }) + ElMessage.error(res.message) return Promise.reject(new Error(res.errMsg || res.message || 'error')) } else { // 默认只返回data,不返回状态码和message @@ -123,11 +118,7 @@ service.interceptors.response.use( } error.config && removePending(error.config) console.log('err' + error) // for debug - // showToast({ - // message: error.message, - // type: 'fail', - // duration: 5000 - // }) + ElMessage.error(error.message) } return Promise.reject(error) } diff --git a/src/views/home/agent/components/Agent.vue b/src/views/home/agent/components/Agent.vue index 0667d73..3b39c0c 100644 --- a/src/views/home/agent/components/Agent.vue +++ b/src/views/home/agent/components/Agent.vue @@ -77,8 +77,8 @@ }) messageList.value.push(aiMessage) - const threadId = '' // - console.log('token---', params.token, '参数---', params) + // const threadId = '' // + // console.log('token---', params.token, '参数---', params) try { const urlParams = new URLSearchParams({ @@ -100,7 +100,10 @@ // 非流式错误响应,使用 text() 读取错误信息 const errorText = await response.text() console.error('请求错误:', errorText) - throw new Error(`发起对话错误--- ${response.status}: ${errorText}`) + aiMessage.text = '发送失败,请重试' + aiMessage.streaming = false + aiMessage.loading = false + return } // 不是流式响应,使用 text()读取错误信息 @@ -112,6 +115,7 @@ } catch (e) { console.error('非流式响应文本:', text) } + aiMessage.text = '发送失败,请重试' aiMessage.streaming = false aiMessage.loading = false return @@ -195,6 +199,7 @@ } } catch (error) { console.error('流式传输错误:', error) + aiMessage.text = '发送失败,请重试' aiMessage.streaming = false aiMessage.loading = false } finally { @@ -202,6 +207,7 @@ } } catch (error) { console.error('fetch请求失败:', error) + aiMessage.text = '发送失败,请重试' aiMessage.streaming = false aiMessage.loading = false } diff --git a/src/views/home/components/Input.vue b/src/views/home/components/Input.vue index 652e783..f528da1 100644 --- a/src/views/home/components/Input.vue +++ b/src/views/home/components/Input.vue @@ -23,24 +23,7 @@ @input="handleEditorInput" @paste="handleEditorPaste" @keypress="handleKeyPress" - > - -
- - {{ $t('Input.trendingReport') }} - -
- + >
@@ -178,6 +161,8 @@ import { computed, ref, watch, nextTick, onMounted } from 'vue' import { areaList } from '@/utils/area' import { useI18n } from 'vue-i18n' + import lightIcon from '@/assets/images/light-icon.png' + import closeIcon from '@/assets/images/close-icon.png' // import Tag from './Tag.vue' const props = withDefaults( @@ -202,8 +187,6 @@ fileInputRef.value?.click() } - // BUG 标签被删除后无法重新出现 - // 处理文件选择 const handleFileChange = (event: Event) => { const input = event.target as HTMLInputElement @@ -243,21 +226,101 @@ 'NordicNoir' ] - // 标签相关(固定标签,v-show 控制显示) - const showReportTag = ref(false) const editorRef = ref(null) const inputValue = ref('') - const toogltReportTag = () => { - console.log(showReportTag.value) + const reportTags = ref([]) + const addReportTag = () => { + // create container matching static structure:
... + const tag = document.createElement('div') + tag.className = 'editor-tag report-btn flex-center' + tag.contentEditable = 'false' - showReportTag.value = !showReportTag.value + const imgLeft = document.createElement('img') + imgLeft.className = 'light-icon' + imgLeft.src = lightIcon as unknown as string + + const textSpan = document.createElement('span') + textSpan.innerText = t('Input.trendingReport') + + const imgClose = document.createElement('img') + imgClose.className = 'close-icon' + imgClose.src = closeIcon as unknown as string + imgClose.addEventListener('click', (ev) => { + ev.stopPropagation() + // remove tag when close clicked + tag.remove() + const idx = reportTags.value.indexOf(tag) + if (idx > -1) reportTags.value.splice(idx, 1) + }) + + // assemble + tag.appendChild(imgLeft) + tag.appendChild(textSpan) + tag.appendChild(imgClose) + + // Insert tag at the current cursor position + const selection = window.getSelection() + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0) + range.insertNode(tag) + + // Insert a zero-width space text node after the tag so the caret can be placed there + const zwsp = document.createTextNode('\u200B') + if (tag.parentNode) tag.parentNode.insertBefore(zwsp, tag.nextSibling) + + // Create a new collapsed range positioned inside the zwsp (after the tag) + const newRange = document.createRange() + newRange.setStart(zwsp, 1) + newRange.collapse(true) + + selection.removeAllRanges() + selection.addRange(newRange) + + // ensure editor has focus + editorRef.value && (editorRef.value as HTMLElement).focus() + } else if (editorRef.value) { + // If no selection, append directly to editor and place caret after + editorRef.value.appendChild(tag) + const zwsp = document.createTextNode('\u200B') + editorRef.value.appendChild(zwsp) + const sel = window.getSelection() + if (sel) { + const r = document.createRange() + r.setStart(zwsp, 1) + r.collapse(true) + sel.removeAllRanges() + sel.addRange(r) + } + editorRef.value && (editorRef.value as HTMLElement).focus() + } + + reportTags.value.push(tag) + } + + const toogltReportTag = () => { + // 清理掉已被删除的标签引用(从 DOM 中移除的元素) + reportTags.value = reportTags.value.filter((tag) => tag.parentNode !== null) + + if (reportTags.value.length > 0) { + // 移除所有标签及其关联的零宽空格 + reportTags.value.forEach((tag) => { + if (tag.nextSibling && tag.nextSibling.nodeType === Node.TEXT_NODE && tag.nextSibling.textContent === '\u200B') { + tag.nextSibling.remove() + } + tag.remove() + }) + reportTags.value = [] + } else { + // 添加标签 + addReportTag() + } } const handleEditorInput = () => { if (!editorRef.value) return - // 提取纯文本(排除标签) + // 提取纯文本(排除插入的report标签) let text = '' const walker = document.createTreeWalker(editorRef.value, NodeFilter.SHOW_TEXT, null) @@ -266,7 +329,7 @@ text += node.textContent } - // 移除末尾的空格(如果有的话) + // 移除末尾的空格 text = text.replace(/\s+$/, '') inputValue.value = text @@ -288,19 +351,58 @@ // editor.style.overflowY = 'auto' return } else { - editor.style.height = 'auto' - const maxHeight = - 20 * parseFloat(getComputedStyle(document.documentElement).fontSize || '16') - editor.style.height = Math.min(editor.scrollHeight, maxHeight) + 'px' + // editor.style.height = 'auto' + // const maxHeight = + // 20 * parseFloat(getComputedStyle(document.documentElement).fontSize || '16') + // editor.style.height = Math.min(editor.scrollHeight, maxHeight) + 'px' } } } const handleKeyPress = (e) => { // 检测回车 - if (e.key === 'Enter' && !e.shiftKey) { + if (e.key === 'Enter') { e.preventDefault() handleSendAgent() + return + } + if (e.key === 'Backspace') { + const selection = window.getSelection() + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0) + if (range.collapsed) { + let nodeToDelete = null + const startContainer = range.startContainer + const startOffset = range.startOffset + + if (startContainer.nodeType === Node.TEXT_NODE) { + // Cursor at the end of a text node, check next sibling + if (startOffset === startContainer.length) { + nodeToDelete = startContainer.nextSibling + } + } else if (startContainer.nodeType === Node.ELEMENT_NODE) { + // Cursor positioned between child nodes + nodeToDelete = startContainer.childNodes[startOffset] + } + + if ( + nodeToDelete && + nodeToDelete.nodeType === Node.ELEMENT_NODE && + (nodeToDelete as Element).classList && + ((nodeToDelete as Element).classList.contains('editor-tag') || + (nodeToDelete as Element).classList.contains('report-tag')) + ) { + e.preventDefault() + ;(nodeToDelete as Element).remove() + // Optional: remove from reportTags if tracking + const index = reportTags.value.indexOf(nodeToDelete) + if (index > -1) { + reportTags.value.splice(index, 1) + } + return + } + } + } } } @@ -400,7 +502,7 @@ border-radius: 2.2rem; width: 20rem; background-color: #fff; - border: 0.11rem solid #f6f4ef1a; + border: 1px solid #F6F4EF; column-gap: 1.2rem; cursor: pointer; .c-svg { @@ -410,14 +512,15 @@ } .scroll-content { - overflow-y: visible; + display: flex; + flex: 1; + overflow-y: auto; padding: 3.4rem 1.7rem 1.7rem; } .editor { width: 100%; - min-height: 5rem; - max-height: 20rem; + flex: 1; border: none; outline: none; padding: 0 1.4rem 1.4rem; @@ -437,41 +540,6 @@ color: #999; pointer-events: none; } - - // 标签样式 - .editor-tag { - width: 21.8rem; - height: 4.4rem; - display: inline-flex; - position: initial; - bottom: initial; - border: 0.11rem solid #0000001a; - font-family: 'GeneralMedium'; - font-weight: 500; - font-size: 1.8rem; - column-gap: 0; - span { - margin: 0 0.7rem 0 1.2rem; - } - .c-svg.close-icon { - width: 2.4rem; - height: 2.4rem; - cursor: pointer; - } - } - } - - // 标签容器(已废弃,保留兼容性) - .tags-container { - display: inline-flex; - flex-wrap: wrap; - gap: 0.5rem; - padding: 0 1.4rem 0.5rem; - - .tag-item { - display: inline-flex; - align-items: center; - } } // 图片预览区域样式 @@ -804,4 +872,36 @@ display: none; } } + + /* 动态添加的编辑器标签样式 */ + .assist-input-wrapper .editor .editor-tag { + width: 21.8rem; + height: 4.4rem; + display: inline-flex; + border: 0.11rem solid #0000001a; + font-family: 'GeneralMedium'; + font-weight: 500; + font-size: 1.8rem; + column-gap: 0; + margin: 0 0.5rem; + vertical-align: middle; + border-radius: 2.2rem; + + span { + margin: 0 0.7rem 0 1.2rem; + } + + .light-icon { + width: 1.5rem; + height: 1.9rem; + flex-shrink: 0; + } + + .close-icon { + width: 1rem; + height: 1rem; + cursor: pointer; + flex-shrink: 0; + } + } diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 86057fc..bd9b8aa 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -15,12 +15,14 @@
+ + + diff --git a/src/views/home/setting/LearnMore.vue b/src/views/home/setting/LearnMore.vue new file mode 100644 index 0000000..721608a --- /dev/null +++ b/src/views/home/setting/LearnMore.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/src/views/home/setting/Profile.vue b/src/views/home/setting/Profile.vue new file mode 100644 index 0000000..a9f784c --- /dev/null +++ b/src/views/home/setting/Profile.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/views/home/setting/index.vue b/src/views/home/setting/index.vue new file mode 100644 index 0000000..9195ae7 --- /dev/null +++ b/src/views/home/setting/index.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/views/home/top-nav.vue b/src/views/home/top-nav.vue index cfab55c..6d5e6c4 100644 --- a/src/views/home/top-nav.vue +++ b/src/views/home/top-nav.vue @@ -12,13 +12,38 @@
- + + + + diff --git a/src/views/login/login.vue b/src/views/login/login.vue index 21f46f7..d09a0e8 100644 --- a/src/views/login/login.vue +++ b/src/views/login/login.vue @@ -102,13 +102,17 @@ verificationCode: code }) .then((res) => { + console.log(res) if (res) { userInfoStore.setToken(res) + userInfoStore.setUserInfo({ + email: formData.email + }) router.push({ name: 'mainInput' }) } }) - .catch(() => { - console.warn('error verify code!') + .catch((error) => { + console.warn(error) }) } diff --git a/src/views/login/register.vue b/src/views/login/register.vue index fc127b7..3598ccc 100644 --- a/src/views/login/register.vue +++ b/src/views/login/register.vue @@ -111,6 +111,9 @@ .then((res) => { if (res) { userInfoStore.setToken(res) + userInfoStore.setUserInfo({ + email: formData.email + }) router.push({ name: 'nuic' }) } }) diff --git a/src/views/nuic/index.vue b/src/views/nuic/index.vue index 8d453c9..9114474 100644 --- a/src/views/nuic/index.vue +++ b/src/views/nuic/index.vue @@ -25,7 +25,7 @@
- +
{{ $t('Nuic.loadingTip') }}
@@ -167,7 +167,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - width: 14.4rem; + width: 60rem; height: auto; opacity: 0; &.loading {