| <template> |
| <v-dialog v-model="showDialog" :max-width="$vuetify.display.smAndDown ? undefined : '1200px'" scrollable> |
| <v-card class="persona-form-card" :class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }"> |
| <v-card-title class="persona-form-title text-h2 px-6 pt-6 pl-6"> |
| {{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }} |
| </v-card-title> |
| |
| <v-card-text class="persona-form-content"> |
| |
| <v-alert v-if="!editingPersona" type="info" variant="tonal" density="compact" class="mb-4" |
| icon="mdi-folder-outline"> |
| {{ tm('form.createInFolder', { folder: folderDisplayName }) }} |
| </v-alert> |
| |
| <v-form ref="personaForm" v-model="formValid"> |
| <v-row class="persona-form-layout"> |
| <v-col cols="12" md="6" class="persona-basic-col"> |
| <v-text-field v-model="personaForm.persona_id" :label="tm('form.personaId')" |
| :rules="personaIdRules" :disabled="editingPersona" variant="outlined" |
| density="comfortable" class="mb-4" /> |
| |
| <v-textarea v-model="personaForm.system_prompt" :label="tm('form.systemPrompt')" |
| :rules="systemPromptRules" variant="outlined" rows="16" class="mb-4" /> |
| |
| <v-textarea |
| v-model="personaForm.custom_error_message" |
| :label="tm('form.customErrorMessage')" |
| :hint="tm('form.customErrorMessageHelp')" |
| variant="outlined" |
| rows="4" |
| persistent-hint |
| clearable |
| class="mb-4" |
| /> |
| </v-col> |
| |
| <v-col cols="12" md="6" class="persona-panels-col"> |
| <v-expansion-panels v-model="expandedPanels" multiple> |
| |
| <v-expansion-panel value="tools"> |
| <v-expansion-panel-title> |
| <v-icon class="mr-2">mdi-tools</v-icon> |
| {{ tm('form.tools') }} |
| <v-chip v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0" |
| size="small" color="primary" variant="tonal" class="ml-2"> |
| {{ personaForm.tools.length }} |
| </v-chip> |
| </v-expansion-panel-title> |
| |
| <v-expansion-panel-text> |
| <div class="mb-3"> |
| <p class="text-body-2 text-medium-emphasis"> |
| {{ tm('form.toolsHelp') }} |
| </p> |
| </div> |
| |
| <v-radio-group class="mt-2" v-model="toolSelectValue" hide-details="true"> |
| <v-radio label="默认使用全部函数工具" value="0"></v-radio> |
| <v-radio label="选择指定函数工具" value="1"> |
| </v-radio> |
| </v-radio-group> |
| |
| <div v-if="toolSelectValue === '1'" class="mt-3 selected-config-area"> |
| |
| |
| <v-text-field v-model="toolSearch" :label="tm('form.searchTools')" |
| prepend-inner-icon="mdi-magnify" variant="outlined" density="compact" |
| hide-details clearable class="mb-3" /> |
| |
| |
| |
| <div v-if="mcpServers.length > 0" class="mb-4"> |
| <h4 class="text-subtitle-2 mb-2">{{ tm('form.mcpServersQuickSelect') }}</h4> |
| <div class="d-flex flex-wrap ga-2"> |
| <v-chip v-for="server in mcpServers" :key="server.name" |
| :color="isServerSelected(server) ? 'primary' : 'default'" |
| :variant="isServerSelected(server) ? 'flat' : 'outlined'" size="small" |
| clickable @click="toggleMcpServer(server)" |
| :disabled="!server.tools || server.tools.length === 0"> |
| <v-icon start size="small">mdi-server</v-icon> |
| {{ server.name }} |
| <v-chip-text v-if="server.tools" class="ml-1"> |
| ({{ server.tools.length }}) |
| </v-chip-text> |
| </v-chip> |
| </div> |
| </div> |
| |
| |
| <div v-if="filteredTools.length > 0" class="tools-selection"> |
| <v-virtual-scroll :items="filteredTools" height="300" item-height="72"> |
| <template v-slot:default="{ item }"> |
| <v-list-item :key="item.name" density="comfortable" |
| @click="toggleTool(item.name)"> |
| <template v-slot:prepend> |
| <v-checkbox-btn :model-value="isToolSelected(item.name)" |
| @click.stop="toggleTool(item.name)" /> |
| </template> |
| |
| <v-list-item-title> |
| {{ item.name }} |
| |
| <v-chip v-if="item.origin" size="x-small" color="info" class="mr-2" |
| variant="tonal"> |
| {{ item.origin }} |
| </v-chip> |
| <v-chip v-if="item.origin_name" size="x-small" color="info" |
| variant="outlined"> |
| {{ item.origin_name }} |
| </v-chip> |
| |
| </v-list-item-title> |
| |
| <v-list-item-subtitle v-if="item.description"> |
| {{ truncateText(item.description, 100) }} |
| </v-list-item-subtitle> |
| </v-list-item> |
| </template> |
| </v-virtual-scroll> |
| </div> |
| |
| <div v-else-if="!loadingTools && availableTools.length === 0" |
| class="text-center pa-4"> |
| <v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-tools</v-icon> |
| <p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsAvailable') |
| }} |
| </p> |
| </div> |
| |
| <div v-else-if="!loadingTools && filteredTools.length === 0" |
| class="text-center pa-4"> |
| <v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon> |
| <p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsFound') }} |
| </p> |
| </div> |
| |
| |
| <div v-if="loadingTools" class="text-center pa-4"> |
| <v-progress-circular indeterminate color="primary" /> |
| <p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingTools') |
| }} |
| </p> |
| </div> |
| |
| |
| <div class="mt-4"> |
| <h4 class="text-subtitle-2 mb-2"> |
| {{ tm('form.selectedTools') }} |
| <span v-if="personaForm.tools === null" class="text-success"> |
| ({{ tm('form.allSelected') }}) |
| </span> |
| <span v-else-if="Array.isArray(personaForm.tools)"> |
| ({{ personaForm.tools.length }}) |
| </span> |
| </h4> |
| <div v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0" |
| class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;"> |
| <v-chip v-for="toolName in personaForm.tools" :key="toolName" size="small" |
| color="primary" variant="tonal" closable |
| @click:close="removeTool(toolName)"> |
| {{ toolName }} |
| </v-chip> |
| </div> |
| <div v-else class="text-body-2 text-medium-emphasis"> |
| {{ tm('form.noToolsSelected') }} |
| </div> |
| </div> |
| </div> |
| |
| </v-expansion-panel-text> |
| </v-expansion-panel> |
| |
| |
| <v-expansion-panel value="skills"> |
| <v-expansion-panel-title> |
| <v-icon class="mr-2">mdi-lightning-bolt</v-icon> |
| {{ tm('form.skills') }} |
| <v-chip v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0" |
| size="small" color="primary" variant="tonal" class="ml-2"> |
| {{ personaForm.skills.length }} |
| </v-chip> |
| </v-expansion-panel-title> |
| |
| <v-expansion-panel-text> |
| <div class="mb-3"> |
| <p class="text-body-2 text-medium-emphasis"> |
| {{ tm('form.skillsHelp') }} |
| </p> |
| </div> |
| |
| <v-radio-group class="mt-2" v-model="skillSelectValue" hide-details="true"> |
| <v-radio :label="tm('form.skillsAllAvailable')" value="0"></v-radio> |
| <v-radio :label="tm('form.skillsSelectSpecific')" value="1"></v-radio> |
| </v-radio-group> |
| |
| <div v-if="skillSelectValue === '1'" class="mt-3 selected-config-area"> |
| <v-text-field v-model="skillSearch" :label="tm('form.searchSkills')" |
| prepend-inner-icon="mdi-magnify" variant="outlined" density="compact" |
| hide-details clearable class="mb-3" /> |
| |
| <div v-if="filteredSkills.length > 0" class="skills-selection"> |
| <v-virtual-scroll :items="filteredSkills" height="240" item-height="48"> |
| <template v-slot:default="{ item }"> |
| <v-list-item :key="item.name" density="comfortable" |
| @click="toggleSkill(item.name)"> |
| <template v-slot:prepend> |
| <v-checkbox-btn :model-value="isSkillSelected(item.name)" |
| @click.stop="toggleSkill(item.name)" /> |
| </template> |
| <v-list-item-title> |
| {{ item.name }} |
| </v-list-item-title> |
| <v-list-item-subtitle v-if="item.description"> |
| {{ truncateText(item.description, 100) }} |
| </v-list-item-subtitle> |
| </v-list-item> |
| </template> |
| </v-virtual-scroll> |
| </div> |
| |
| <div v-else-if="!loadingSkills && availableSkills.length === 0" |
| class="text-center pa-4"> |
| <v-icon size="48" color="grey-lighten-2" |
| class="mb-2">mdi-lightning-bolt</v-icon> |
| <p class="text-body-2 text-medium-emphasis">{{ tm('form.noSkillsAvailable') }} |
| </p> |
| </div> |
| |
| <div v-else-if="!loadingSkills && filteredSkills.length === 0" |
| class="text-center pa-4"> |
| <v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon> |
| <p class="text-body-2 text-medium-emphasis">{{ tm('form.noSkillsFound') }} |
| </p> |
| </div> |
| |
| <div v-if="loadingSkills" class="text-center pa-4"> |
| <v-progress-circular indeterminate color="primary" /> |
| <p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingSkills') }} |
| </p> |
| </div> |
| |
| <div class="mt-4"> |
| <h4 class="text-subtitle-2 mb-2"> |
| {{ tm('form.selectedSkills') }} |
| <span v-if="personaForm.skills === null" class="text-success"> |
| ({{ tm('form.allSelected') }}) |
| </span> |
| <span v-else-if="Array.isArray(personaForm.skills)"> |
| ({{ personaForm.skills.length }}) |
| </span> |
| </h4> |
| <div v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0" |
| class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;"> |
| <v-chip v-for="skillName in personaForm.skills" :key="skillName" |
| size="small" color="primary" variant="tonal" closable |
| @click:close="removeSkill(skillName)"> |
| {{ skillName }} |
| </v-chip> |
| </div> |
| <div v-else class="text-body-2 text-medium-emphasis"> |
| {{ tm('form.noSkillsSelected') }} |
| </div> |
| </div> |
| </div> |
| </v-expansion-panel-text> |
| </v-expansion-panel> |
| |
| |
| <v-expansion-panel value="dialogs"> |
| <v-expansion-panel-title> |
| <v-icon class="mr-2">mdi-chat</v-icon> |
| {{ tm('form.presetDialogs') }} |
| <v-chip v-if="personaForm.begin_dialogs.length > 0" size="small" color="primary" |
| variant="tonal" class="ml-2"> |
| {{ personaForm.begin_dialogs.length / 2 }} |
| </v-chip> |
| </v-expansion-panel-title> |
| |
| <v-expansion-panel-text> |
| <div class="mb-3"> |
| <p class="text-body-2 text-medium-emphasis"> |
| {{ tm('form.presetDialogsHelp') }} |
| </p> |
| </div> |
| |
| <div v-for="(dialog, index) in personaForm.begin_dialogs" :key="index" class="mb-3"> |
| <v-textarea v-model="personaForm.begin_dialogs[index]" |
| :label="index % 2 === 0 ? tm('form.userMessage') : tm('form.assistantMessage')" |
| :rules="getDialogRules(index)" variant="outlined" rows="2" |
| density="comfortable"> |
| <template v-slot:append> |
| <v-btn icon="mdi-delete" variant="text" size="small" color="error" |
| @click="removeDialog(index)" /> |
| </template> |
| </v-textarea> |
| </div> |
| |
| <v-btn variant="outlined" prepend-icon="mdi-plus" @click="addDialogPair" block> |
| {{ tm('buttons.addDialogPair') }} |
| </v-btn> |
| </v-expansion-panel-text> |
| </v-expansion-panel> |
| </v-expansion-panels> |
| </v-col> |
| </v-row> |
| </v-form> |
| </v-card-text> |
| |
| <v-card-actions class="persona-form-actions"> |
| <v-btn v-if="editingPersona" color="error" variant="text" @click="deletePersona"> |
| {{ tm('buttons.delete') }} |
| </v-btn> |
| <v-spacer /> |
| <v-btn color="grey" variant="text" @click="closeDialog"> |
| {{ tm('buttons.cancel') }} |
| </v-btn> |
| <v-btn color="primary" variant="flat" @click="savePersona" :loading="saving" :disabled="!formValid"> |
| {{ tm('buttons.save') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| </template> |
| |
| <script> |
| import axios from 'axios'; |
| import { useModuleI18n } from '@/i18n/composables'; |
| import { |
| askForConfirmation as askForConfirmationDialog, |
| useConfirmDialog |
| } from '@/utils/confirmDialog'; |
| |
| export default { |
| name: 'PersonaForm', |
| props: { |
| modelValue: { |
| type: Boolean, |
| default: false |
| }, |
| editingPersona: { |
| type: Object, |
| default: null |
| }, |
| currentFolderId: { |
| type: String, |
| default: null |
| }, |
| currentFolderName: { |
| type: String, |
| default: null |
| } |
| }, |
| emits: ['update:modelValue', 'saved', 'error', 'deleted'], |
| setup() { |
| const { tm } = useModuleI18n('features/persona'); |
| const confirmDialog = useConfirmDialog(); |
| return { tm, confirmDialog }; |
| }, |
| data() { |
| return { |
| toolSelectValue: '0', |
| saving: false, |
| expandedPanels: [], |
| formValid: false, |
| mcpServers: [], |
| availableTools: [], |
| loadingTools: false, |
| availableSkills: [], |
| loadingSkills: false, |
| existingPersonaIds: [], |
| personaForm: { |
| persona_id: '', |
| system_prompt: '', |
| custom_error_message: '', |
| begin_dialogs: [], |
| tools: [], |
| skills: [], |
| folder_id: null |
| }, |
| personaIdRules: [ |
| v => !!v || this.tm('validation.required'), |
| v => (v && v.length >= 1) || this.tm('validation.minLength', { min: 1 }), |
| v => !this.existingPersonaIds.includes(v) || this.tm('validation.personaIdExists'), |
| ], |
| systemPromptRules: [ |
| v => !!v || this.tm('validation.required'), |
| v => (v && v.length >= 10) || this.tm('validation.minLength', { min: 10 }) |
| ], |
| toolSearch: '', |
| skillSearch: '', |
| skillSelectValue: '0' |
| } |
| }, |
| |
| computed: { |
| showDialog: { |
| get() { |
| return this.modelValue; |
| }, |
| set(value) { |
| this.$emit('update:modelValue', value); |
| } |
| }, |
| filteredTools() { |
| if (!this.toolSearch) { |
| return this.availableTools; |
| } |
| const search = this.toolSearch.toLowerCase(); |
| return this.availableTools.filter(tool => |
| tool.name.toLowerCase().includes(search) || |
| (tool.description && tool.description.toLowerCase().includes(search)) || |
| (tool.mcp_server_name && tool.mcp_server_name.toLowerCase().includes(search)) |
| ); |
| }, |
| filteredSkills() { |
| if (!this.skillSearch) { |
| return this.availableSkills; |
| } |
| const search = this.skillSearch.toLowerCase(); |
| return this.availableSkills.filter(skill => |
| skill.name.toLowerCase().includes(search) || |
| (skill.description && skill.description.toLowerCase().includes(search)) |
| ); |
| }, |
| folderDisplayName() { |
| |
| if (this.currentFolderName) { |
| return this.currentFolderName; |
| } |
| |
| if (!this.currentFolderId) { |
| return this.tm('form.rootFolder'); |
| } |
| |
| return this.currentFolderId; |
| } |
| }, |
| |
| watch: { |
| modelValue(newValue) { |
| if (newValue) { |
| |
| if (this.editingPersona) { |
| this.initFormWithPersona(this.editingPersona); |
| } else { |
| this.initForm(); |
| |
| this.loadExistingPersonaIds(); |
| } |
| this.loadMcpServers(); |
| this.loadTools(); |
| this.loadSkills(); |
| } |
| }, |
| editingPersona: { |
| immediate: true, |
| handler(newPersona) { |
| |
| if (this.modelValue) { |
| if (newPersona) { |
| this.initFormWithPersona(newPersona); |
| } else { |
| this.initForm(); |
| } |
| } |
| } |
| }, |
| toolSelectValue(newValue) { |
| if (newValue === '0') { |
| |
| this.personaForm.tools = null; |
| } else if (newValue === '1') { |
| |
| if (this.personaForm.tools === null) { |
| this.personaForm.tools = []; |
| } |
| } |
| }, |
| skillSelectValue(newValue) { |
| if (newValue === '0') { |
| this.personaForm.skills = null; |
| } else if (newValue === '1') { |
| if (this.personaForm.skills === null) { |
| this.personaForm.skills = []; |
| } |
| } |
| } |
| }, |
| |
| methods: { |
| initForm() { |
| this.personaForm = { |
| persona_id: '', |
| system_prompt: '', |
| custom_error_message: '', |
| begin_dialogs: [], |
| tools: [], |
| skills: [], |
| folder_id: this.currentFolderId |
| }; |
| this.toolSelectValue = '0'; |
| this.skillSelectValue = '0'; |
| this.expandedPanels = this.getDefaultExpandedPanels(); |
| }, |
| |
| initFormWithPersona(persona) { |
| this.personaForm = { |
| persona_id: persona.persona_id, |
| system_prompt: persona.system_prompt, |
| custom_error_message: persona.custom_error_message || '', |
| begin_dialogs: [...(persona.begin_dialogs || [])], |
| tools: persona.tools === null ? null : [...(persona.tools || [])], |
| skills: persona.skills === null ? null : [...(persona.skills || [])], |
| folder_id: persona.folder_id |
| }; |
| |
| this.toolSelectValue = persona.tools === null ? '0' : '1'; |
| this.skillSelectValue = persona.skills === null ? '0' : '1'; |
| this.expandedPanels = this.getDefaultExpandedPanels(); |
| }, |
| |
| getDefaultExpandedPanels() { |
| return this.$vuetify.display.smAndDown ? [] : ['tools', 'skills', 'dialogs']; |
| }, |
| |
| closeDialog() { |
| this.showDialog = false; |
| }, |
| |
| async loadMcpServers() { |
| try { |
| const response = await axios.get('/api/tools/mcp/servers'); |
| if (response.data.status === 'ok') { |
| this.mcpServers = response.data.data || []; |
| } else { |
| this.$emit('error', response.data.message || 'Failed to load MCP servers'); |
| } |
| } catch (error) { |
| this.$emit('error', error.response?.data?.message || 'Failed to load MCP servers'); |
| this.mcpServers = []; |
| } |
| }, |
| |
| async loadTools() { |
| this.loadingTools = true; |
| try { |
| const response = await axios.get('/api/tools/list'); |
| if (response.data.status === 'ok') { |
| this.availableTools = response.data.data || []; |
| } else { |
| this.$emit('error', response.data.message || 'Failed to load tools'); |
| } |
| } catch (error) { |
| this.$emit('error', error.response?.data?.message || 'Failed to load tools'); |
| this.availableTools = []; |
| } finally { |
| this.loadingTools = false; |
| } |
| }, |
| |
| async loadSkills() { |
| this.loadingSkills = true; |
| try { |
| const response = await axios.get('/api/skills'); |
| if (response.data.status === 'ok') { |
| const payload = response.data.data || []; |
| if (Array.isArray(payload)) { |
| this.availableSkills = payload.filter(skill => skill.active !== false); |
| } else { |
| const skills = payload.skills || []; |
| this.availableSkills = skills.filter(skill => skill.active !== false); |
| } |
| } else { |
| this.$emit('error', response.data.message || 'Failed to load skills'); |
| } |
| } catch (error) { |
| this.$emit('error', error.response?.data?.message || 'Failed to load skills'); |
| this.availableSkills = []; |
| } finally { |
| this.loadingSkills = false; |
| } |
| }, |
| |
| async loadExistingPersonaIds() { |
| try { |
| const response = await axios.get('/api/persona/list'); |
| if (response.data.status === 'ok') { |
| this.existingPersonaIds = (response.data.data || []).map(p => p.persona_id); |
| } |
| } catch (error) { |
| |
| this.existingPersonaIds = []; |
| } |
| }, |
| |
| async savePersona() { |
| if (!this.formValid) return; |
| |
| |
| if (this.personaForm.begin_dialogs.length > 0) { |
| for (let i = 0; i < this.personaForm.begin_dialogs.length; i++) { |
| if (!this.personaForm.begin_dialogs[i] || this.personaForm.begin_dialogs[i].trim() === '') { |
| const dialogType = i % 2 === 0 ? this.tm('form.userMessage') : this.tm('form.assistantMessage'); |
| this.$emit('error', this.tm('validation.dialogRequired', { type: dialogType })); |
| return; |
| } |
| } |
| } |
| |
| this.saving = true; |
| try { |
| const url = this.editingPersona ? '/api/persona/update' : '/api/persona/create'; |
| const response = await axios.post(url, this.personaForm); |
| |
| if (response.data.status === 'ok') { |
| this.$emit('saved', response.data.message || this.tm('messages.saveSuccess')); |
| this.closeDialog(); |
| } else { |
| this.$emit('error', response.data.message || this.tm('messages.saveError')); |
| } |
| } catch (error) { |
| this.$emit('error', error.response?.data?.message || this.tm('messages.saveError')); |
| } |
| this.saving = false; |
| }, |
| |
| async deletePersona() { |
| if (!this.editingPersona) return; |
| |
| if ( |
| !(await askForConfirmationDialog( |
| this.tm('messages.deleteConfirm', { id: this.editingPersona.persona_id }), |
| this.confirmDialog, |
| )) |
| ) { |
| return; |
| } |
| |
| this.saving = true; |
| try { |
| const response = await axios.post('/api/persona/delete', { |
| persona_id: this.editingPersona.persona_id |
| }); |
| |
| if (response.data.status === 'ok') { |
| this.$emit('deleted', response.data.message || this.tm('messages.deleteSuccess')); |
| this.closeDialog(); |
| } else { |
| this.$emit('error', response.data.message || this.tm('messages.deleteError')); |
| } |
| } catch (error) { |
| this.$emit('error', error.response?.data?.message || this.tm('messages.deleteError')); |
| } finally { |
| this.saving = false; |
| } |
| }, |
| |
| addDialogPair() { |
| this.personaForm.begin_dialogs.push('', ''); |
| |
| if (!this.expandedPanels.includes('dialogs')) { |
| this.expandedPanels.push('dialogs'); |
| } |
| }, |
| |
| removeDialog(index) { |
| |
| if (index % 2 === 0 && index + 1 < this.personaForm.begin_dialogs.length) { |
| this.personaForm.begin_dialogs.splice(index, 2); |
| } |
| |
| else if (index % 2 === 1 && index - 1 >= 0) { |
| this.personaForm.begin_dialogs.splice(index - 1, 2); |
| } |
| }, |
| |
| toggleMcpServer(server) { |
| if (!server.tools || server.tools.length === 0) return; |
| |
| |
| if (this.personaForm.tools === null) { |
| |
| this.personaForm.tools = this.availableTools.map(tool => tool.name) |
| .filter(toolName => !server.tools.includes(toolName)); |
| this.toolSelectValue = '1'; |
| return; |
| } |
| |
| |
| if (!Array.isArray(this.personaForm.tools)) { |
| this.personaForm.tools = []; |
| this.toolSelectValue = '1'; |
| } |
| |
| |
| const serverTools = server.tools; |
| const allSelected = serverTools.every(toolName => this.personaForm.tools.includes(toolName)); |
| |
| if (allSelected) { |
| |
| this.personaForm.tools = this.personaForm.tools.filter( |
| toolName => !serverTools.includes(toolName) |
| ); |
| } else { |
| |
| serverTools.forEach(toolName => { |
| if (!this.personaForm.tools.includes(toolName)) { |
| this.personaForm.tools.push(toolName); |
| } |
| }); |
| } |
| }, |
| |
| toggleTool(toolName) { |
| |
| if (this.personaForm.tools === null) { |
| |
| |
| this.personaForm.tools = this.availableTools.map(tool => tool.name).filter(name => name !== toolName); |
| this.toolSelectValue = '1'; |
| } else if (Array.isArray(this.personaForm.tools)) { |
| const index = this.personaForm.tools.indexOf(toolName); |
| if (index !== -1) { |
| |
| this.personaForm.tools.splice(index, 1); |
| } else { |
| |
| this.personaForm.tools.push(toolName); |
| } |
| } else { |
| |
| this.personaForm.tools = [toolName]; |
| this.toolSelectValue = '1'; |
| } |
| }, |
| |
| removeTool(toolName) { |
| |
| if (this.personaForm.tools === null) { |
| |
| this.personaForm.tools = this.availableTools.map(tool => tool.name).filter(name => name !== toolName); |
| this.toolSelectValue = '1'; |
| } else if (Array.isArray(this.personaForm.tools)) { |
| const index = this.personaForm.tools.indexOf(toolName); |
| if (index !== -1) { |
| this.personaForm.tools.splice(index, 1); |
| } |
| } |
| }, |
| |
| toggleSkill(skillName) { |
| if (this.personaForm.skills === null) { |
| this.personaForm.skills = this.availableSkills.map(skill => skill.name) |
| .filter(name => name !== skillName); |
| this.skillSelectValue = '1'; |
| } else if (Array.isArray(this.personaForm.skills)) { |
| const index = this.personaForm.skills.indexOf(skillName); |
| if (index !== -1) { |
| this.personaForm.skills.splice(index, 1); |
| } else { |
| this.personaForm.skills.push(skillName); |
| } |
| } else { |
| this.personaForm.skills = [skillName]; |
| this.skillSelectValue = '1'; |
| } |
| }, |
| |
| removeSkill(skillName) { |
| if (this.personaForm.skills === null) { |
| this.personaForm.skills = this.availableSkills.map(skill => skill.name) |
| .filter(name => name !== skillName); |
| this.skillSelectValue = '1'; |
| } else if (Array.isArray(this.personaForm.skills)) { |
| const index = this.personaForm.skills.indexOf(skillName); |
| if (index !== -1) { |
| this.personaForm.skills.splice(index, 1); |
| } |
| } |
| }, |
| |
| truncateText(text, maxLength) { |
| if (!text) return ''; |
| return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; |
| }, |
| |
| getDialogRules(index) { |
| const dialogType = index % 2 === 0 ? this.tm('form.userMessage') : this.tm('form.assistantMessage'); |
| return [ |
| v => !!v || this.tm('validation.dialogRequired', { type: dialogType }), |
| v => (v && v.trim().length > 0) || this.tm('validation.dialogRequired', { type: dialogType }) |
| ]; |
| }, |
| |
| isToolSelected(toolName) { |
| |
| if (this.personaForm.tools === null) { |
| return true; |
| } |
| return Array.isArray(this.personaForm.tools) && this.personaForm.tools.includes(toolName); |
| }, |
| |
| isSkillSelected(skillName) { |
| if (this.personaForm.skills === null) { |
| return true; |
| } |
| return Array.isArray(this.personaForm.skills) && this.personaForm.skills.includes(skillName); |
| }, |
| |
| isServerSelected(server) { |
| if (!server.tools || server.tools.length === 0) return false; |
| |
| |
| if (this.personaForm.tools === null) { |
| return true; |
| } |
| |
| |
| return Array.isArray(this.personaForm.tools) && |
| server.tools.every(toolName => this.personaForm.tools.includes(toolName)); |
| } |
| } |
| } |
| </script> |
| |
| <style scoped> |
| .persona-form-card { |
| border-radius: 12px; |
| overflow: hidden; |
| } |
| |
| .persona-form-content { |
| max-height: min(78vh, 760px); |
| overflow-y: auto; |
| } |
| |
| .persona-form-title { |
| line-height: 1.3; |
| } |
| |
| .persona-form-actions { |
| position: sticky; |
| bottom: 0; |
| z-index: 2; |
| background: rgb(var(--v-theme-surface)); |
| border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); |
| } |
| |
| .selected-config-area { |
| margin-left: 32px; |
| } |
| |
| .persona-form-layout { |
| align-items: flex-start; |
| } |
| |
| .tools-selection { |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| |
| .skills-selection { |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| |
| .v-virtual-scroll { |
| padding-bottom: 16px; |
| } |
| |
| @media (max-width: 600px) { |
| .persona-form-card-mobile { |
| border-radius: 0; |
| } |
| |
| .persona-form-content { |
| max-height: calc(100vh - 128px); |
| padding: 16px !important; |
| } |
| |
| .persona-basic-col, |
| .persona-panels-col { |
| padding-top: 0 !important; |
| } |
| |
| .persona-form-title { |
| font-size: 1.15rem !important; |
| padding: 12px 16px !important; |
| } |
| |
| .selected-config-area { |
| margin-left: 0; |
| } |
| |
| .tools-selection, |
| .skills-selection { |
| max-height: 38vh; |
| } |
| |
| .persona-form-actions { |
| padding: 12px 16px !important; |
| gap: 8px; |
| } |
| |
| .persona-form-actions .v-btn { |
| min-width: 0; |
| } |
| } |
| </style> |
| |