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 @@
name="viewport"
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>
</head>
......
......@@ -22,12 +22,14 @@
"@unocss/reset": "^0.61.9",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.7",
"bowser": "^2.11.0",
"clipboardy": "^4.0.0",
"cropperjs": "^1.6.2",
"dayjs": "^1.11.13",
"dompurify": "^3.2.0",
"github-markdown-css": "^5.7.0",
"highlight.js": "^11.10.0",
"howler": "^2.2.4",
"lodash-es": "^4.17.21",
"marked": "^15.0.0",
"marked-highlight": "^2.2.1",
......@@ -46,6 +48,7 @@
"@commitlint/config-conventional": "^19.5.0",
"@commitlint/types": "^19.5.0",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@types/howler": "^2.2.12",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.16.5",
"@types/spark-md5": "^3.0.4",
......
This diff is collapsed.
......@@ -170,3 +170,20 @@ export function fetchRemoveSalePublishApplication<T>(agentPublishId: number) {
export function fetchGetApplicationMallInfo<T>(agentId: string) {
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 type { AxiosProgressEvent } from 'axios'
export function fetchAgentApplicationSelectList<T>() {
return request.post<T>('/agentApplicationRest/getDefaultList.json', {
......@@ -28,3 +29,17 @@ export function fetchMessageRecordList<T>(dialogueId: string) {
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> = {
DEV: 'https://poc-sit.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:
bind: 'Bind'
sms: 'Short message'
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:
continue_question_message: 'You can keep asking questions'
......@@ -103,6 +118,8 @@ common_module:
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'
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:
action: 'Controls'
......@@ -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?'
add_knowledge_successfully: 'Data set {0} was added 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:
edit_memory_variable: 'Edit memory variable'
......@@ -462,3 +483,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: '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:
bind: '绑定'
sms: '短信'
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:
continue_question_message: '你可以继续提问'
......@@ -102,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再围绕这个文件回答'
upload_file_limit: '仅支持上传单个文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上传的文件会覆盖原有文件,是否继续上传'
stop_playing_and_then_operate: '音频播放中,请停止播放后再操作'
do_not_operate_until_the_reply_is_complete: '回复完成后再操作'
data_table_module:
action: '操作'
......@@ -279,6 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '数据删除后不可撤销,确定要删除吗?'
add_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:
edit_memory_variable: '编辑记忆变量'
......@@ -460,3 +481,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: '请输入正确验证码'
binding_successful: '绑定成功'
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:
bind: '綁定'
sms: '短信'
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:
continue_question_message: '你可以繼續提問'
......@@ -102,6 +117,8 @@ common_module:
cancel_associate_file_tip: '不再圍繞這個文件回答'
upload_file_limit: '僅支持上傳單個文件,支持PDF、DOC、DOCX、MD、TXT格式,最大10MB'
overwrite_file_tip: '新上傳的文件會覆蓋原有文件,是否繼續上傳'
stop_playing_and_then_operate: '音頻播放中,請停止播放後再操作'
do_not_operate_until_the_reply_is_complete: '回覆完成後再操作'
data_table_module:
action: '操作'
......@@ -279,6 +296,10 @@ personal_space_module:
memory_fragment_delete_row_tip_content: '數據删除後不可撤銷,確定要删除嗎?'
add_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:
edit_memory_variable: '編輯記憶變數'
......@@ -460,3 +481,9 @@ personal_settings_module:
please_enter_the_correct_verification_code: '請輸入正確驗證碼'
binding_successful: '綁定成功'
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 {
communicationTurn: 3,
temperature: 0.5,
},
voiceConfig: {
defaultOpen: 'Y',
timbreId: '',
},
modifiedTime: new Date(),
createdTime: '',
isCollect: '',
......
......@@ -40,6 +40,11 @@ export const useSystemLanguageStore = defineStore('system-language-store', {
languageOptions: defaultLanguageOptions,
}),
getters: {
currentLanguage(state): I18n.LangType {
return state.currentLanguageInfo.key
},
},
actions: {
updateCurrentLanguageInfo(key: I18n.LangType) {
if (this.currentLanguageInfo.key === key) return ''
......
......@@ -36,6 +36,10 @@ export interface PersonalAppConfigState {
communicationTurn: number //参考对话轮次 0-100
temperature: number //多样性 0-1.00
}
voiceConfig: {
defaultOpen: 'Y' | 'N' //是否默认开启 Y-开启 N-关闭
timbreId: string //音色ID
}
popularity?: number
modifiedTime: Date
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">
import { computed, nextTick, ref, toValue } from 'vue'
import { computed, nextTick, ref, shallowRef, toValue, useTemplateRef } from 'vue'
import type { AgentApplicationRecordItem, MessageItemInterface } from '../types'
import { fetchAgentApplicationSelectList } from '@/apis/home-agent'
import { fetchAgentApplicationSelectList, fetchFileUpload } from '@/apis/home-agent'
import { nanoid } from 'nanoid'
import fetchEventStreamSource from '../utils/fetch-event-stream-source'
import { throttle } from 'lodash-es'
......@@ -31,13 +31,24 @@ const currentFetchEventSourceController = defineModel<AbortController | null>('c
const { t } = useI18n()
const inputFileRef = useTemplateRef<HTMLInputElement | null>('inputFileRef')
let fileUploadController = shallowRef<AbortController | null>(null)
const isShowApplicationSelectMenu = ref(false)
const agentApplicationSelectList = ref<AgentApplicationRecordItem[]>([])
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
const currentInputFileInfo = ref({
url: '',
fileName: '',
percentage: 5,
uploading: false,
})
const isQuestionSubmitBtnDisabled = computed(() => {
return questionContent.value.trim().length === 0 || isAgentResponding.value
return questionContent.value.trim().length === 0 || isAgentResponding.value || currentInputFileInfo.value.uploading
})
;(function () {
......@@ -150,6 +161,7 @@ function questionSubmit() {
dialogsId: props.currentSessionId, //会话ID
agentId: currentAgentApplication.value.agentId, //应用ID
input: questionContent.value.trim(), //提问文本
fileUrls: currentInputFileInfo.value.url ? [currentInputFileInfo.value.url] : [],
},
{
onmessage: (message) => {
......@@ -177,7 +189,9 @@ function questionSubmit() {
emit('historyRecordListUpdate')
})
},
onerror: () => {
onerror: (err) => {
err.message && window.$message.error(err.message)
emit('deleteMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!)
},
},
......@@ -190,11 +204,85 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (questionContent.value.trim().length > 0) {
if (questionContent.value.trim().length > 0 && !isAgentResponding.value && !currentInputFileInfo.value.uploading) {
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>
<template>
......@@ -281,12 +369,77 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
</Transition>
</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">
<div class="box-border flex w-full items-center justify-between px-[12px]">
<i class="iconfont icon-session mr-[5px] text-[14px]"></i>
<span class="text-[14px]">{{ t('home_module.starting_a_new_session') }}</span>
</div>
</n-button>
<input
ref="inputFileRef"
type="file"
class="hidden"
accept=".pdf,.txt,.md,.doc,.docx"
@change="handleFileUpload"
/>
</div>
</div>
<div>
......@@ -340,4 +493,16 @@ function handleQuestionSubmitEnter(event: KeyboardEvent) {
opacity: 0;
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>
......@@ -166,7 +166,7 @@ defineExpose({
<template>
<Transition name="history-menu">
<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>
<div class="pr-[10px]">
<!-- <div class="select-none">
......@@ -263,7 +263,7 @@ defineExpose({
</Transition>
<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]']]"
@click="handleShowHistoryMenuSwitch"
>
......
......@@ -57,9 +57,9 @@ const currentFetchEventSourceController = ref<AbortController | null>(null)
// })
const homeContainerWidthWatchDebounce = debounce((newWidth) => {
if (newWidth <= 1060) {
if (newWidth <= 1120) {
isShowHistoryMenu.value = false
} else if (newWidth >= 1300) {
} else if (newWidth >= 1320) {
isShowHistoryMenu.value = true
}
}, 300)
......@@ -201,7 +201,7 @@ function onGetMessageRecordList(recordId: string) {
<div ref="homeContainerRef" class="relative h-full min-h-[650px] w-full">
<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="{ '!pr-[273px]': isShowHistoryMenu }"
:class="{ '!pr-[293px]': isShowHistoryMenu }"
>
<div class="mx-auto flex h-full w-[750px] flex-col px-[5px] py-[40px]">
<AgentAbout
......
......@@ -50,6 +50,9 @@ export default function fetchEventStreamSource(
data.message && options.onmessage && options.onmessage(data.message)
} else {
options.onerror && options.onerror(new Error(data.message))
controller.abort()
options.onclose && options.onclose()
}
} catch (error) {
options.onerror && options.onerror(error as Error)
......
......@@ -424,16 +424,13 @@ function handleEmailCodeGain() {
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="text-end">
<n-button
<button
v-show="!isShowCountdown"
class="!text-[11px]"
type="tertiary"
size="small"
class="cursor-pointer text-[11px] text-[#333] transition active:text-[#6A6A6A]"
@click="handleSMSCodeGain"
>
{{ t('login_module.get_verification_code') }}
</n-button>
</button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
ref="countdownRef"
......@@ -498,15 +495,13 @@ function handleEmailCodeGain() {
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="text-end">
<n-button
<button
v-show="!isShowCountdown"
class="!text-[11px]"
type="tertiary"
size="small"
class="cursor-pointer text-[11px] text-[#333] transition active:text-[#6A6A6A]"
@click="handleEmailCodeGain"
>
{{ t('login_module.get_verification_code') }}
</n-button>
</button>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
......
<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 isEmail from 'validator/es/lib/isEmail'
import { onMounted, ref, shallowReadonly, useTemplateRef } from 'vue'
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 userStore = useUserStore()
const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
......@@ -145,6 +146,7 @@ function handleSMSCodeGain() {
:bordered="false"
size="medium"
closable
content-style="margin-top: 20px;"
@close="() => (isShowMailboxBindingModal = false)"
>
<n-form
......@@ -153,16 +155,13 @@ function handleSMSCodeGain() {
label-width="auto"
:model="emailInfoForm"
:rules="emailInfoFormRules"
size="large"
>
<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')">
<template #suffix>
<span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span>
<span
v-show="!isShowCountdown"
class="text-theme-color cursor-pointer text-[12px]"
@click="handleSMSCodeGain"
>
<span v-show="!isShowCountdown" class="cursor-pointer text-[12px] text-[#333]" @click="handleSMSCodeGain">
{{ t('login_module.get_verification_code') }}
</span>
......@@ -190,10 +189,16 @@ function handleSMSCodeGain() {
<template #footer>
<div class="text-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') }}
</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') }}
</n-button>
</n-space>
......
<script setup lang="ts">
import { fetchEmailCode, fetchSMSCode, fetchUserPasswordUpdate, fetchVerifyCode } from '@/apis/user'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { useUserStore } from '@/store/modules/user'
import { ss } from '@/utils/storage'
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
......@@ -8,9 +9,11 @@ import { onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vu
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const userStore = useUserStore()
const systemLanguageStore = useSystemLanguageStore()
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const userStore = useUserStore()
const isShowPasswordChangeModal = defineModel<boolean>('isShowPasswordChangeModal', { default: false })
......@@ -51,7 +54,7 @@ const passwordFormRules = shallowReadonly({
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
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'))
}
......@@ -186,11 +189,12 @@ function handleSMSCodeGain() {
<template>
<n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:class="systemLanguageStore.currentLanguage === 'en' ? '!w-[700px]' : '!w-[600px]'"
:title="t('personal_settings_module.password_change')"
:bordered="false"
size="medium"
closable
content-style="margin-top: 20px;"
@close="() => (isShowPasswordChangeModal = false)"
>
<n-form
......@@ -199,6 +203,7 @@ function handleSMSCodeGain() {
label-width="auto"
:model="passwordInfoForm"
:rules="passwordFormRules"
size="large"
>
<n-form-item :label="t('personal_settings_module.obtaining_the_verification_code')">
<div>
......@@ -265,10 +270,16 @@ function handleSMSCodeGain() {
<template #footer>
<div class="text-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') }}
</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') }}
</n-button>
</n-space>
......
......@@ -175,13 +175,15 @@ function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit
</h4>
<div v-if="userInfoFormItemEdit.nickName" class="flex">
<div class="flex w-[220px] items-center">
<div class="flex w-[330px] items-center">
<n-input
ref="inputRefs"
v-model:value="userInfoForm.nickName"
:placeholder="t('personal_settings_module.please_enter_the_account_nickname')"
type="text"
size="small"
maxlength="20"
show-count
/>
</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'
import { Close, Delete } from '@icon-park/vue-next'
import { DataTableColumns } from 'naive-ui'
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 {
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'
import { useScroll } from '@/composables/useScroll'
interface Props {
messageList: ConversationMessageItem[]
messageList: Map<string, ConversationMessageItem>
continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[]
}
const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => {
return (
props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading
props.messageList.size > 1 &&
!Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
)
})
......@@ -31,10 +36,12 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div>
<MessageItem
v-for="messageItem in messageList"
:key="messageItem.timestamp"
v-for="[key, messageItem] in messageList"
:key="key"
:role="messageItem.role"
:message-item="messageItem"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/>
</div>
......
......@@ -8,8 +8,8 @@ import { useThrottleFn } from '@vueuse/core'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { Help, People, RightOne } from '@icon-park/vue-next'
import UploadImage from '@/components/upload-image/upload-image.vue'
import AutoConfigModal from './auto-config-modal.vue'
import OptimizeSystemModal from './optimize-system-modal.vue'
import AutoConfigModal from './components/auto-config-modal.vue'
import OptimizeSystemModal from './components/optimize-system-modal.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import {
......@@ -20,10 +20,11 @@ import {
fetchSaveAgentApplication,
} from '@/apis/agent-application'
import { fetchCustomEventSource } from '@/composables/useEventSource'
import AgentModelSetting from './agent-model-setting.vue'
import AgentAssociatedKnowledge from './agent-associated-knowledge.vue'
import AgentMemorySetting from './agent-memory-setting.vue'
import AgentDialogueSetting from './agent-dialogue-setting.vue'
import AgentModelSetting from './components/agent-model-setting.vue'
import AgentAssociatedKnowledge from './components/agent-associated-knowledge.vue'
import AgentMemorySetting from './components/agent-memory-setting.vue'
import AgentDialogueSetting from './components/agent-dialogue-setting.vue'
import AgentRoleSetting from './components/agent-role-setting.vue'
const { t } = useI18n()
......@@ -74,10 +75,10 @@ watch(
() => personalAppConfigStore.$state,
() => {
if (!baseInfo.value.agentId) {
handleUpdatePersonalAppId()
handleCreatePersonalAgent()
}
},
{ deep: true, once: true },
{ deep: true },
)
watch(
......@@ -114,7 +115,7 @@ const handleSavePersonalAppConfig = useThrottleFn(
true,
)
// 保存应用配置
// 更新保存应用配置
async function handleSaveAgentApplication() {
if (!baseInfo.value.agentTitle) {
return
......@@ -123,8 +124,8 @@ async function handleSaveAgentApplication() {
await fetchSaveAgentApplication<PersonalAppConfigState>(personalAppConfigStore.$state)
}
// 更新保存应用ID
async function handleUpdatePersonalAppId() {
// 新建应用
async function handleCreatePersonalAgent() {
if (!baseInfo.value.agentTitle) {
return
}
......@@ -250,10 +251,14 @@ async function handleSettingAgent(autoConfigInputValue: string) {
handleAIGeneratePreamble(),
handleAIGenerateFeaturedQuestions(),
handleAIGenerateAgentSystem(),
]).finally(() => {
])
.finally(() => {
isFullScreenLoading.value = false
emitter?.emit('resetAgent')
})
.catch(() => {
handleStopGenerate()
})
})
.catch(() => {
isFullScreenLoading.value = false
......@@ -558,14 +563,15 @@ function handleStopGenerate() {
</div>
</div>
<div class="h-full flex-1 overflow-auto border-r border-[#e8e9eb] py-4">
<div class="flex h-6 items-center px-5 leading-6">
<div class="flex h-full flex-1 flex-col overflow-auto border-r border-[#e8e9eb] py-4">
<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" />
<span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.ability_expand') }}
</span>
</div>
<div class="flex-1 overflow-auto">
<AgentAssociatedKnowledge v-model:knowledge-config="knowledgeConfig" />
<AgentMemorySetting
......@@ -581,6 +587,9 @@ function handleStopGenerate() {
@generate-preamble="handleAIGeneratePreamble"
@generate-featured-questions="handleAIGenerateFeaturedQuestions"
/>
<AgentRoleSetting />
</div>
</div>
</div>
</div>
......
......@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
import { Plus, RightOne } from '@icon-park/vue-next'
import { fetchGetKnowledgeListByKdIds } from '@/apis/knowledge'
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'
const { t } = useI18n()
......@@ -92,7 +92,7 @@ function handleUpdateDocumentParsing(value: boolean) {
<template>
<section class="border-b border-[#e8e9eb] px-5">
<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') }}
</h2>
......
......@@ -61,7 +61,7 @@ function handleAIGenerateFeaturedQuestions() {
<template>
<section class="border-b border-[#e8e9eb] px-5">
<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') }}
</h2>
<NCollapse
......
......@@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
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 { copyToClip } from '@/utils/copy'
......@@ -69,7 +69,7 @@ function handleChangeMemoryFragmentState(value: boolean) {
<template>
<section class="border-b border-[#e8e9eb] px-5">
<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') }}
</h2>
<NCollapse
......
......@@ -4,11 +4,14 @@ import { SelectOption } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { Help, Down } from '@icon-park/vue-next'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { DiversityModeItem, diversityModeList } from '@/data/agent-setting-data'
import { fetchGetLargeModelInfo, fetchGetLargeModelList } from '@/apis/agent-application'
const { t } = useI18n()
const systemLanguageStore = useSystemLanguageStore()
let modalListOptions = reactive<SelectOption[]>([])
let modalListRenderLabel: (option: SelectOption) => VNodeChild
......@@ -17,6 +20,13 @@ const commModelConfig = defineModel<PersonalAppConfigState['commModelConfig']>('
const currentLargeModelIcon = ref('')
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(() => {
return currentDiversityMode.value !== 'custom'
})
......@@ -107,7 +117,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<template>
<div>
<NPopover placement="bottom" trigger="click" style="width: 420px">
<NPopover placement="bottom" trigger="click" :style="{ width: modelSettingWidth }">
<template #trigger>
<div
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) {
<Down theme="outline" size="16" fill="#333" class="ml-1.5 text-base outline-none" />
</div>
</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]">
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.question_answer_model') }}
</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')
}}
......@@ -141,7 +154,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
<span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.generate_diversity') }}
</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
v-for="(diversityModeItem, index) in diversityModeList"
:key="index"
......@@ -159,8 +172,8 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
</div>
<div class="mt-4 text-xs">
<div class="mb-2.5 flex h-[34px] items-center justify-between">
<div class="flex w-[105px] items-center">
<div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.topP') }}
</span>
......@@ -202,12 +215,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:max="1"
:disabled="isDisabledCommModelConfig"
size="small"
class="w-[90px]! text-xs!"
class="common-model-config-input-number w-[90px]!"
/>
</div>
<div class="mb-2.5 flex h-[34px] items-center justify-between">
<div class="flex w-[105px] items-center">
<div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.temperature') }}
</span>
......@@ -251,12 +264,12 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:max="1"
:disabled="isDisabledCommModelConfig"
size="small"
class="w-[90px]! text-xs!"
class="common-model-config-input-number w-[90px]!"
/>
</div>
<div class="mb-2.5 flex h-[34px] items-center justify-between">
<div class="flex w-[105px] items-center">
<div class="flex h-[34px] items-center justify-between" :class="isEnLanguage ? 'mb-3.5' : 'mb-2.5'">
<div class="flex items-center" :style="{ width: sliderLabelWidth }">
<span>
{{ t('personal_space_module.agent_module.agent_setting_module.agent_config_module.communication_turn') }}
</span>
......@@ -296,7 +309,7 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
:min="0"
:max="100"
size="small"
class="w-[90px]!"
class="common-model-config-input-number w-[90px]!"
placeholder=""
/>
</div>
......@@ -312,4 +325,10 @@ function handleDiversityModeChange(diversityModeItem: DiversityModeItem) {
border-radius: 6px;
}
}
.common-model-config-input-number {
:deep(.n-input__input) {
font-size: 12px;
}
}
</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'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { fetchCreateKnowledge, fetchGetKnowledgeList } from '@/apis/knowledge'
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 CreateKnowledgeModal, {
KnowledgeFormDataInterface,
} from '../../personal-knowledge/components/create-knowledge-modal.vue'
} from '@/views/personal-space/personal-knowledge/components/create-knowledge-modal.vue'
interface Props {
isShowModal: boolean
......@@ -95,7 +96,7 @@ async function handleGetKnowledgeList() {
paginationData.pageSize = 999999
knowledgeListLoading.value = true
const res = await fetchGetKnowledgeList<KnowledgeItem[]>('', searchKnowledgeInputValue.value, {
const res = await fetchGetKnowledgeList<KnowledgeItem[]>(TrainStatus.COMPLETE, searchKnowledgeInputValue.value, {
pagingInfo: paginationData,
})
......@@ -171,6 +172,11 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD
<template #content>
<div class="flex w-full justify-end px-3">
<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
v-model:value="searchKnowledgeInputValue"
: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 @@
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Computer, PreviewOpen, AllApplication, SettingOne } from '@icon-park/vue-next'
import useTableScrollY from '@/composables/useTableScrollY'
import { copyToClip } from '@/utils/copy'
import { formatDateTime } from '@/utils/date-formatter'
import { ref, watch } from 'vue'
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 {
fetchGetApplicationInfo,
fetchGetApplicationMallInfo,
......@@ -82,8 +80,7 @@ function handleClickChannelPublishTableAction(actionType: string) {
}
function handleAccessPage() {
const channelUrl = `${window.location.origin}/fe/share/web_source/${router.currentRoute.value.params.agentId}`
location.href = channelUrl
router.push({ name: 'ShareWebApplication', params: { agentId: router.currentRoute.value.params.agentId } })
}
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'
import { Emitter } from 'mitt'
import { useI18n } from 'vue-i18n'
import PageNarBar from './components/page-narbar.vue'
import AppSetting from './components/app-setting.vue'
import AppPreview from './components/app-preview.vue'
import AppPublish from './components/app-publish.vue'
import AgentConfig from './components/agent-config/agent-config.vue'
import AgentPublish from './components/agent-publish.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { fetchGetDebugApplicationInfo } from '@/apis/agent-application'
......@@ -79,13 +78,11 @@ function handleChangeAgentAppTabKey(currentTabKey: string) {
<div class="h-content flex w-full flex-1">
<div v-if="currentAgentAppTabKey === 'config'" class="flex h-full w-full flex-1">
<AppSetting />
<AppPreview />
<AgentConfig />
</div>
<div v-if="currentAgentAppTabKey === 'publish'" class="flex h-full w-full flex-1">
<AppPublish />
<AgentPublish />
</div>
</div>
......
This diff is collapsed.
......@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import CustomLoading from './custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { useLayoutConfig } from '@/composables/useLayoutConfig'
......@@ -17,6 +18,11 @@ const { t } = useI18n()
const props = defineProps<Props>()
const emit = defineEmits<{
audioPlay: []
audioPause: []
}>()
const userStore = useUserStore()
const { isMobile } = useLayoutConfig()
......@@ -31,6 +37,34 @@ const assistantAvatar = computed(() => {
'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>
<template>
......@@ -48,14 +82,15 @@ const assistantAvatar = computed(() => {
:width="32"
:height="32"
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
class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="[
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">
......@@ -74,10 +109,61 @@ const assistantAvatar = computed(() => {
/>
</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 />
</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>
</template>
......@@ -6,7 +6,7 @@ import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { computed } from 'vue'
interface Props {
messageList: ConversationMessageItem[]
messageList: Map<string, ConversationMessageItem>
agentApplicationConfig: PersonalAppConfigState
continuousQuestionStatus: 'default' | 'close'
continuousQuestionList: string[]
......@@ -14,13 +14,18 @@ interface Props {
const props = defineProps<Props>()
defineEmits<{
audioPlay: [messageItem: ConversationMessageItem]
audioPause: []
}>()
const { scrollRef, scrollToBottom } = useScroll()
const isShowContinueQuestion = computed(() => {
return (
props.continuousQuestionStatus === 'default' &&
props.messageList.length > 1 &&
!props.messageList[props.messageList.length - 1].isAnswerResponseLoading
props.messageList.size > 1 &&
!Array.from(props.messageList.entries()).pop()?.[1].isAnswerResponseLoading
)
})
......@@ -33,11 +38,13 @@ defineExpose({
<main ref="scrollRef" class="h-full overflow-y-auto px-5">
<div>
<MessageItem
v-for="messageItem in messageList"
:key="messageItem.timestamp"
v-for="[key, messageItem] in messageList"
:key="key"
:role="messageItem.role"
:message-item="messageItem"
:agent-application-config="agentApplicationConfig"
@audio-play="() => $emit('audioPlay', messageItem)"
@audio-pause="() => $emit('audioPause')"
/>
</div>
......
......@@ -38,7 +38,7 @@ function handleToLogin() {
<NButton
v-show="isLogin"
type="primary"
class="rounded-md! h-[32px]! text-xs! w-[80px]!"
class="rounded-md! h-[32px]! text-xs! min-w-[80px]!"
@click="handleToCreateApplication"
>
{{ 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() {
<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]">
<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">
<span class="select-none">{{ agentMemberInfo.nickName }}</span>
<n-ellipsis class="max-w-[120px]! mr-4">
<span v-show="isLogin" class="select-none">{{ agentMemberInfo.nickName }}</span>
</n-ellipsis>
<span>
{{ t('common_module.publish_time_in') }}
......
This diff is collapsed.
......@@ -5,4 +5,8 @@ declare interface ConversationMessageItem {
isEmptyContent: boolean
isTextContentLoading: boolean
isAnswerResponseLoading: boolean
isVoiceLoading: boolean
isVoicePlaying: boolean
voiceFragmentUrlList: string[]
isVoiceEnabled?: boolean
}
......@@ -91,6 +91,21 @@ declare namespace I18n {
bind: string
sms: 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: {
continue_question_message: string
......@@ -103,6 +118,8 @@ declare namespace I18n {
cancel_associate_file_tip: string
upload_file_limit: string
overwrite_file_tip: string
stop_playing_and_then_operate: string
do_not_operate_until_the_reply_is_complete: string
}
data_table_module: {
......@@ -279,6 +296,10 @@ declare namespace I18n {
memory_fragment_delete_row_tip_content: string
add_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: {
edit_memory_variable: string
......@@ -476,6 +497,13 @@ declare namespace I18n {
please_enter_the_correct_verification_code: string
binding_successful: 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