Commit ff7b0ded authored by shirlyn.guo's avatar shirlyn.guo 👌🏻

Merge branch 'master' of https://gitlab.gsstcloud.com/poc/poc-fe into shirlyn

parents 4363bd5a 4cc157d3
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_f5muspehl1h.css" /> <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4711453_i03chzm1n0e.css" />
<title>Model Link</title> <title>Model Link</title>
</head> </head>
......
...@@ -22,12 +22,14 @@ ...@@ -22,12 +22,14 @@
"@unocss/reset": "^0.61.9", "@unocss/reset": "^0.61.9",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^10.11.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"bowser": "^2.11.0",
"clipboardy": "^4.0.0", "clipboardy": "^4.0.0",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dompurify": "^3.2.0", "dompurify": "^3.2.0",
"github-markdown-css": "^5.7.0", "github-markdown-css": "^5.7.0",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"howler": "^2.2.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.0", "marked": "^15.0.0",
"marked-highlight": "^2.2.1", "marked-highlight": "^2.2.1",
...@@ -46,6 +48,7 @@ ...@@ -46,6 +48,7 @@
"@commitlint/config-conventional": "^19.5.0", "@commitlint/config-conventional": "^19.5.0",
"@commitlint/types": "^19.5.0", "@commitlint/types": "^19.5.0",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/howler": "^2.2.12",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.16.5", "@types/node": "^20.16.5",
"@types/spark-md5": "^3.0.4", "@types/spark-md5": "^3.0.4",
......
This diff is collapsed.
...@@ -170,3 +170,20 @@ export function fetchRemoveSalePublishApplication<T>(agentPublishId: number) { ...@@ -170,3 +170,20 @@ export function fetchRemoveSalePublishApplication<T>(agentPublishId: number) {
export function fetchGetApplicationMallInfo<T>(agentId: string) { export function fetchGetApplicationMallInfo<T>(agentId: string) {
return request.post<T>(`/bizAgentApplicationMallRest/getMallInfoByAgentId.json?agentId=${agentId}`) return request.post<T>(`/bizAgentApplicationMallRest/getMallInfoByAgentId.json?agentId=${agentId}`)
} }
/**
* @query agentId 应用Id
* @returns 获取用户自动播放配置
*/
export function fetchGetAutoPlayByAgentId<T>(agentId: string) {
return request.post<T>(`/agentApplicationRest/autoPlayByAgentId.json?agentId=${agentId}`)
}
/**
* @query agentId 应用Id
* @query autoPlay 是否自动播放
* @returns 设置用户自动播放配置
*/
export function fetchUpdateAutoPlay<T>(agentId: string, autoPlay: 'Y' | 'N') {
return request.post<T>(`/agentApplicationRest/enableAutoPlay.json?agentId=${agentId}&autoPlay=${autoPlay}`)
}
import { request } from '@/utils/request' import { request } from '@/utils/request'
import type { AxiosProgressEvent } from 'axios'
export function fetchAgentApplicationSelectList<T>() { export function fetchAgentApplicationSelectList<T>() {
return request.post<T>('/agentApplicationRest/getDefaultList.json', { return request.post<T>('/agentApplicationRest/getDefaultList.json', {
...@@ -28,3 +29,17 @@ export function fetchMessageRecordList<T>(dialogueId: string) { ...@@ -28,3 +29,17 @@ export function fetchMessageRecordList<T>(dialogueId: string) {
timeout: 12000, timeout: 12000,
}) })
} }
export function fetchFileUpload<T>(
formData: FormData,
config: { onUploadProgress: (progressEvent?: AxiosProgressEvent) => void; signal?: AbortSignal } = {
onUploadProgress: () => {},
},
) {
return request.post<T>(`/bosRest/upload.json`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 0,
onUploadProgress: config.onUploadProgress,
signal: config.signal,
})
}
import { request } from '@/utils/request'
/**
* @returns 获取音色列表
*/
export function fetchGetTimbreList<T>() {
return request.post<T>('/bizVoiceTimbreRest/getTimbreList.json')
}
/**
* @query timbreId 音色Id
* @returns 获取音色详情
*/
export function fetchGetTimbreInfoDetail<T>(timbreId: string) {
return request.post<T>(`/bizVoiceTimbreRest/getTimbreInfo.json?timbreId=${timbreId}`)
}
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>黑色暂停</title>
<defs>
<rect id="path-1" x="0" y="0" width="34" height="34"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="对话-进入" transform="translate(-181.000000, -310.000000)">
<g id="编组-2" transform="translate(181.000000, 310.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<path d="M17.0006063,1.5 C21.2734934,1.5 25.1479946,3.23913189 27.9545032,6.04584067 C30.7609677,8.85250533 32.5,12.7272396 32.5,17.0006063 C32.5,21.2738531 30.7610549,25.1482139 27.9547057,27.9545631 C25.1481717,30.7610971 21.2735836,32.5 17.0006063,32.5 C12.7272396,32.5 8.85250534,30.7609677 6.04584068,27.9545032 C3.23913189,25.1479946 1.5,21.2734933 1.5,17.0006063 C1.5,12.7275994 3.23921912,8.8527246 6.04604319,6.04590054 C8.85268248,3.23926124 12.72733,1.5 17.0006063,1.5 L17.0006063,1.5 Z" id="路径" stroke="#333333" stroke-width="3" fill-rule="nonzero" mask="url(#mask-2)"></path>
<rect id="矩形" fill="#000DFF" mask="url(#mask-2)" x="11.3333333" y="11.3333333" width="3.23809524" height="11.3333333" rx="1.61904762"></rect>
<rect id="矩形备份" fill="#000DFF" mask="url(#mask-2)" x="19.4285714" y="11.3333333" width="3.23809524" height="11.3333333" rx="1.61904762"></rect>
</g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>播放</title>
<defs>
<rect id="path-1" x="0" y="0" width="34" height="34"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="对话-按住语音-提示语" transform="translate(-181.000000, -310.000000)">
<g id="播放" transform="translate(181.000000, 310.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<g mask="url(#mask-2)" fill-rule="nonzero">
<path d="M17.0005306,1.5 C21.2734482,1.5 25.1478582,3.23921344 27.9543229,6.04585326 C30.7609106,8.85261617 32.5,12.7274291 32.5,17.0005306 C32.5,21.2735273 30.7609869,25.1480133 27.9545001,27.9545001 C25.1480133,30.7609869 21.2735273,32.5 17.0005306,32.5 C12.7269453,32.5 8.85210629,30.7611966 6.04545533,27.9547208 C3.23892745,25.1483681 1.5,21.273932 1.5,17.0005306 C1.5,12.7270243 3.23900379,8.85226132 6.04563255,6.04563255 C8.85226132,3.23900379 12.7270243,1.5 17.0005306,1.5 Z" id="路径" stroke="#CCCCCC" stroke-width="3"></path>
<path d="M13.6855279,9.59377047 C14.4484879,9.59377047 15.1658188,9.88982862 15.2443432,9.92378513 L15.2443432,9.92378513 L15.3812303,9.99488155 L24.6725758,15.6783496 L24.8073406,15.9213507 C25.5108767,17.1883524 24.8275023,18.3131612 24.1950626,18.7609625 L24.1950626,18.7609625 L24.1048657,18.8193252 L14.9196343,24.02528 C14.471833,24.2544864 14.0654162,24.3606004 13.6727943,24.3606004 L13.6727943,24.3606004 L13.4721536,24.3516533 C12.36468,24.2516108 11.7569642,23.3358336 11.5685528,22.7816235 L11.5685528,22.7816235 L11.5112512,22.6150245 L11.5112512,11.827471 C11.5112512,10.4713336 12.3654693,9.59377047 13.6855279,9.59377047 Z M13.64096,11.7171125 C13.6367155,11.7447021 13.6345932,11.781842 13.6345932,11.8274711 L13.6345932,11.8274711 L13.6345932,22.1884461 C13.6515714,22.21073 13.6674885,22.2287694 13.6802222,22.2393808 C13.7046285,22.2372585 13.7799694,22.2266471 13.9136731,22.1587341 L13.9136731,22.1587341 L22.8856153,17.071627 L14.3551075,11.8539996 C14.2044256,11.7988203 13.837271,11.7065011 13.64096,11.7171125 Z" id="形状结合" stroke="#000DFF" stroke-width="0.5" fill="#000DFF"></path>
</g>
</g>
</g>
</g>
</svg>
...@@ -7,3 +7,12 @@ export const INDEX_URLS: Record<'DEV' | 'PROD', string> = { ...@@ -7,3 +7,12 @@ export const INDEX_URLS: Record<'DEV' | 'PROD', string> = {
DEV: 'https://poc-sit.gsstcloud.com/fe/', DEV: 'https://poc-sit.gsstcloud.com/fe/',
PROD: 'https://model-link.gsstcloud.com/fe/', PROD: 'https://model-link.gsstcloud.com/fe/',
} }
const ENV = import.meta.env.VITE_APP_ENV
export const Domain_Name: Record<'DEV' | 'PROD', string> = {
DEV: 'poc-sit.gsstcloud.com',
PROD: 'model-link.gsstcloud.com',
}
export const TEXTTOSPEECH_WS_URL = `wss://${Domain_Name[ENV || 'DEV']}/websocket/textToSpeechTC.ws`
...@@ -91,6 +91,21 @@ common_module: ...@@ -91,6 +91,21 @@ common_module:
bind: 'Bind' bind: 'Bind'
sms: 'Short message' sms: 'Short message'
verificationCode: 'Verification code' verificationCode: 'Verification code'
role: 'Role'
mandarin: 'Mandarin'
cantonese: 'Cantonese'
english: 'English'
voice: 'Voice'
sound: 'Sound'
voice_auto_play: 'Voice auto play'
start_playing: 'play'
stop_playing: 'stop'
unplayable: 'unplayable'
unplayable_tip: 'The voice setting do not match the model output language'
response_error: 'Response error'
agent_exception: 'Agent exception, please try again later!'
equity: 'Equity'
file_size_limit: 'The file size cannot exceed {size}'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
...@@ -103,6 +118,8 @@ common_module: ...@@ -103,6 +118,8 @@ common_module:
cancel_associate_file_tip: 'No longer answer around this file' cancel_associate_file_tip: 'No longer answer around this file'
upload_file_limit: 'Only a single file can be uploaded in PDF, DOC, DOCX, MD, TXT format, up to 10MB' upload_file_limit: 'Only a single file can be uploaded in PDF, DOC, DOCX, MD, TXT format, up to 10MB'
overwrite_file_tip: 'The newly uploaded file overwrites the original file, whether to continue uploading' overwrite_file_tip: 'The newly uploaded file overwrites the original file, whether to continue uploading'
stop_playing_and_then_operate: 'When the audio is playing, stop playing and then perform the operation'
do_not_operate_until_the_reply_is_complete: 'Do not operate until the reply is complete'
data_table_module: data_table_module:
action: 'Controls' action: 'Controls'
...@@ -281,6 +298,10 @@ personal_space_module: ...@@ -281,6 +298,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: 'After data deletion, it cannot be revoked. Are you sure you want to delete it?' memory_fragment_delete_row_tip_content: 'After data deletion, it cannot be revoked. Are you sure you want to delete it?'
add_knowledge_successfully: 'Data set {0} was added successfully' add_knowledge_successfully: 'Data set {0} was added successfully'
remove_knowledge_successfully: 'Data set {0} was removed successfully' remove_knowledge_successfully: 'Data set {0} was removed successfully'
setting_voice: 'Setting voice'
setting_voice_message: 'You can set the language and tone. If the selected language is inconsistent with the model language, the speech cannot be played'
setting_voice_desc: 'You can customize the voice and timbre for voice broadcast, and you can set only one at a time.'
currently_only_one_voice_can_be_set: 'Currently, you can set only one voice'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: 'Edit memory variable' edit_memory_variable: 'Edit memory variable'
...@@ -462,3 +483,9 @@ personal_settings_module: ...@@ -462,3 +483,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: 'Please enter the correct verification code' please_enter_the_correct_verification_code: 'Please enter the correct verification code'
binding_successful: 'Binding successful' binding_successful: 'Binding successful'
obtaining_the_verification_code: 'Obtaining the verification code' obtaining_the_verification_code: 'Obtaining the verification code'
upload_file: 'Upload file'
file_type_restriction_doc: 'Only.pdf,.txt, .md, .doc, and.docx file types are supported'
the_file_type_cannot_be_uploaded: 'The file type cannot be uploaded'
equity_Module:
file_empty_tip: 'The file content cannot be empty'
...@@ -90,6 +90,21 @@ common_module: ...@@ -90,6 +90,21 @@ common_module:
bind: '绑定' bind: '绑定'
sms: '短信' sms: '短信'
verificationCode: '验证码' verificationCode: '验证码'
role: '角色'
mandarin: '普通话'
cantonese: '粤语'
english: '英语'
voice: '语音'
sound: '声音'
voice_auto_play: '语音自动播放'
start_playing: '开始播放'
stop_playing: '停止播放'
unplayable: '不可播放'
unplayable_tip: '语音设置与模型输出语言不匹配'
response_error: '响应错误'
agent_exception: '应用异常,请稍后重试!'
equity: '权益'
file_size_limit: '文件大小不能超过 {size}'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
...@@ -102,6 +117,8 @@ common_module: ...@@ -102,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再围绕这个文件回答' cancel_associate_file_tip: '不再围绕这个文件回答'
upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB' upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传' overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
stop_playing_and_then_operate: '音频播放中,请停止播放后再操作'
do_not_operate_until_the_reply_is_complete: '回复完成后再操作'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -279,6 +296,10 @@ personal_space_module: ...@@ -279,6 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '数据删除后不可撤销,确定要删除吗?' memory_fragment_delete_row_tip_content: '数据删除后不可撤销,确定要删除吗?'
add_knowledge_successfully: '数据集 {0} 添加成功' add_knowledge_successfully: '数据集 {0} 添加成功'
remove_knowledge_successfully: '数据集 {0} 移除成功' remove_knowledge_successfully: '数据集 {0} 移除成功'
setting_voice: '设置语音'
setting_voice_message: '你可以设置语言和音色,若所选语言与模型语言不一致,则无法播放语音'
setting_voice_desc: '您可自定义语音及音色,用于语音播报,且每次仅可设置一种。'
currently_only_one_voice_can_be_set: '当前仅可设置一种声音'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '编辑记忆变量' edit_memory_variable: '编辑记忆变量'
...@@ -460,3 +481,9 @@ personal_settings_module: ...@@ -460,3 +481,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: '请输入正确验证码' please_enter_the_correct_verification_code: '请输入正确验证码'
binding_successful: '绑定成功' binding_successful: '绑定成功'
obtaining_the_verification_code: '获取验证码方式' obtaining_the_verification_code: '获取验证码方式'
upload_file: '上传文件'
file_type_restriction_doc: '只支持.pdf, .txt,.md,.doc,.docx文件类型'
the_file_type_cannot_be_uploaded: '暂不支持上传该文件类型'
equity_Module:
file_empty_tip: '文件内容不能为空'
...@@ -90,6 +90,21 @@ common_module: ...@@ -90,6 +90,21 @@ common_module:
bind: '綁定' bind: '綁定'
sms: '短信' sms: '短信'
verificationCode: '驗證碼' verificationCode: '驗證碼'
role: '角色'
mandarin: '普通話'
cantonese: '粵語'
english: '英語'
voice: '語音'
sound: '聲音'
voice_auto_play: '語音自動播放'
start_playing: '開始播放'
stop_playing: '停止播放'
unplayable: '不可播放'
unplayable_tip: '語音設置與模型輸出語言不匹配'
response_error: '響應錯誤'
agent_exception: '應用異常,請稍後重試!'
equity: '权益'
file_size_limit: '文件大小不能超過 {size}'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
...@@ -102,6 +117,8 @@ common_module: ...@@ -102,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再圍繞這個文件回答' cancel_associate_file_tip: '不再圍繞這個文件回答'
upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB' upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳' overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
stop_playing_and_then_operate: '音頻播放中,請停止播放後再操作'
do_not_operate_until_the_reply_is_complete: '回覆完成後再操作'
data_table_module: data_table_module:
action: '操作' action: '操作'
...@@ -279,6 +296,10 @@ personal_space_module: ...@@ -279,6 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '數據删除後不可撤銷,確定要删除嗎?' memory_fragment_delete_row_tip_content: '數據删除後不可撤銷,確定要删除嗎?'
add_knowledge_successfully: '數據集 {0} 添加成功' add_knowledge_successfully: '數據集 {0} 添加成功'
remove_knowledge_successfully: '數據集 {0} 移除成功' remove_knowledge_successfully: '數據集 {0} 移除成功'
setting_voice: '設置語音'
setting_voice_message: '你可以設置語言和音色,若所選語言與模型語言不一致,則無法播放語音'
setting_voice_desc: '您可自定義語音及音色,用於語音播報,且每次僅可設置一種。'
currently_only_one_voice_can_be_set: '當前僅可設置一種聲音'
memory_variable_modal: memory_variable_modal:
edit_memory_variable: '編輯記憶變數' edit_memory_variable: '編輯記憶變數'
...@@ -460,3 +481,9 @@ personal_settings_module: ...@@ -460,3 +481,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: '請輸入正確驗證碼' please_enter_the_correct_verification_code: '請輸入正確驗證碼'
binding_successful: '綁定成功' binding_successful: '綁定成功'
obtaining_the_verification_code: '獲取驗證碼方式' obtaining_the_verification_code: '獲取驗證碼方式'
upload_file: '上傳文件'
file_type_restriction_doc: '只支持.pdf, .txt,.md,.doc,.docx文件類型'
the_file_type_cannot_be_uploaded: '暫不支持上傳該文件類型'
equity_Module:
file_empty_tip: '文件內容不能為空'
...@@ -32,6 +32,10 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState { ...@@ -32,6 +32,10 @@ export function defaultPersonalAppConfigState(): PersonalAppConfigState {
communicationTurn: 3, communicationTurn: 3,
temperature: 0.5, temperature: 0.5,
}, },
voiceConfig: {
defaultOpen: 'Y',
timbreId: '',
},
modifiedTime: new Date(), modifiedTime: new Date(),
createdTime: '', createdTime: '',
isCollect: '', isCollect: '',
......
...@@ -40,6 +40,11 @@ export const useSystemLanguageStore = defineStore('system-language-store', { ...@@ -40,6 +40,11 @@ export const useSystemLanguageStore = defineStore('system-language-store', {
languageOptions: defaultLanguageOptions, languageOptions: defaultLanguageOptions,
}), }),
getters: {
currentLanguage(state): I18n.LangType {
return state.currentLanguageInfo.key
},
},
actions: { actions: {
updateCurrentLanguageInfo(key: I18n.LangType) { updateCurrentLanguageInfo(key: I18n.LangType) {
if (this.currentLanguageInfo.key === key) return '' if (this.currentLanguageInfo.key === key) return ''
......
...@@ -36,6 +36,10 @@ export interface PersonalAppConfigState { ...@@ -36,6 +36,10 @@ export interface PersonalAppConfigState {
communicationTurn: number //参考对话轮次 0-100 communicationTurn: number //参考对话轮次 0-100
temperature: number //多样性 0-1.00 temperature: number //多样性 0-1.00
} }
voiceConfig: {
defaultOpen: 'Y' | 'N' //是否默认开启 Y-开启 N-关闭
timbreId: string //音色ID
}
popularity?: number popularity?: number
modifiedTime: Date modifiedTime: Date
createdTime: string createdTime: string
......
import Bowser from 'bowser'
export function validBrowser() {
const browser = Bowser.getParser(window.navigator.userAgent)
return browser.satisfies({
android: {
chrome: '> 80',
},
})
}
import i18n from '@/locales'
const { t } = i18n.global
export default class WebSocketCtr {
private socket: WebSocket | null = null
private readonly url: string
public isDisconnect: boolean = true
constructor(url: string) {
this.url = url
}
private initSocket(callBack?: () => void) {
const socket = new WebSocket(this.url)
socket.onopen = () => {
this.isDisconnect = false
this.onConnect()
callBack && callBack()
}
socket.onmessage = (event: any) => {
let data: any = { final: false }
try {
data = JSON.parse(event.data)
} catch (err) {
window.$message.error(t('common_module.response_error'))
this.onMessageError()
}
if (data.code && data.code !== '0') {
if (this.socket) {
this.socket.close()
this.socket = null
}
this.onMessageError()
return
}
this.onMessage(data)
}
socket.onerror = (event) => {
this.onError(event)
}
socket.onclose = () => {
this.isDisconnect = true
this.onDisconnect()
}
this.socket = socket
}
connect(callBack?: () => void) {
if (this.socket && [0, 1].includes(this.socket.readyState)) {
callBack && callBack()
return
}
this.initSocket(callBack)
}
send(content: { [k: string]: any }) {
this.socket && this.socket.send(JSON.stringify(content))
}
disconnect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
this.onDisconnect()
}
/* call back */
onConnect() {}
onMessage(_message: any) {}
onMessageError() {}
onError(_event?: any) {}
onDisconnect() {}
}
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, toValue } from 'vue' import { computed, nextTick, ref, shallowRef, toValue, useTemplateRef } from 'vue'
import type { AgentApplicationRecordItem, MessageItemInterface } from '../types' import type { AgentApplicationRecordItem, MessageItemInterface } from '../types'
import { fetchAgentApplicationSelectList } from '@/apis/home-agent' import { fetchAgentApplicationSelectList, fetchFileUpload } from '@/apis/home-agent'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import fetchEventStreamSource from '../utils/fetch-event-stream-source' import fetchEventStreamSource from '../utils/fetch-event-stream-source'
import { throttle } from 'lodash-es' import { throttle } from 'lodash-es'
...@@ -31,13 +31,24 @@ const currentFetchEventSourceController = defineModel<AbortController | null>('c ...@@ -31,13 +31,24 @@ const currentFetchEventSourceController = defineModel<AbortController | null>('c
const { t } = useI18n() const { t } = useI18n()
const inputFileRef = useTemplateRef<HTMLInputElement | null>('inputFileRef')
let fileUploadController = shallowRef<AbortController | null>(null)
const isShowApplicationSelectMenu = ref(false) const isShowApplicationSelectMenu = ref(false)
const agentApplicationSelectList = ref<AgentApplicationRecordItem[]>([]) const agentApplicationSelectList = ref<AgentApplicationRecordItem[]>([])
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>()) const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
const currentInputFileInfo = ref({
url: '',
fileName: '',
percentage: 5,
uploading: false,
})
const isQuestionSubmitBtnDisabled = computed(() => { const isQuestionSubmitBtnDisabled = computed(() => {
return questionContent.value.trim().length === 0 || isAgentResponding.value return questionContent.value.trim().length === 0 || isAgentResponding.value || currentInputFileInfo.value.uploading
}) })
;(function () { ;(function () {
...@@ -150,6 +161,7 @@ function questionSubmit() { ...@@ -150,6 +161,7 @@ function questionSubmit() {
dialogsId: props.currentSessionId, //会话ID dialogsId: props.currentSessionId, //会话ID
agentId: currentAgentApplication.value.agentId, //应用ID agentId: currentAgentApplication.value.agentId, //应用ID
input: questionContent.value.trim(), //提问文本 input: questionContent.value.trim(), //提问文本
fileUrls: currentInputFileInfo.value.url ? [currentInputFileInfo.value.url] : [],
}, },
{ {
onmessage: (message) => { onmessage: (message) => {
...@@ -177,7 +189,9 @@ function questionSubmit() { ...@@ -177,7 +189,9 @@ function questionSubmit() {
emit('historyRecordListUpdate') emit('historyRecordListUpdate')
}) })
}, },
onerror: () => { onerror: (err) => {
err.message && window.$message.error(err.message)
emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!) emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!)
}, },
}, },
...@@ -190,11 +204,85 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) { ...@@ -190,11 +204,85 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) { if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault() event.preventDefault()
if (questionContent.value.trim().length > 0) { if (questionContent.value.trim().length > 0 && !isAgentResponding.value && !currentInputFileInfo.value.uploading) {
questionSubmit() questionSubmit()
} }
} }
} }
function handleFileUploadPopup() {
inputFileRef.value && inputFileRef.value.click()
}
function handleFileUpload(e: Event) {
const target = e.target as HTMLInputElement
const file = target.files && target.files[0]
if (file) {
if (!['.pdf', '.txt', '.md', '.doc', '.docx'].some((fileType) => file.name.endsWith(fileType))) {
window.$message.warning(t('personal_settings_module.the_file_type_cannot_be_uploaded'))
return
}
if (file.size === 0) {
window.$message.warning(t('equity_Module.file_empty_tip'))
return
} else if (file.size > 1024 * 1024 * 10) {
window.$message.warning(t('common_module.file_size_limit', { size: '10MB' }))
return
}
currentInputFileInfo.value = {
fileName: file.name,
url: URL.createObjectURL(file),
percentage: 5,
uploading: true,
}
fileUploadController.value && fileUploadController.value.abort()
fileUploadController.value = new AbortController()
const formData = new FormData()
formData.append('file', file)
fetchFileUpload<string>(formData, {
onUploadProgress: (progressEvent) => {
const percentage = Number.parseInt((progressEvent?.progress || 0).toFixed(2)) * 100
currentInputFileInfo.value.percentage = percentage < 5 ? 5 : percentage
if (progressEvent?.progress === 1) {
currentInputFileInfo.value.uploading = false
}
},
signal: fileUploadController.value.signal,
}).then((res) => {
currentInputFileInfo.value.url = res.data
})
}
}
function handleFileUploadCancel() {
currentInputFileInfo.value = {
url: '',
fileName: '',
percentage: 0,
uploading: false,
}
inputFileRef.value && (inputFileRef.value.value = '')
if (fileUploadController.value) {
fileUploadController.value.abort()
fileUploadController.value = null
}
}
function handleFileUploadReplace() {
handleFileUploadPopup()
}
</script> </script>
<template> <template>
...@@ -281,12 +369,77 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) { ...@@ -281,12 +369,77 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
</Transition> </Transition>
</div> </div>
<div class="flex items-center">
<Transition name="file-upload" mode="out-in">
<div v-if="!currentInputFileInfo.fileName">
<n-popover trigger="hover">
<template #trigger>
<n-button
class="application-select-btn !mr-[14px] !h-[34px] !rounded-[10px] !p-0"
@click="handleFileUploadPopup"
>
<div class="box-border flex w-full items-center justify-between px-[12px]">
<i class="iconfont icon-upload mr-[5px] text-[14px]"></i>
<span class="text-[14px]">{{ t('personal_settings_module.upload_file') }}</span>
</div>
</n-button>
</template>
<span>{{ t('personal_settings_module.file_type_restriction_doc') }}</span>
</n-popover>
</div>
<div v-else class="relative !mr-[14px] flex !h-[34px] items-center rounded-[10px] bg-[#ECEFFF] px-[14px]">
<div
class="mr-[5px] h-[18px] w-[18px] bg-[url('https://gsst-poe-sit.gz.bcebos.com/icon/doc.svg')] bg-contain bg-no-repeat"
></div>
<div class="w-[260px] text-[14px]">
<n-ellipsis :tooltip="{ width: 400 }">
{{ currentInputFileInfo.fileName }}
</n-ellipsis>
</div>
<div class="ml-[10px]">
<i
class="iconfont icon-huanyihuan hover:text-theme-color mr-[10px] cursor-pointer text-[14px] transition"
@click="handleFileUploadReplace"
></i>
<i
class="iconfont icon-close hover:text-theme-color cursor-pointer text-[14px] transition"
@click="handleFileUploadCancel"
></i>
</div>
<div v-show="currentInputFileInfo.uploading" class="absolute bottom-[1px] left-[40px] w-[86%]">
<n-progress
type="line"
:percentage="currentInputFileInfo.percentage"
:show-indicator="false"
processing
:fill-border-radius="0"
:border-radius="0"
:height="3"
rail-color="#DCDCDC"
/>
</div>
</div>
</Transition>
<n-button class="application-select-btn !h-[34px] !rounded-[10px] !p-0" @click="handleCreateNewSession"> <n-button class="application-select-btn !h-[34px] !rounded-[10px] !p-0" @click="handleCreateNewSession">
<div class="box-border flex w-full items-center justify-between px-[12px]"> <div class="box-border flex w-full items-center justify-between px-[12px]">
<i class="iconfont icon-session mr-[5px] text-[14px]"></i> <i class="iconfont icon-session mr-[5px] text-[14px]"></i>
<span class="text-[14px]">{{ t('home_module.starting_a_new_session') }}</span> <span class="text-[14px]">{{ t('home_module.starting_a_new_session') }}</span>
</div> </div>
</n-button> </n-button>
<input
ref="inputFileRef"
type="file"
class="hidden"
accept=".pdf,.txt,.md,.doc,.docx"
@change="handleFileUpload"
/>
</div>
</div> </div>
<div> <div>
...@@ -340,4 +493,16 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) { ...@@ -340,4 +493,16 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
opacity: 0; opacity: 0;
scale: 0.8; scale: 0.8;
} }
.file-upload-enter-active,
.file-upload-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
transition-property: opacity;
}
.file-upload-enter-from,
.file-upload-leave-to {
opacity: 0;
}
</style> </style>
...@@ -166,7 +166,7 @@ defineExpose({ ...@@ -166,7 +166,7 @@ defineExpose({
<template> <template>
<Transition name="history-menu"> <Transition name="history-menu">
<div v-show="isShowHistoryMenu" class="absolute bottom-0 right-[24px] top-0 z-10 py-[24px]"> <div v-show="isShowHistoryMenu" class="absolute bottom-0 right-[24px] top-0 z-10 py-[24px]">
<div class="box-border h-full w-[249px] rounded-[10px] bg-[#ECEFFF] py-[15px] pl-[10px]"> <div class="box-border h-full w-[269px] rounded-[10px] bg-[#ECEFFF] py-[15px] pl-[10px]">
<n-scrollbar> <n-scrollbar>
<div class="pr-[10px]"> <div class="pr-[10px]">
<!-- <div class="select-none"> <!-- <div class="select-none">
...@@ -263,7 +263,7 @@ defineExpose({ ...@@ -263,7 +263,7 @@ defineExpose({
</Transition> </Transition>
<button <button
class="flex-center absolute right-[278px] top-1/2 z-10 h-[24px] w-[24px] rounded-full bg-[#ECEFFF] transition-[right] duration-300 ease-in-out" class="flex-center absolute right-[298px] top-1/2 z-10 h-[24px] w-[24px] rounded-full bg-[#ECEFFF] transition-[right] duration-300 ease-in-out"
:class="[isShowHistoryMenu ? [] : ['!right-[5px]']]" :class="[isShowHistoryMenu ? [] : ['!right-[5px]']]"
@click="handleShowHistoryMenuSwitch" @click="handleShowHistoryMenuSwitch"
> >
......
...@@ -57,9 +57,9 @@ const currentFetchEventSourceController = ref<AbortController | null>(null) ...@@ -57,9 +57,9 @@ const currentFetchEventSourceController = ref<AbortController | null>(null)
// }) // })
const homeContainerWidthWatchDebounce = debounce((newWidth) => { const homeContainerWidthWatchDebounce = debounce((newWidth) => {
if (newWidth <= 1060) { if (newWidth <= 1120) {
isShowHistoryMenu.value = false isShowHistoryMenu.value = false
} else if (newWidth >= 1300) { } else if (newWidth >= 1320) {
isShowHistoryMenu.value = true isShowHistoryMenu.value = true
} }
}, 300) }, 300)
...@@ -201,7 +201,7 @@ function onGetMessageRecordList(recordId: string) { ...@@ -201,7 +201,7 @@ function onGetMessageRecordList(recordId: string) {
<div ref="homeContainerRef" class="relative h-full min-h-[650px] w-full"> <div ref="homeContainerRef" class="relative h-full min-h-[650px] w-full">
<div <div
class="bg-px-home-home_bg-png relative h-full w-full bg-contain bg-center bg-no-repeat pr-0 transition-[padding] duration-300 ease-in-out" class="bg-px-home-home_bg-png relative h-full w-full bg-contain bg-center bg-no-repeat pr-0 transition-[padding] duration-300 ease-in-out"
:class="{ '!pr-[273px]': isShowHistoryMenu }" :class="{ '!pr-[293px]': isShowHistoryMenu }"
> >
<div class="mx-auto flex h-full w-[750px] flex-col px-[5px] py-[40px]"> <div class="mx-auto flex h-full w-[750px] flex-col px-[5px] py-[40px]">
<AgentAbout <AgentAbout
......
...@@ -50,6 +50,9 @@ export default function fetchEventStreamSource( ...@@ -50,6 +50,9 @@ export default function fetchEventStreamSource(
data.message && options.onmessage && options.onmessage(data.message) data.message && options.onmessage && options.onmessage(data.message)
} else { } else {
options.onerror && options.onerror(new Error(data.message)) options.onerror && options.onerror(new Error(data.message))
controller.abort()
options.onclose && options.onclose()
} }
} catch (error) { } catch (error) {
options.onerror && options.onerror(error as Error) options.onerror && options.onerror(error as Error)
......
...@@ -424,16 +424,13 @@ function handleEmailCodeGain() { ...@@ -424,16 +424,13 @@ function handleEmailCodeGain() {
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div> <div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="text-end"> <div class="text-end">
<n-button <button
v-show="!isShowCountdown" v-show="!isShowCountdown"
class="!text-[11px]" class="cursor-pointer text-[11px] text-[#333] transition active:text-[#6A6A6A]"
type="tertiary"
size="small"
@click="handleSMSCodeGain" @click="handleSMSCodeGain"
> >
{{ t('login_module.get_verification_code') }} {{ t('login_module.get_verification_code') }}
</n-button> </button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center"> <div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown <n-countdown
ref="countdownRef" ref="countdownRef"
...@@ -498,15 +495,13 @@ function handleEmailCodeGain() { ...@@ -498,15 +495,13 @@ function handleEmailCodeGain() {
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div> <div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="text-end"> <div class="text-end">
<n-button <button
v-show="!isShowCountdown" v-show="!isShowCountdown"
class="!text-[11px]" class="cursor-pointer text-[11px] text-[#333] transition active:text-[#6A6A6A]"
type="tertiary"
size="small"
@click="handleEmailCodeGain" @click="handleEmailCodeGain"
> >
{{ t('login_module.get_verification_code') }} {{ t('login_module.get_verification_code') }}
</n-button> </button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center"> <div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown <n-countdown
......
<script setup lang="ts"> <script setup lang="ts">
import { fetchEmailCode, fetchUserInfoUpdate, fetchVerifyCode } from '@/apis/user'
import { useUserStore } from '@/store/modules/user'
import { ss } from '@/utils/storage'
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui' import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
import isEmail from 'validator/es/lib/isEmail'
import { onMounted, ref, shallowReadonly, useTemplateRef } from 'vue' import { onMounted, ref, shallowReadonly, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/modules/user'
import isEmail from 'validator/es/lib/isEmail'
import { fetchEmailCode, fetchUserInfoUpdate, fetchVerifyCode } from '@/apis/user'
import { ss } from '@/utils/storage'
const { t } = useI18n() const { t } = useI18n()
const userStore = useUserStore() const userStore = useUserStore()
const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef') const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef') const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
...@@ -145,6 +146,7 @@ function handleSMSCodeGain() { ...@@ -145,6 +146,7 @@ function handleSMSCodeGain() {
:bordered="false" :bordered="false"
size="medium" size="medium"
closable closable
content-style="margin-top: 20px;"
@close="() => (isShowMailboxBindingModal = false)" @close="() => (isShowMailboxBindingModal = false)"
> >
<n-form <n-form
...@@ -153,16 +155,13 @@ function handleSMSCodeGain() { ...@@ -153,16 +155,13 @@ function handleSMSCodeGain() {
label-width="auto" label-width="auto"
:model="emailInfoForm" :model="emailInfoForm"
:rules="emailInfoFormRules" :rules="emailInfoFormRules"
size="large"
> >
<n-form-item :label="t('common_module.email')" path="email"> <n-form-item :label="t('common_module.email')" path="email">
<n-input v-model:value="emailInfoForm.email" :placeholder="t('login_module.please_enter_your_email_address')"> <n-input v-model:value="emailInfoForm.email" :placeholder="t('login_module.please_enter_your_email_address')">
<template #suffix> <template #suffix>
<span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span> <span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span>
<span <span v-show="!isShowCountdown" class="cursor-pointer text-[12px] text-[#333]" @click="handleSMSCodeGain">
v-show="!isShowCountdown"
class="text-theme-color cursor-pointer text-[12px]"
@click="handleSMSCodeGain"
>
{{ t('login_module.get_verification_code') }} {{ t('login_module.get_verification_code') }}
</span> </span>
...@@ -190,10 +189,16 @@ function handleSMSCodeGain() { ...@@ -190,10 +189,16 @@ function handleSMSCodeGain() {
<template #footer> <template #footer>
<div class="text-end"> <div class="text-end">
<n-space justify="end"> <n-space justify="end">
<n-button @click="() => (isShowMailboxBindingModal = false)"> <n-button class="!h-[34px] !w-[96px]" round @click="() => (isShowMailboxBindingModal = false)">
{{ t('common_module.cancel_btn_text') }} {{ t('common_module.cancel_btn_text') }}
</n-button> </n-button>
<n-button type="primary" :loading="mailboxBindingSubmitBtnLoading" @click="handleMailboxBindingSubmit"> <n-button
class="!h-[34px] !w-[96px]"
color="#6F77FF"
round
:loading="mailboxBindingSubmitBtnLoading"
@click="handleMailboxBindingSubmit"
>
{{ t('common_module.confirm_btn_text') }} {{ t('common_module.confirm_btn_text') }}
</n-button> </n-button>
</n-space> </n-space>
......
<script setup lang="ts"> <script setup lang="ts">
import { fetchEmailCode, fetchSMSCode, fetchUserPasswordUpdate, fetchVerifyCode } from '@/apis/user' import { fetchEmailCode, fetchSMSCode, fetchUserPasswordUpdate, fetchVerifyCode } from '@/apis/user'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { ss } from '@/utils/storage' import { ss } from '@/utils/storage'
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui' import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
...@@ -8,9 +9,11 @@ import { onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vu ...@@ -8,9 +9,11 @@ import { onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vu
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n()
const userStore = useUserStore()
const systemLanguageStore = useSystemLanguageStore()
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef') const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef') const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const userStore = useUserStore()
const isShowPasswordChangeModal = defineModel<boolean>('isShowPasswordChangeModal', { default: false }) const isShowPasswordChangeModal = defineModel<boolean>('isShowPasswordChangeModal', { default: false })
...@@ -51,7 +54,7 @@ const passwordFormRules = shallowReadonly({ ...@@ -51,7 +54,7 @@ const passwordFormRules = shallowReadonly({
validator: (_rule: FormItemRule, value: string) => { validator: (_rule: FormItemRule, value: string) => {
if (!value) { if (!value) {
return new Error(t('personal_settings_module.please_enter_your_new_password')) return new Error(t('personal_settings_module.please_enter_your_new_password'))
} else if (value.length <= 6) { } else if (value.length < 6) {
return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters')) return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters'))
} }
...@@ -186,11 +189,12 @@ function handleSMSCodeGain() { ...@@ -186,11 +189,12 @@ function handleSMSCodeGain() {
<template> <template>
<n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave"> <n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card <n-card
class="!w-[600px]" :class="systemLanguageStore.currentLanguage === 'en' ? '!w-[700px]' : '!w-[600px]'"
:title="t('personal_settings_module.password_change')" :title="t('personal_settings_module.password_change')"
:bordered="false" :bordered="false"
size="medium" size="medium"
closable closable
content-style="margin-top: 20px;"
@close="() => (isShowPasswordChangeModal = false)" @close="() => (isShowPasswordChangeModal = false)"
> >
<n-form <n-form
...@@ -199,6 +203,7 @@ function handleSMSCodeGain() { ...@@ -199,6 +203,7 @@ function handleSMSCodeGain() {
label-width="auto" label-width="auto"
:model="passwordInfoForm" :model="passwordInfoForm"
:rules="passwordFormRules" :rules="passwordFormRules"
size="large"
> >
<n-form-item :label="t('personal_settings_module.obtaining_the_verification_code')"> <n-form-item :label="t('personal_settings_module.obtaining_the_verification_code')">
<div> <div>
...@@ -265,10 +270,16 @@ function handleSMSCodeGain() { ...@@ -265,10 +270,16 @@ function handleSMSCodeGain() {
<template #footer> <template #footer>
<div class="text-end"> <div class="text-end">
<n-space justify="end"> <n-space justify="end">
<n-button @click="() => (isShowPasswordChangeModal = false)"> <n-button class="!h-[34px] !w-[96px]" round @click="() => (isShowPasswordChangeModal = false)">
{{ t('common_module.cancel_btn_text') }} {{ t('common_module.cancel_btn_text') }}
</n-button> </n-button>
<n-button type="primary" :loading="passwordChangeSubmitBtnLoading" @click="handlePasswordChangeSubmit"> <n-button
class="!h-[34px] !w-[96px]"
color="#6F77FF"
round
:loading="passwordChangeSubmitBtnLoading"
@click="handlePasswordChangeSubmit"
>
{{ t('common_module.confirm_btn_text') }} {{ t('common_module.confirm_btn_text') }}
</n-button> </n-button>
</n-space> </n-space>
......
...@@ -175,13 +175,15 @@ function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit ...@@ -175,13 +175,15 @@ function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit
</h4> </h4>
<div v-if="userInfoFormItemEdit.nickName" class="flex"> <div v-if="userInfoFormItemEdit.nickName" class="flex">
<div class="flex w-[220px] items-center"> <div class="flex w-[330px] items-center">
<n-input <n-input
ref="inputRefs" ref="inputRefs"
v-model:value="userInfoForm.nickName" v-model:value="userInfoForm.nickName"
:placeholder="t('personal_settings_module.please_enter_the_account_nickname')" :placeholder="t('personal_settings_module.please_enter_the_account_nickname')"
type="text" type="text"
size="small" size="small"
maxlength="20"
show-count
/> />
</div> </div>
......
<script setup lang="ts">
import AgentSetting from './agent-setting/agent-setting.vue'
import AgentPreview from './agent-preview/agent-preview.vue'
</script>
<template>
<div class="flex h-full w-full flex-1">
<AgentSetting />
<AgentPreview />
</div>
</template>
...@@ -3,7 +3,7 @@ import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' ...@@ -3,7 +3,7 @@ import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { Close, Delete } from '@icon-park/vue-next' import { Close, Delete } from '@icon-park/vue-next'
import { DataTableColumns } from 'naive-ui' import { DataTableColumns } from 'naive-ui'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { MemoryVariableForm } from './memory-variable-modal.vue' import { MemoryVariableForm } from '../../components/memory-variable-modal.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { import {
fetchDeleteAllLongMemory, fetchDeleteAllLongMemory,
......
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { useUserStore } from '@/store/modules/user'
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
}
const props = defineProps<Props>()
const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
const { t } = useI18n()
const userStore = useUserStore()
const personalAppConfigStore = usePersonalAppConfigStore()
const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
})
const assistantAvatar = computed(() => {
return personalAppConfigStore.baseInfo.agentAvatar
})
const isShowAudioControl = computed(() => {
return props.role === 'assistant' && !props.messageItem.isVoiceLoading
})
const isPlayableAudio = computed(() => {
return isShowAudioControl.value && !!props.messageItem.voiceFragmentUrlList.length
})
const isShowVoiceLoading = computed(() => {
return props.role === 'assistant' && props.messageItem.isVoiceLoading && props.messageItem.isVoiceEnabled
})
function handleAudioControl() {
if (!isPlayableAudio.value) {
return
}
if (props.messageItem.isVoicePlaying) {
emit('audioPause')
} else {
emit('audioPlay')
}
}
</script>
<template>
<div class="mb-5 flex last:mb-0">
<NImage
:src="role === 'user' ? useAvatar : assistantAvatar"
preview-disabled
:width="32"
:height="32"
object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full"
/>
<div class="flex flex-col items-start">
<div
class="min-w-[80px] max-w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent
"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading />
</div>
</div>
</div>
<div
v-show="isShowAudioControl"
class="text-font-color flex items-center gap-0.5"
:class="isPlayableAudio ? 'hover:text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed'"
@click="handleAudioControl"
>
<i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" />
<div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" />
<span
v-show="isPlayableAudio"
class="text-[12px]"
:class="messageItem.isVoicePlaying ? 'text-theme-color' : ''"
>
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span>
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
<div v-if="isShowVoiceLoading" class="py-3.5 pl-6">
<CustomLoading />
</div>
</div>
</div>
</template>
...@@ -5,20 +5,25 @@ import ContinueQuestion from './continue-question.vue' ...@@ -5,20 +5,25 @@ import ContinueQuestion from './continue-question.vue'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[] continuousQuestionList: string[]
} }
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 && props.messageList.size > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading !Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
) )
}) })
...@@ -31,10 +36,12 @@ defineExpose({ ...@@ -31,10 +36,12 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5"> <main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div> <div>
<MessageItem <MessageItem
v-for="messageItem in messageList" v-for="[key, messageItem] in messageList"
:key="messageItem.timestamp" :key="key"
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/> />
</div> </div>
......
...@@ -8,8 +8,8 @@ import { useThrottleFn } from '@vueuse/core' ...@@ -8,8 +8,8 @@ import { useThrottleFn } from '@vueuse/core'
import CustomIcon from '@/components/custom-icon/custom-icon.vue' import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { Help, People, RightOne } from '@icon-park/vue-next' import { Help, People, RightOne } from '@icon-park/vue-next'
import UploadImage from '@/components/upload-image/upload-image.vue' import UploadImage from '@/components/upload-image/upload-image.vue'
import AutoConfigModal from './auto-config-modal.vue' import AutoConfigModal from './components/auto-config-modal.vue'
import OptimizeSystemModal from './optimize-system-modal.vue' import OptimizeSystemModal from './components/optimize-system-modal.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { import {
...@@ -20,10 +20,11 @@ import { ...@@ -20,10 +20,11 @@ import {
fetchSaveAgentApplication, fetchSaveAgentApplication,
} from '@/apis/agent-application' } from '@/apis/agent-application'
import { fetchCustomEventSource } from '@/composables/useEventSource' import { fetchCustomEventSource } from '@/composables/useEventSource'
import AgentModelSetting from './agent-model-setting.vue' import AgentModelSetting from './components/agent-model-setting.vue'
import AgentAssociatedKnowledge from './agent-associated-knowledge.vue' import AgentAssociatedKnowledge from './components/agent-associated-knowledge.vue'
import AgentMemorySetting from './agent-memory-setting.vue' import AgentMemorySetting from './components/agent-memory-setting.vue'
import AgentDialogueSetting from './agent-dialogue-setting.vue' import AgentDialogueSetting from './components/agent-dialogue-setting.vue'
import AgentRoleSetting from './components/agent-role-setting.vue'
const { t } = useI18n() const { t } = useI18n()
...@@ -74,10 +75,10 @@ watch( ...@@ -74,10 +75,10 @@ watch(
() => personalAppConfigStore.$state, () => personalAppConfigStore.$state,
() => { () => {
if (!baseInfo.value.agentId) { if (!baseInfo.value.agentId) {
handleUpdatePersonalAppId() handleCreatePersonalAgent()
} }
}, },
{ deep: true, once: true }, { deep: true },
) )
watch( watch(
...@@ -114,7 +115,7 @@ const handleSavePersonalAppConfig = useThrottleFn( ...@@ -114,7 +115,7 @@ const handleSavePersonalAppConfig = useThrottleFn(
true, true,
) )
// 保存应用配置 // 更新保存应用配置
async function handleSaveAgentApplication() { async function handleSaveAgentApplication() {
if (!baseInfo.value.agentTitle) { if (!baseInfo.value.agentTitle) {
return return
...@@ -123,8 +124,8 @@ async function handleSaveAgentApplication() { ...@@ -123,8 +124,8 @@ async function handleSaveAgentApplication() {
await fetchSaveAgentApplication<PersonalAppConfigState>(personalAppConfigStore.$state) await fetchSaveAgentApplication<PersonalAppConfigState>(personalAppConfigStore.$state)
} }
// 更新保存应用ID // 新建应用
async function handleUpdatePersonalAppId() { async function handleCreatePersonalAgent() {
if (!baseInfo.value.agentTitle) { if (!baseInfo.value.agentTitle) {
return return
} }
...@@ -250,10 +251,14 @@ async function handleSettingAgent(autoConfigInputValue: string) { ...@@ -250,10 +251,14 @@ async function handleSettingAgent(autoConfigInputValue: string) {
handleAIGeneratePreamble(), handleAIGeneratePreamble(),
handleAIGenerateFeaturedQuestions(), handleAIGenerateFeaturedQuestions(),
handleAIGenerateAgentSystem(), handleAIGenerateAgentSystem(),
]).finally(() => { ])
.finally(() => {
isFullScreenLoading.value = false isFullScreenLoading.value = false
emitter?.emit('resetAgent') emitter?.emit('resetAgent')
}) })
.catch(() => {
handleStopGenerate()
})
}) })
.catch(() => { .catch(() => {
isFullScreenLoading.value = false isFullScreenLoading.value = false
...@@ -558,14 +563,15 @@ function handleStopGenerate() { ...@@ -558,14 +563,15 @@ function handleStopGenerate() {
</div> </div>
</div> </div>
<div class="h-full flex-1 overflow-auto border-r border-[#e8e9eb] py-4"> <div class="flex h-full flex-1 flex-col overflow-auto border-r border-[#e8e9eb] py-4">
<div class="flex h-6 items-center px-5 leading-6"> <div class="mb-1 flex h-6 items-center px-5 leading-6">
<CustomIcon icon="streamline:decent-work-and-economic-growth-solid" class="mr-1.5 text-base" /> <CustomIcon icon="streamline:decent-work-and-economic-growth-solid" class="mr-1.5 text-base" />
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.ability_expand') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.ability_expand') }}
</span> </span>
</div> </div>
<div class="flex-1 overflow-auto">
<AgentAssociatedKnowledge v-model:knowledge-config="knowledgeConfig" /> <AgentAssociatedKnowledge v-model:knowledge-config="knowledgeConfig" />
<AgentMemorySetting <AgentMemorySetting
...@@ -581,6 +587,9 @@ function handleStopGenerate() { ...@@ -581,6 +587,9 @@ function handleStopGenerate() {
@generate-preamble="handleAIGeneratePreamble" @generate-preamble="handleAIGeneratePreamble"
@generate-featured-questions="handleAIGenerateFeaturedQuestions" @generate-featured-questions="handleAIGenerateFeaturedQuestions"
/> />
<AgentRoleSetting />
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n' ...@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
import { Plus, RightOne } from '@icon-park/vue-next' import { Plus, RightOne } from '@icon-park/vue-next'
import { fetchGetKnowledgeListByKdIds } from '@/apis/knowledge' import { fetchGetKnowledgeListByKdIds } from '@/apis/knowledge'
import AssociatedKnowledgeModal from './associated-knowledge-modal.vue' import AssociatedKnowledgeModal from './associated-knowledge-modal.vue'
import { KnowledgeItem } from '../../personal-knowledge/types' import { KnowledgeItem } from '@/views/personal-space/personal-knowledge/types'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
const { t } = useI18n() const { t } = useI18n()
...@@ -92,7 +92,7 @@ function handleUpdateDocumentParsing(value: boolean) { ...@@ -92,7 +92,7 @@ function handleUpdateDocumentParsing(value: boolean) {
<template> <template>
<section class="border-b border-[#e8e9eb] px-5"> <section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4"> <div class="pt-4">
<h2 class="my-3 text-[#84868c]"> <h2 class="mb-3 text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.knowledge') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.knowledge') }}
</h2> </h2>
......
...@@ -61,7 +61,7 @@ function handleAIGenerateFeaturedQuestions() { ...@@ -61,7 +61,7 @@ function handleAIGenerateFeaturedQuestions() {
<template> <template>
<section class="border-b border-[#e8e9eb] px-5"> <section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4"> <div class="pt-4">
<h2 class="my-3 text-[#84868c]"> <h2 class="mb-3 text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.dialogue') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.dialogue') }}
</h2> </h2>
<NCollapse <NCollapse
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Plus, Help, MoreOne, Edit, Copy, ReduceOne, RightOne } from '@icon-park/vue-next' import { Plus, Help, MoreOne, Edit, Copy, ReduceOne, RightOne } from '@icon-park/vue-next'
import MemoryVariableModal, { MemoryVariableForm } from './memory-variable-modal.vue' import MemoryVariableModal, { MemoryVariableForm } from '../../components/memory-variable-modal.vue'
import { VariableStructureItem } from '@/store/types/personal-app-config' import { VariableStructureItem } from '@/store/types/personal-app-config'
import { copyToClip } from '@/utils/copy' import { copyToClip } from '@/utils/copy'
...@@ -69,7 +69,7 @@ function handleChangeMemoryFragmentState(value: boolean) { ...@@ -69,7 +69,7 @@ function handleChangeMemoryFragmentState(value: boolean) {
<template> <template>
<section class="border-b border-[#e8e9eb] px-5"> <section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4"> <div class="pt-4">
<h2 class="my-3 text-[#84868c]"> <h2 class="mb-3 text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.memory') }}
</h2> </h2>
<NCollapse <NCollapse
......
...@@ -4,11 +4,14 @@ import { SelectOption } from 'naive-ui' ...@@ -4,11 +4,14 @@ import { SelectOption } from 'naive-ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Help, Down } from '@icon-park/vue-next' import { Help, Down } from '@icon-park/vue-next'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { DiversityModeItem, diversityModeList } from '@/data/agent-setting-data' import { DiversityModeItem, diversityModeList } from '@/data/agent-setting-data'
import { fetchGetLargeModelInfo, fetchGetLargeModelList } from '@/apis/agent-application' import { fetchGetLargeModelInfo, fetchGetLargeModelList } from '@/apis/agent-application'
const { t } = useI18n() const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
let modalListOptions = reactive<SelectOption[]>([]) let modalListOptions = reactive<SelectOption[]>([])
let modalListRenderLabel: (option: SelectOption) => VNodeChild let modalListRenderLabel: (option: SelectOption) => VNodeChild
...@@ -17,6 +20,13 @@ const commModelConfig = defineModel<PersonalAppConfigState['commModelConfig']>(' ...@@ -17,6 +20,13 @@ const commModelConfig = defineModel<PersonalAppConfigState['commModelConfig']>('
const currentLargeModelIcon = ref('') const currentLargeModelIcon = ref('')
const currentDiversityMode = ref('balance') const currentDiversityMode = ref('balance')
const modelSettingWidth = ref(systemLanguageStore.currentLanguageInfo.key === 'en' ? '508px' : '420px')
const sliderLabelWidth = ref(systemLanguageStore.currentLanguageInfo.key === 'en' ? '158px' : '105px')
const isEnLanguage = computed(() => {
return systemLanguageStore.currentLanguageInfo.key === 'en'
})
const isDisabledCommModelConfig = computed(() => { const isDisabledCommModelConfig = computed(() => {
return currentDiversityMode.value !== 'custom' return currentDiversityMode.value !== 'custom'
}) })
...@@ -107,7 +117,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -107,7 +117,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<template> <template>
<div> <div>
<NPopover placement="bottom" trigger="click" style="width: 420px"> <NPopover placement="bottom" trigger="click" :style="{ width: modelSettingWidth }">
<template #trigger> <template #trigger>
<div <div
class="hover:border-theme-color flex cursor-pointer items-center justify-between rounded-md border border-[#d4d6d9] px-3 py-[7px]" class="hover:border-theme-color flex cursor-pointer items-center justify-between rounded-md border border-[#d4d6d9] px-3 py-[7px]"
...@@ -119,11 +129,14 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -119,11 +129,14 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<Down theme="outline" size="16" fill="#333" class="ml-1.5 text-base outline-none" /> <Down theme="outline" size="16" fill="#333" class="ml-1.5 text-base outline-none" />
</div> </div>
</template> </template>
<div class="mb-2 mt-[6px] flex items-center"> <div class="flex items-center" :class="isEnLanguage ? 'my-[18px] justify-between' : 'mb-2 mt-1.5 justify-start'">
<span class="font-500 mr-3 text-sm text-[#151b26]"> <span class="font-500 mr-3 text-sm text-[#151b26]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model') }}
</span> </span>
<span class="rounded bg-[#f2f5f9] px-1 text-xs text-[#5c5f66]"> <span
class="rounded-theme bg-[#f2f5f9] text-xs text-[#5c5f66]"
:class="isEnLanguage ? 'px-[13px] py-2' : 'px-1'"
>
{{ {{
t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model_desc') t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model_desc')
}} }}
...@@ -141,7 +154,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -141,7 +154,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.generate_diversity') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.generate_diversity') }}
</span> </span>
<ul class="rounded-theme mt-2 grid grid-cols-4 overflow-hidden"> <ul class="rounded-theme grid grid-cols-4 overflow-hidden" :class="isEnLanguage ? 'mt-3' : 'mt-2'">
<li <li
v-for="(diversityModeItem, index) in diversityModeList" v-for="(diversityModeItem, index) in diversityModeList"
:key="index" :key="index"
...@@ -159,8 +172,8 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -159,8 +172,8 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
</div> </div>
<div class="mt-4 text-xs"> <div class="mt-4 text-xs">
<div class="mb-2.5 flex h-[34px] items-center justify-between"> <div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex w-[105px] items-center"> <div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.topP') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.topP') }}
</span> </span>
...@@ -202,12 +215,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -202,12 +215,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:max="1" :max="1"
:disabled="isDisabledCommModelConfig" :disabled="isDisabledCommModelConfig"
size="small" size="small"
class="w-[90px]! text-xs!" class="common-model-config-input-number w-[90px]!"
/> />
</div> </div>
<div class="mb-2.5 flex h-[34px] items-center justify-between"> <div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex w-[105px] items-center"> <div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.temperature') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.temperature') }}
</span> </span>
...@@ -251,12 +264,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -251,12 +264,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:max="1" :max="1"
:disabled="isDisabledCommModelConfig" :disabled="isDisabledCommModelConfig"
size="small" size="small"
class="w-[90px]! text-xs!" class="common-model-config-input-number w-[90px]!"
/> />
</div> </div>
<div class="mb-2.5 flex h-[34px] items-center justify-between"> <div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex w-[105px] items-center"> <div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span> <span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.communication_turn') }} {{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.communication_turn') }}
</span> </span>
...@@ -296,7 +309,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -296,7 +309,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:min="0" :min="0"
:max="100" :max="100"
size="small" size="small"
class="w-[90px]!" class="common-model-config-input-number w-[90px]!"
placeholder="" placeholder=""
/> />
</div> </div>
...@@ -312,4 +325,10 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) { ...@@ -312,4 +325,10 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
border-radius: 6px; border-radius: 6px;
} }
} }
.common-model-config-input-number {
:deep(.n-input__input) {
font-size: 12px;
}
}
</style> </style>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { Plus, RightOne, MoreOne, Edit, ReduceOne } from '@icon-park/vue-next'
import TimbreSettingModal from './timbre-setting-modal.vue'
import { TimbreLanguageInfoItem } from '../../types'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { fetchGetTimbreInfoDetail } from '@/apis/timber'
const { t } = useI18n()
const personalAppConfigStore = usePersonalAppConfigStore()
const { voiceConfig } = storeToRefs(personalAppConfigStore)
const roleConfigExpandedNames = ref<string[]>([])
const isShowTimbreSettingModal = ref(false)
const timbreInfoDetail = ref<TimbreLanguageInfoItem>()
let timbreInfo = reactive<TimbreLanguageInfoItem>({ language: 0, matchLang: '', timbreInfo: [] })
const timberFullName = computed(() => {
const languageOptions = [
{ label: 'common_module.mandarin', value: 'zh-CN' },
{ label: 'common_module.cantonese', value: 'zh-HK' },
{ label: 'common_module.english', value: 'en' },
]
const currentLanguage = languageOptions.find((item) => item.value === timbreInfoDetail.value?.matchLang)
return `${timbreInfoDetail.value?.timbreInfo?.[0]?.timbreName || '--'}(${t(currentLanguage?.label || 'common_module.sound')})`
})
const isHasTimbreId = computed(() => !!voiceConfig.value.timbreId)
onMounted(() => {
voiceConfig.value.timbreId && handleGetTimberInfoDetail()
})
async function handleGetTimberInfoDetail() {
const res = await fetchGetTimbreInfoDetail<TimbreLanguageInfoItem>(voiceConfig.value.timbreId)
if (res.code === 0) {
timbreInfoDetail.value = res.data
roleConfigExpandedNames.value = ['timbre']
}
}
function handleUpdateRoleConfigExpandedNames(expandedNames: string[]) {
roleConfigExpandedNames.value = expandedNames
}
function handleShowAssociatedTimbreModel() {
if (voiceConfig.value.timbreId) {
return
}
isShowTimbreSettingModal.value = true
timbreInfo = { language: 0, matchLang: '', timbreInfo: [] }
}
function handleUpdateTimbreId(timberId: string) {
isShowTimbreSettingModal.value = false
voiceConfig.value.timbreId = timberId
voiceConfig.value.defaultOpen = 'Y'
handleGetTimberInfoDetail()
}
function handleRemoveTimbreId() {
voiceConfig.value.timbreId = ''
}
function handleEditAssociatedTimbreModel() {
isShowTimbreSettingModal.value = true
timbreInfo = timbreInfoDetail.value!
}
</script>
<template>
<section class="border-b border-[#e8e9eb] px-5">
<div class="pt-4">
<h2 class="mb-3 text-[#84868c]">{{ t('common_module.role') }}</h2>
<NCollapse
:expanded-names="roleConfigExpandedNames"
:trigger-areas="['main', 'arrow']"
@update:expanded-names="handleUpdateRoleConfigExpandedNames"
>
<template #arrow>
<RightOne theme="filled" size="17" fill="#333" :stroke-width="3" />
</template>
<NCollapseItem :title="t('common_module.voice')" name="timbre" class="my-[13px]!">
<template #header-extra>
<NTooltip trigger="hover">
<template #trigger>
<Plus
theme="outline"
size="22"
:stroke-width="3"
class="text-theme-color"
:class="isHasTimbreId ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleShowAssociatedTimbreModel"
/>
</template>
{{
isHasTimbreId
? t(
'personal_space_module.agent_module.agent_setting_module.agent_config_module.currently_only_one_voice_can_be_set',
)
: t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice')
}}
</NTooltip>
</template>
<span v-show="!voiceConfig.timbreId" class="text-xs text-[#84868c]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice_desc') }}
</span>
<div class="flex flex-1 flex-wrap items-center gap-[12px] overflow-hidden">
<div
v-show="voiceConfig.timbreId"
class="font-400 line-height-[20px] flex cursor-pointer items-center rounded-[4px] bg-[#f2f5f9] py-[2px] pl-[8px] text-[12px] hover:bg-[#e3e8f0]"
@click="handleEditAssociatedTimbreModel"
>
<div class="max-w-[205px] truncate text-[#151b26]">{{ timberFullName }}</div>
<n-popover placement="bottom" trigger="hover" :show-arrow="false" class="p-[4px]!">
<template #trigger>
<MoreOne theme="outline" size="14" fill="#333" :stroke-width="3" class="mr-[4px] mt-[2px]" />
</template>
<div class="text-[12px]">
<div
class="flex h-[30px] w-[90px] cursor-pointer items-center justify-start px-[8px] py-[5px] hover:rounded-[4px] hover:bg-[#f2f5f9]"
@click="handleEditAssociatedTimbreModel"
>
<Edit theme="outline" size="16" fill="#333" :stroke-width="3" /><span class="ml-[4px]">
{{
t(
'personal_space_module.agent_module.agent_setting_module.agent_config_module.memory_variable_action_edit',
)
}}
</span>
</div>
<n-space>
<div
class="flex h-[30px] w-[90px] cursor-pointer items-center justify-start px-[8px] py-[5px] hover:rounded-[4px] hover:bg-[#f2f5f9]"
@click="handleRemoveTimbreId"
>
<ReduceOne theme="outline" size="16" fill="#333" :stroke-width="3" />
<span class="ml-[4px]">
{{ t('common_module.delete') }}
</span>
</div>
</n-space>
</div>
</n-popover>
</div>
</div>
</NCollapseItem>
</NCollapse>
</div>
</section>
<TimbreSettingModal
v-model:is-show-modal="isShowTimbreSettingModal"
:btn-loading="false"
:modal-title="t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice')"
:timbre-info="timbreInfo"
@confirm="handleUpdateTimbreId"
/>
</template>
...@@ -7,10 +7,11 @@ import { usePagination } from '@/composables/usePagination.ts' ...@@ -7,10 +7,11 @@ import { usePagination } from '@/composables/usePagination.ts'
import CustomModal from '@/components/custom-modal/custom-modal.vue' import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { fetchCreateKnowledge, fetchGetKnowledgeList } from '@/apis/knowledge' import { fetchCreateKnowledge, fetchGetKnowledgeList } from '@/apis/knowledge'
import { KnowledgeDocumentItem, KnowledgeItem } from '@/views/personal-space/personal-knowledge/types' import { KnowledgeDocumentItem, KnowledgeItem } from '@/views/personal-space/personal-knowledge/types'
import { TrainStatus } from '@/views/personal-space/personal-knowledge/types.d'
import { formatDateTime } from '@/utils/date-formatter' import { formatDateTime } from '@/utils/date-formatter'
import CreateKnowledgeModal, { import CreateKnowledgeModal, {
KnowledgeFormDataInterface, KnowledgeFormDataInterface,
} from '../../personal-knowledge/components/create-knowledge-modal.vue' } from '@/views/personal-space/personal-knowledge/components/create-knowledge-modal.vue'
interface Props { interface Props {
isShowModal: boolean isShowModal: boolean
...@@ -95,7 +96,7 @@ async function handleGetKnowledgeList() { ...@@ -95,7 +96,7 @@ async function handleGetKnowledgeList() {
paginationData.pageSize = 999999 paginationData.pageSize = 999999
knowledgeListLoading.value = true knowledgeListLoading.value = true
const res = await fetchGetKnowledgeList<KnowledgeItem[]>('', searchKnowledgeInputValue.value, { const res = await fetchGetKnowledgeList<KnowledgeItem[]>(TrainStatus.COMPLETE, searchKnowledgeInputValue.value, {
pagingInfo: paginationData, pagingInfo: paginationData,
}) })
...@@ -171,6 +172,11 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD ...@@ -171,6 +172,11 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD
<template #content> <template #content>
<div class="flex w-full justify-end px-3"> <div class="flex w-full justify-end px-3">
<div class="gap-4.5 flex items-center"> <div class="gap-4.5 flex items-center">
<i
class="iconfont icon-shuaxin text-gray-font-color hover:text-font-color cursor-pointer"
@click="handleGetKnowledgeList"
/>
<NInput <NInput
v-model:value="searchKnowledgeInputValue" v-model:value="searchKnowledgeInputValue"
:placeholder="t('personal_space_module.knowledge_module.search_knowledge_placeholder')" :placeholder="t('personal_space_module.knowledge_module.search_knowledge_placeholder')"
......
<script setup lang="ts">
import { computed, h, readonly, ref, shallowRef, watch } from 'vue'
import { SelectOption, SelectRenderTag } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { Howl } from 'howler'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { fetchGetTimbreList } from '@/apis/timber'
import { TimbreLanguageInfoItem, TimbreInfoItem } from '../../types'
import { validBrowser } from '@/utils/browser-detection'
interface Props {
modalTitle: string
isShowModal: boolean
btnLoading: boolean
timbreInfo?: TimbreLanguageInfoItem
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'confirm', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
const languageInfoList = ref<TimbreLanguageInfoItem[]>([])
const timbreInfoList = ref<TimbreInfoItem[]>([])
const timbreOptionList = ref<SelectOption[]>([])
const selectedLanguage = ref('')
const selectedTimberId = ref('')
const timberUrl = ref('')
const isPlaySound = ref(false)
const requestDataLoading = ref(false)
const currentSoundCtl = shallowRef<Howl | null>(null)
let timbreRenderTag: SelectRenderTag
const languageOptions = readonly([
{ label: () => h('span', {}, t('common_module.mandarin')), value: 'zh-CN' },
{ label: () => h('span', {}, t('common_module.cantonese')), value: 'zh-HK' },
{ label: () => h('span', {}, t('common_module.english')), value: 'en' },
])
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
handleAudioPause()
emit('update:isShowModal', value)
},
})
const isHasTimberUrl = computed(() => {
return !!timberUrl.value
})
const confirmBtnDisabled = computed(() => {
return requestDataLoading.value || !selectedTimberId.value
})
watch(
() => showModal.value,
(value) => {
value && handleGetTimberList()
},
)
async function handleGetTimberList() {
requestDataLoading.value = true
selectedLanguage.value = props.timbreInfo?.matchLang || systemLanguageStore.currentLanguageInfo.key
const res = await fetchGetTimbreList<TimbreLanguageInfoItem[]>()
if (res.code === 0) {
requestDataLoading.value = false
languageInfoList.value = res.data
timbreInfoList.value =
languageInfoList.value.find((item) => item.matchLang === selectedLanguage.value)?.timbreInfo || []
selectedTimberId.value = props.timbreInfo?.timbreInfo?.[0]?.timbreId || timbreInfoList.value?.[0]?.timbreId || ''
timberUrl.value = props.timbreInfo?.timbreInfo?.[0]?.voiceUrl || timbreInfoList.value?.[0]?.voiceUrl || ''
handleRenderTimbreOption()
}
}
function handleRenderTimbreOption() {
timbreOptionList.value = timbreInfoList.value.map((item) => {
return {
label: () =>
h('div', { class: 'flex items-baseline justify-between w-[180px]' }, [
h('span', { class: 'max-w-[160px] line-clamp-1 whitespace-normal' }, { default: () => item.timbreName }),
h('i', {
class: {
'iconfont icon-playing ': item.isPlaying,
'iconfont icon-stop text-[14px]': !item.isPlaying,
'text-theme-color text-[14px]': true,
},
onClick: (e) => {
e.stopPropagation()
timbreInfoList.value.forEach((mockItem) => {
if (mockItem.timbreId !== item.timbreId) {
mockItem.isPlaying = false
}
})
isPlaySound.value = false
item.isPlaying = !item.isPlaying
if (item.isPlaying) {
handleAudioPlay(item.voiceUrl)
}
},
}),
]),
value: item.timbreId,
nickName: item.timbreName,
timberUrl: item.voiceUrl,
}
})
timbreRenderTag = ({ option }) => {
return h('span', {}, { default: () => option.nickName as string })
}
}
function handleUpdateTimberLanguage(value: string) {
timbreInfoList.value = languageInfoList.value.find((item) => item.matchLang === value)?.timbreInfo || []
selectedTimberId.value = timbreInfoList.value?.[0].timbreId || ''
timberUrl.value = timbreInfoList.value?.[0].voiceUrl || ''
isPlaySound.value = false
handleAudioPause()
handleRenderTimbreOption()
}
function handleUpdateTimber(_value: string, option: SelectOption) {
timberUrl.value = option.timberUrl as string
}
function handleClickTimber() {
isPlaySound.value = !isPlaySound.value
if (isPlaySound.value) {
timbreInfoList.value.forEach((item) => (item.isPlaying = false))
handleAudioPlay(timberUrl.value)
} else {
handleAudioPause()
}
}
function handleUpdateTimberId() {
selectedTimberId.value && emit('confirm', selectedTimberId.value)
}
function howlSoundFactory(url: string) {
const soundCtl = new Howl({
src: [url],
format: ['mpeg'],
html5: !validBrowser(),
preload: true,
autoplay: true,
onplay: () => {
currentSoundCtl.value = soundCtl
},
onend: () => {
soundCtl.unload()
timbreInfoList.value.forEach((timbreItem) => (timbreItem.isPlaying = false))
isPlaySound.value = false
},
})
return soundCtl
}
function handleAudioPlay(audioUrl: string) {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
}
howlSoundFactory(audioUrl)
}
function handleAudioPause() {
if (currentSoundCtl.value) {
currentSoundCtl.value.pause()
currentSoundCtl.value.unload()
currentSoundCtl.value = null
}
timbreInfoList.value.forEach((timbreItem) => (timbreItem.isPlaying = false))
isPlaySound.value = false
}
</script>
<template>
<CustomModal
v-model:is-show="showModal"
:title="modalTitle"
:btn-loading="btnLoading"
:btn-disabled="confirmBtnDisabled"
:width="515"
:height="378"
@confirm="handleUpdateTimberId"
>
<template #content>
<div class="text-gray-font-color mb-2">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.setting_voice_message') }}
</div>
<div v-show="!requestDataLoading" class="flex items-center gap-3">
<n-select
v-model:value="selectedLanguage"
class="w-[211px]!"
:options="languageOptions"
@update:value="handleUpdateTimberLanguage"
/>
<n-select
v-model:value="selectedTimberId"
class="w-[211px]!"
:options="timbreOptionList"
:render-tag="timbreRenderTag"
:show-checkmark="false"
@update:value="handleUpdateTimber"
/>
<div
class="flex h-5 w-5 items-center justify-center rounded-full bg-[#eff0ff]"
:class="isHasTimberUrl ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'"
>
<i
class="iconfont text-theme-color text-[13px] leading-[13px]"
:class="isPlaySound ? 'icon-playing' : 'icon-stop'"
@click="handleClickTimber"
/>
</div>
</div>
<div v-show="requestDataLoading" class="flex items-center gap-3">
<n-skeleton height="32px" />
<n-skeleton height="32px" />
<n-skeleton height="20px" width="20px" circle class="flex-shrink-0" />
</div>
</template>
</CustomModal>
</template>
export interface TimbreInfoItem {
timbreId: string
timbreName: string
voiceUrl: string
isPlaying: boolean
}
export interface TimbreLanguageInfoItem {
language: number
matchLang: string
timbreInfo: TimbreInfoItem[]
}
...@@ -2,14 +2,12 @@ ...@@ -2,14 +2,12 @@
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Computer, PreviewOpen, AllApplication, SettingOne } from '@icon-park/vue-next' import { Computer, PreviewOpen, AllApplication, SettingOne } from '@icon-park/vue-next'
import useTableScrollY from '@/composables/useTableScrollY' import useTableScrollY from '@/composables/useTableScrollY'
import { copyToClip } from '@/utils/copy' import { copyToClip } from '@/utils/copy'
import { formatDateTime } from '@/utils/date-formatter' import { formatDateTime } from '@/utils/date-formatter'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import SaleApplicationsConfigurationModal from '../../personal-app/sale-applications-configuration-modal.vue' import SaleApplicationsConfigurationModal from '@/views/personal-space/personal-app/sale-applications-configuration-modal.vue'
import { import {
fetchGetApplicationInfo, fetchGetApplicationInfo,
fetchGetApplicationMallInfo, fetchGetApplicationMallInfo,
...@@ -82,8 +80,7 @@ function handleClickChannelPublishTableAction(actionType: string) { ...@@ -82,8 +80,7 @@ function handleClickChannelPublishTableAction(actionType: string) {
} }
function handleAccessPage() { function handleAccessPage() {
const channelUrl = `${window.location.origin}/fe/share/web_source/${router.currentRoute.value.params.agentId}` router.push({ name: 'ShareWebApplication', params: { agentId: router.currentRoute.value.params.agentId } })
location.href = channelUrl
} }
function handleCopyShareLink() { function handleCopyShareLink() {
......
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { useUserStore } from '@/store/modules/user'
interface Props {
role: 'user' | 'assistant'
messageItem: ConversationMessageItem
}
const { t } = useI18n()
const userStore = useUserStore()
defineProps<Props>()
const personalAppConfigStore = usePersonalAppConfigStore()
const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
})
const assistantAvatar = computed(() => {
return personalAppConfigStore.baseInfo.agentAvatar
})
</script>
<template>
<div class="mb-5 flex last:mb-0">
<NImage
:src="role === 'user' ? useAvatar : assistantAvatar"
preview-disabled
:width="32"
:height="32"
object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 rounded-full"
/>
<div
class="min-w-[80px] max-w-[calc(100%-32px-12px)] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
>
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
<CustomLoading />
</div>
<div v-else>
<p class="break-all">
<MarkdownRender
:raw-text-content="
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent
"
:color="role === 'user' ? '#fff' : '#192338'"
/>
</p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4">
<CustomLoading />
</div>
</div>
</div>
</div>
</template>
...@@ -4,9 +4,8 @@ import { useRouter } from 'vue-router' ...@@ -4,9 +4,8 @@ import { useRouter } from 'vue-router'
import { Emitter } from 'mitt' import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import PageNarBar from './components/page-narbar.vue' import PageNarBar from './components/page-narbar.vue'
import AppSetting from './components/app-setting.vue' import AgentConfig from './components/agent-config/agent-config.vue'
import AppPreview from './components/app-preview.vue' import AgentPublish from './components/agent-publish.vue'
import AppPublish from './components/app-publish.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { fetchGetDebugApplicationInfo } from '@/apis/agent-application' import { fetchGetDebugApplicationInfo } from '@/apis/agent-application'
...@@ -79,13 +78,11 @@ function handleChangeAgentAppTabKey(currentTabKey: string) { ...@@ -79,13 +78,11 @@ function handleChangeAgentAppTabKey(currentTabKey: string) {
<div class="h-content flex w-full flex-1"> <div class="h-content flex w-full flex-1">
<div v-if="currentAgentAppTabKey === 'config'" class="flex h-full w-full flex-1"> <div v-if="currentAgentAppTabKey === 'config'" class="flex h-full w-full flex-1">
<AppSetting /> <AgentConfig />
<AppPreview />
</div> </div>
<div v-if="currentAgentAppTabKey === 'publish'" class="flex h-full w-full flex-1"> <div v-if="currentAgentAppTabKey === 'publish'" class="flex h-full w-full flex-1">
<AppPublish /> <AgentPublish />
</div> </div>
</div> </div>
......
This diff is collapsed.
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useLayoutConfig } from '@/composables/useLayoutConfig' import { useLayoutConfig } from '@/composables/useLayoutConfig'
...@@ -17,6 +18,11 @@ const { t } = useI18n() ...@@ -17,6 +18,11 @@ const { t } = useI18n()
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
const userStore = useUserStore() const userStore = useUserStore()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
...@@ -31,6 +37,34 @@ const assistantAvatar = computed(() => { ...@@ -31,6 +37,34 @@ const assistantAvatar = computed(() => {
'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp' 'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
) )
}) })
const timbreEnabled = computed(() => {
return !!props.agentApplicationConfig.voiceConfig.timbreId
})
const isShowAudioControl = computed(() => {
return props.role === 'assistant' && !props.messageItem.isVoiceLoading && timbreEnabled.value
})
const isPlayableAudio = computed(() => {
return isShowAudioControl.value && !!props.messageItem.voiceFragmentUrlList.length
})
const isShowWebVoiceLoading = computed(() => {
return props.role === 'assistant' && !isMobile.value && props.messageItem.isVoiceLoading && timbreEnabled.value
})
function handleAudioControl() {
if (!isPlayableAudio.value) {
return
}
if (props.messageItem.isVoicePlaying) {
emit('audioPause')
} else {
emit('audioPlay')
}
}
</script> </script>
<template> <template>
...@@ -48,14 +82,15 @@ const assistantAvatar = computed(() => { ...@@ -48,14 +82,15 @@ const assistantAvatar = computed(() => {
:width="32" :width="32"
:height="32" :height="32"
object-fit="cover" object-fit="cover"
class="mr-2 mt-1.5 h-8 w-8 rounded-full" class="mr-2 mt-1.5 h-8 w-8 flex-shrink-0 rounded-full"
/> />
<div class="flex w-full flex-col" :class="isMobile && role === 'user' ? 'items-end' : 'items-start'">
<div <div
class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="[ :class="[
role === 'user' ? 'bg-theme-color text-white' : 'bg-white text-[#333]', role === 'user' ? 'bg-theme-color text-white' : 'bg-white text-[#333]',
isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-[calc(100%-32px-12px)]', isMobile ? 'max-w-[calc(100%-20px)]' : 'max-w-full',
]" ]"
> >
<div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4"> <div v-if="messageItem.isTextContentLoading" class="py-1.5 pl-4">
...@@ -74,10 +109,61 @@ const assistantAvatar = computed(() => { ...@@ -74,10 +109,61 @@ const assistantAvatar = computed(() => {
/> />
</p> </p>
<div v-show="role === 'assistant' && messageItem.isAnswerResponseLoading" class="mb-[5px] mt-4 px-4"> <div
v-show="
role === 'assistant' && (messageItem.isAnswerResponseLoading || (isMobile && messageItem.isVoiceLoading))
"
class="mb-[5px] mt-4 px-4"
>
<CustomLoading /> <CustomLoading />
</div> </div>
</div> </div>
<div v-show="isShowAudioControl && isMobile" class="mt-[13px] flex items-center gap-2">
<div
class="h-[18px] w-[18px] cursor-pointer"
:class="messageItem.isVoicePlaying ? 'bg-svg-pause' : 'bg-svg-play'"
@click="handleAudioControl"
/>
<MusicWavesLoading v-show="messageItem.isVoicePlaying && isPlayableAudio" bar-bg-color="#333" />
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
</div>
<div
v-show="isShowAudioControl && !isMobile"
class="text-font-color flex items-center gap-0.5"
:class="isPlayableAudio ? 'hover:text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed'"
@click="handleAudioControl"
>
<i v-if="!messageItem.isVoicePlaying" class="iconfont icon-play text-[24px]" />
<div v-else class="mx-1.5 my-3 h-[12px] w-[12px] bg-[url(@/assets/images/playing.gif)] bg-[length:100%_100%]" />
<span
v-show="isPlayableAudio"
class="text-[12px]"
:class="messageItem.isVoicePlaying ? 'text-theme-color' : ''"
>
{{ messageItem.isVoicePlaying ? t('common_module.stop_playing') : t('common_module.start_playing') }}
</span>
<n-popover style="max-width: 310px">
<template #trigger>
<span v-show="!isPlayableAudio" class="text-[12px]"> {{ t('common_module.unplayable') }} </span>
</template>
{{ t('common_module.unplayable_tip') }}
</n-popover>
</div>
<div v-if="isShowWebVoiceLoading" class="py-3.5 pl-5">
<CustomLoading />
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -6,7 +6,7 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config' ...@@ -6,7 +6,7 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { computed } from 'vue' import { computed } from 'vue'
interface Props { interface Props {
messageList: ConversationMessageItem[] messageList: Map<string, ConversationMessageItem>
agentApplicationConfig: PersonalAppConfigState agentApplicationConfig: PersonalAppConfigState
continuousQuestionStatus: 'default' | 'close' continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[] continuousQuestionList: string[]
...@@ -14,13 +14,18 @@ interface Props { ...@@ -14,13 +14,18 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 && props.messageList.size > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading !Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
) )
}) })
...@@ -33,11 +38,13 @@ defineExpose({ ...@@ -33,11 +38,13 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5"> <main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div> <div>
<MessageItem <MessageItem
v-for="messageItem in messageList" v-for="[key, messageItem] in messageList"
:key="messageItem.timestamp" :key="key"
:role="messageItem.role" :role="messageItem.role"
:message-item="messageItem" :message-item="messageItem"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/> />
</div> </div>
......
...@@ -38,7 +38,7 @@ function handleToLogin() { ...@@ -38,7 +38,7 @@ function handleToLogin() {
<NButton <NButton
v-show="isLogin" v-show="isLogin"
type="primary" type="primary"
class="rounded-md! h-[32px]! text-xs! w-[80px]!" class="rounded-md! h-[32px]! text-xs! min-w-[80px]!"
@click="handleToCreateApplication" @click="handleToCreateApplication"
> >
{{ t('common_module.create_agent_btn_text') }} {{ t('common_module.create_agent_btn_text') }}
......
<script setup lang="ts">
interface Props {
barBgColor?: string
height?: string
}
withDefaults(defineProps<Props>(), {
barBgColor: '#363636',
height: '12px',
})
</script>
<template>
<div class="music">
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
<div class="bar" />
</div>
</template>
<style lang="scss" scoped>
.music {
display: flex;
align-items: center;
justify-content: space-between;
width: 42px;
height: v-bind(height);
.bar {
width: 2px;
/* stylelint-disable-next-line value-keyword-case */
background: v-bind(barBgColor);
border-radius: 50px;
animation: loader 1.5s ease-in-out infinite;
&:nth-child(1) {
/* background: purple; */
animation-delay: 1s;
}
&:nth-child(2) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(3) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(4) {
/* background: orange; */
animation-delay: 0.4s;
}
&:nth-child(5) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(6) {
/* background: gold; */
animation-delay: 0.2s;
}
&:nth-child(7) {
/* background: gold; */
animation-delay: 0.4s;
}
&:nth-child(8) {
/* background: deeppink; */
animation-delay: 0.6s;
}
&:nth-child(9) {
/* background: crimson; */
animation-delay: 0.8s;
}
&:nth-child(10) {
/* background: purple; */
animation-delay: 1s;
}
}
}
@keyframes loader {
0% {
height: 4px;
}
50% {
height: v-bind(height);
}
100% {
height: 4px;
}
}
</style>
...@@ -55,8 +55,8 @@ function handleToLogin() { ...@@ -55,8 +55,8 @@ function handleToLogin() {
<span class="mb-1 line-clamp-1 max-w-[200px] break-all">{{ agentApplicationConfig.baseInfo.agentTitle }}</span> <span class="mb-1 line-clamp-1 max-w-[200px] break-all">{{ agentApplicationConfig.baseInfo.agentTitle }}</span>
<div class="flex items-center text-xs text-[#84868c]"> <div class="flex items-center text-xs text-[#84868c]">
<img v-show="isLogin" :src="agentMemberInfo.avatarUrl" class="mr-2 h-5 w-5 rounded-full" /> <img v-show="isLogin" :src="agentMemberInfo.avatarUrl" class="mr-2 h-5 w-5 rounded-full" />
<n-ellipsis v-show="isLogin" class="max-w-[120px]! mr-4"> <n-ellipsis class="max-w-[120px]! mr-4">
<span class="select-none">{{ agentMemberInfo.nickName }}</span> <span v-show="isLogin" class="select-none">{{ agentMemberInfo.nickName }}</span>
</n-ellipsis> </n-ellipsis>
<span> <span>
{{ t('common_module.publish_time_in') }} {{ t('common_module.publish_time_in') }}
......
This diff is collapsed.
...@@ -5,4 +5,8 @@ declare interface ConversationMessageItem { ...@@ -5,4 +5,8 @@ declare interface ConversationMessageItem {
isEmptyContent: boolean isEmptyContent: boolean
isTextContentLoading: boolean isTextContentLoading: boolean
isAnswerResponseLoading: boolean isAnswerResponseLoading: boolean
isVoiceLoading: boolean
isVoicePlaying: boolean
voiceFragmentUrlList: string[]
isVoiceEnabled?: boolean
} }
...@@ -91,6 +91,21 @@ declare namespace I18n { ...@@ -91,6 +91,21 @@ declare namespace I18n {
bind: string bind: string
sms: string sms: string
verificationCode: string verificationCode: string
role: string
mandarin: string
cantonese: string
english: string
voice: string
sound: string
voice_auto_play: string
start_playing: string
stop_playing: string
unplayable: string
unplayable_tip: string
response_error: string
agent_exception: string
equity: string
file_size_limit: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
...@@ -103,6 +118,8 @@ declare namespace I18n { ...@@ -103,6 +118,8 @@ declare namespace I18n {
cancel_associate_file_tip: string cancel_associate_file_tip: string
upload_file_limit: string upload_file_limit: string
overwrite_file_tip: string overwrite_file_tip: string
stop_playing_and_then_operate: string
do_not_operate_until_the_reply_is_complete: string
} }
data_table_module: { data_table_module: {
...@@ -279,6 +296,10 @@ declare namespace I18n { ...@@ -279,6 +296,10 @@ declare namespace I18n {
memory_fragment_delete_row_tip_content: string memory_fragment_delete_row_tip_content: string
add_knowledge_successfully: string add_knowledge_successfully: string
remove_knowledge_successfully: string remove_knowledge_successfully: string
setting_voice: string
setting_voice_message: string
setting_voice_desc: string
currently_only_one_voice_can_be_set: string
memory_variable_modal: { memory_variable_modal: {
edit_memory_variable: string edit_memory_variable: string
...@@ -476,6 +497,13 @@ declare namespace I18n { ...@@ -476,6 +497,13 @@ declare namespace I18n {
please_enter_the_correct_verification_code: string please_enter_the_correct_verification_code: string
binding_successful: string binding_successful: string
obtaining_the_verification_code: string obtaining_the_verification_code: string
upload_file: string
file_type_restriction_doc: string
the_file_type_cannot_be_uploaded: string
}
equity_Module: {
file_empty_tip: string
} }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment