feat(frontend): improve agent and KB editor ID display and intent prompts UX

Expose copyable resource IDs in edit modals and replace the intent prompt
dropdown with independent toggle buttons so multi-intent selection wraps cleanly.
This commit is contained in:
wizardchen
2026-05-28 20:53:34 +08:00
committed by lyingbug
parent 0e1282c2da
commit 83808cc5b7
6 changed files with 269 additions and 91 deletions

View File

@@ -1900,6 +1900,8 @@ export default {
basic: {
title: 'Basic Information',
description: 'Configure the knowledge base name and description',
kbId: 'Knowledge Base ID',
kbIdDesc: 'Use this ID to target the knowledge base in API integrations',
typeLabel: 'Knowledge Base Type',
typeDocument: 'Document-based',
typeFAQ: 'FAQ Q&A',
@@ -4280,9 +4282,12 @@ export default {
},
intentPrompts: {
title: 'Intent Prompts',
sectionDesc: 'Configure intent-specific system prompts; defaults apply when not customized',
intentLabel: 'Intent',
intentDescription: 'Select the intent-specific system prompt to edit',
promptPlaceholder: 'Enter a custom system prompt...',
customized: 'Customized',
empty: 'No intent templates available',
},
selection: {
all: 'All',

View File

@@ -2722,6 +2722,8 @@ export default {
basic: {
title: "기본 정보",
description: "지식베이스의 이름과 설명 정보 설정",
kbId: "지식베이스 ID",
kbIdDesc: "API 연동에서 이 ID로 지식베이스를 지정할 수 있습니다",
typeLabel: "지식베이스 유형",
typeDocument: "문서",
typeFAQ: "Q&A",
@@ -4342,9 +4344,12 @@ export default {
},
intentPrompts: {
title: '의도 프롬프트',
sectionDesc: '쿼리 의도별 시스템 프롬프트를 설정합니다. 사용자 정의가 없으면 기본 템플릿이 적용됩니다',
intentLabel: '의도',
intentDescription: '편집할 의도별 시스템 프롬프트를 선택하세요',
promptPlaceholder: '사용자 정의 시스템 프롬프트 입력...',
customized: '사용자 정의됨',
empty: '사용 가능한 의도 템플릿이 없습니다',
},
selection: {
all: '전체',

View File

@@ -2235,6 +2235,8 @@ export default {
basic: {
title: 'Основная информация',
description: 'Укажите название и описание базы знаний',
kbId: 'ID базы знаний',
kbIdDesc: 'Используйте этот ID для указания базы знаний в API-интеграциях',
typeLabel: 'Тип базы знаний',
typeDocument: 'Документальная',
typeFAQ: 'FAQ (вопрос-ответ)',
@@ -3842,9 +3844,12 @@ export default {
},
intentPrompts: {
title: 'Промпты намерений',
sectionDesc: 'Настройте системные промпты для разных намерений запроса; по умолчанию используются шаблоны системы',
intentLabel: 'Намерение',
intentDescription: 'Выберите системный промпт для редактирования',
promptPlaceholder: 'Введите пользовательский системный промпт...',
customized: 'Настроено',
empty: 'Нет доступных шаблонов намерений',
},
selection: {
all: 'Все',

View File

@@ -2697,6 +2697,8 @@ export default {
basic: {
title: "基本信息",
description: "设置知识库的名称和描述信息",
kbId: "知识库 ID",
kbIdDesc: "API 集成时可使用此 ID 指定知识库",
typeLabel: "知识库类型",
typeDocument: "文档",
typeFAQ: "问答",
@@ -4274,9 +4276,12 @@ export default {
},
intentPrompts: {
title: "意图提示词",
sectionDesc: "为不同查询意图配置专属系统提示词;未自定义时使用系统默认模板",
intentLabel: "意图",
intentDescription: "选择要编辑的意图专属系统提示词",
promptPlaceholder: "输入自定义系统提示词...",
customized: "已自定义",
empty: "暂无可用的意图模板",
},
selection: {
all: "全部",

View File

@@ -32,17 +32,19 @@
<!-- 基础设置 -->
<div v-show="currentSection === 'basic'" class="section">
<div class="section-header">
<h2>{{ $t('agent.editor.basicInfo') }}</h2>
<div class="section-header-title">
<h2>{{ $t('agent.editor.basicInfo') }}</h2>
<t-tooltip v-if="isBuiltinAgent" :content="$t('agentEditor.builtinHint')" placement="top">
<span class="builtin-agent-hint" tabindex="0" role="img"
:aria-label="$t('agentEditor.builtinHint')">
<t-icon name="info-circle" />
</span>
</t-tooltip>
</div>
<p class="section-description">{{ $t('agent.editor.basicInfoDesc') }}</p>
</div>
<div class="settings-group">
<!-- 内置智能体提示 -->
<div v-if="isBuiltinAgent" class="builtin-agent-notice">
<t-icon name="info-circle" />
<span>{{ $t('agentEditor.builtinHint') }}</span>
</div>
<!-- 智能体 ID用于 API 集成 -->
<div v-if="mode === 'edit' && props.agent?.id" class="setting-row">
<div class="setting-info">
@@ -50,13 +52,12 @@
<p class="desc">{{ $t('agent.editor.agentIdDesc') }}</p>
</div>
<div class="setting-control">
<div class="agent-id-control">
<t-input :value="props.agent.id" readonly class="agent-id-input" />
<div class="agent-id-field">
<code class="agent-id-value" :title="props.agent.id">{{ props.agent.id }}</code>
<t-tooltip :content="$t('common.copy')" placement="top">
<t-button variant="outline" theme="default" shape="square" @click="copyAgentId">
<template #icon>
<t-icon name="file-copy" />
</template>
<t-button theme="default" size="small" variant="text" class="agent-id-copy"
@click="copyAgentId">
<t-icon name="file-copy" />
</t-button>
</t-tooltip>
</div>
@@ -246,60 +247,72 @@
<div v-if="!isAgentMode" class="setting-row setting-row-vertical">
<div class="setting-info">
<label>{{ $t('agentEditor.intentPrompts.title') }}</label>
<p class="desc">{{ $t('agentEditor.intentPrompts.sectionDesc') }}</p>
</div>
<div class="setting-control setting-control-full">
<div class="intent-prompts-editor">
<div class="intent-selector-row">
<span class="intent-label">{{ $t('agentEditor.intentPrompts.intentLabel') }}</span>
<t-select v-model="selectedIntent" size="small"
:disabled="props.readOnly || intentPromptTemplates.length === 0" class="intent-select">
<t-option v-for="template in intentPromptTemplates" :key="template.id"
:value="template.id" :label="template.name || template.id">
{{ template.name || template.id }}
</t-option>
</t-select>
<span v-if="currentIntentTemplateDesc" class="intent-desc">{{ currentIntentTemplateDesc
}}</span>
<div v-if="intentPromptTemplates.length === 0" class="prompt-disabled-hint">
{{ $t('agentEditor.intentPrompts.empty') }}
</div>
<template v-else>
<div class="intent-toggle-group" role="tablist"
:aria-label="$t('agentEditor.intentPrompts.intentLabel')">
<t-button v-for="template in intentPromptTemplates" :key="template.id" theme="default"
variant="outline" size="small" class="intent-toggle-btn"
:class="{ 'intent-toggle-btn--active': selectedIntent === template.id }"
:disabled="props.readOnly" @click="selectedIntent = template.id">
<span class="intent-toggle-label">
{{ template.name || template.id }}
<t-tooltip v-if="isIntentCustomized(template.id)"
:content="$t('agentEditor.intentPrompts.customized')" placement="top">
<span class="intent-toggle-dot" />
</t-tooltip>
</span>
</t-button>
</div>
<p v-if="currentIntentTemplateDesc" class="intent-active-desc">{{ currentIntentTemplateDesc
}}</p>
<div v-if="placeholderData.system_prompt.length > 0" class="placeholder-tags">
<span class="placeholder-label">{{ $t('agentEditor.placeholders.available') }}</span>
<t-tooltip v-for="placeholder in placeholderData.system_prompt" :key="placeholder.name"
:content="placeholder.description + $t('agentEditor.placeholders.clickToInsert')"
placement="top">
<span class="placeholder-tag" @click="handlePlaceholderClick('intent', placeholder.name)"
v-text="'{{' + placeholder.name + '}}'" />
</t-tooltip>
<span class="placeholder-hint">{{ $t('agentEditor.placeholders.hint') }}</span>
</div>
<div v-if="placeholderData.system_prompt.length > 0" class="placeholder-tags">
<span class="placeholder-label">{{ $t('agentEditor.placeholders.available') }}</span>
<t-tooltip v-for="placeholder in placeholderData.system_prompt" :key="placeholder.name"
:content="placeholder.description + $t('agentEditor.placeholders.clickToInsert')"
placement="top">
<span class="placeholder-tag"
@click="handlePlaceholderClick('intent', placeholder.name)"
v-text="'{{' + placeholder.name + '}}'" />
</t-tooltip>
<span class="placeholder-hint">{{ $t('agentEditor.placeholders.hint') }}</span>
</div>
<div class="textarea-with-template">
<t-textarea ref="intentPromptTextareaRef" v-model="intentEditorValue"
class="system-prompt-textarea" :autosize="{ minRows: 10, maxRows: 25 }"
:disabled="props.readOnly || !selectedIntent"
:placeholder="currentIntentTemplate?.content || $t('agentEditor.intentPrompts.promptPlaceholder')"
@input="handleIntentPromptInput" />
<PromptTemplateSelector type="intentPrompt" position="corner" :intent-id="selectedIntent"
:show-template-picker="false" @reset-default="resetCurrentIntentPrompt" />
</div>
<div class="textarea-with-template">
<t-textarea ref="intentPromptTextareaRef" v-model="intentEditorValue"
class="system-prompt-textarea" :autosize="{ minRows: 10, maxRows: 25 }"
:disabled="props.readOnly || !selectedIntent"
:placeholder="currentIntentTemplate?.content || $t('agentEditor.intentPrompts.promptPlaceholder')"
@input="handleIntentPromptInput" />
<PromptTemplateSelector type="intentPrompt" position="corner" :intent-id="selectedIntent"
:show-template-picker="false" @reset-default="resetCurrentIntentPrompt" />
</div>
<Teleport to="body">
<div v-if="intentPromptPopup.show && filteredIntentPlaceholders.length > 0"
class="placeholder-popup-wrapper" :style="intentPromptPopup.style">
<div class="placeholder-popup">
<div v-for="(placeholder, index) in filteredIntentPlaceholders" :key="placeholder.name"
class="placeholder-item"
:class="{ active: intentPromptPopup.selectedIndex === index }"
@mousedown.prevent="insertGenericPlaceholder('intent', placeholder.name, true)"
@mouseenter="intentPromptPopup.selectedIndex = index">
<div class="placeholder-name">
<code v-html="`{{${placeholder.name}}}`" />
<Teleport to="body">
<div v-if="intentPromptPopup.show && filteredIntentPlaceholders.length > 0"
class="placeholder-popup-wrapper" :style="intentPromptPopup.style">
<div class="placeholder-popup">
<div v-for="(placeholder, index) in filteredIntentPlaceholders" :key="placeholder.name"
class="placeholder-item"
:class="{ active: intentPromptPopup.selectedIndex === index }"
@mousedown.prevent="insertGenericPlaceholder('intent', placeholder.name, true)"
@mouseenter="intentPromptPopup.selectedIndex = index">
<div class="placeholder-name">
<code v-html="`{{${placeholder.name}}}`" />
</div>
<div class="placeholder-desc">{{ placeholder.description }}</div>
</div>
<div class="placeholder-desc">{{ placeholder.description }}</div>
</div>
</div>
</div>
</Teleport>
</Teleport>
</template>
</div>
</div>
</div>
@@ -1862,9 +1875,17 @@ const currentIntentTemplate = computed(() =>
);
const currentIntentTemplateDesc = computed(() =>
currentIntentTemplate.value?.description || t('agentEditor.intentPrompts.intentDescription'),
currentIntentTemplate.value?.description || '',
);
const isIntentCustomized = (intentId: string) => {
const overrides = formData.value.config.intent_prompts || {};
const override = overrides[intentId];
if (!override?.trim()) return false;
const template = intentPromptTemplates.value.find((item) => item.id === intentId);
return override.trim() !== (template?.content || '').trim();
};
const filteredIntentPlaceholders = computed(() => {
if (!intentPromptPopup.value.prefix) {
return placeholderData.value.system_prompt;
@@ -3812,6 +3833,17 @@ const handleSave = async () => {
.section-header {
margin-bottom: 32px;
.section-header-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
h2 {
margin: 0;
}
}
h2 {
font-size: 20px;
font-weight: 600;
@@ -3984,16 +4016,40 @@ const handleSave = async () => {
}
}
.agent-id-control {
.agent-id-field {
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
width: 100%;
}
padding: 6px 8px 6px 12px;
background: var(--td-bg-color-secondarycontainer);
border: 1px solid var(--td-component-stroke);
border-radius: 6px;
.agent-id-input {
flex: 1;
min-width: 0;
.agent-id-value {
flex: 1;
min-width: 0;
margin: 0;
padding: 0;
background: none;
border: none;
font-family: var(--app-font-family-mono);
font-size: 13px;
line-height: 1.5;
color: var(--td-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-id-copy {
flex-shrink: 0;
color: var(--td-text-color-secondary);
&:hover {
color: var(--td-brand-color);
}
}
}
.settings-footer {
@@ -4516,29 +4572,53 @@ const handleSave = async () => {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.intent-toggle-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.intent-selector-row {
display: flex;
.intent-toggle-group :deep(.intent-toggle-btn--active) {
background-color: rgba(7, 192, 95, 0.1);
border-color: var(--td-brand-color);
color: var(--td-brand-color);
font-weight: 500;
&:hover,
&:focus-visible {
background-color: rgba(7, 192, 95, 0.14);
border-color: var(--td-brand-color);
color: var(--td-brand-color);
}
}
.intent-toggle-btn {
max-width: 100%;
}
.intent-toggle-label {
display: inline-flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
gap: 6px;
white-space: nowrap;
}
.intent-label {
font-size: 12px;
color: var(--td-text-color-secondary);
.intent-toggle-dot {
flex-shrink: 0;
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
}
.intent-select {
width: 150px;
}
.intent-desc {
.intent-active-desc {
margin: 0;
font-size: 12px;
color: var(--td-text-color-placeholder);
line-height: 1.4;
line-height: 1.5;
}
// 系统提示词输入框样式
@@ -4663,22 +4743,24 @@ const handleSave = async () => {
}
}
// 内置智能体提示
.builtin-agent-notice {
display: flex;
.builtin-agent-hint {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--td-warning-color-light);
border: 1px solid var(--td-warning-color-focus);
border-radius: 8px;
margin-bottom: 16px;
color: var(--td-warning-color);
font-size: 14px;
color: var(--td-text-color-placeholder);
font-size: 18px;
line-height: 1;
cursor: help;
transition: color 0.2s;
.t-icon {
font-size: 16px;
flex-shrink: 0;
&:hover,
&:focus-visible {
color: var(--td-warning-color);
outline: none;
}
&:focus-visible {
border-radius: 2px;
box-shadow: 0 0 0 2px var(--td-warning-color-focus);
}
}

View File

@@ -41,6 +41,20 @@
<p class="section-desc">{{ $t('knowledgeEditor.basic.description') }}</p>
</div>
<div class="section-body">
<div v-if="mode === 'edit' && props.kbId" class="form-item">
<label class="form-label">{{ $t('knowledgeEditor.basic.kbId') }}</label>
<p class="form-tip">{{ $t('knowledgeEditor.basic.kbIdDesc') }}</p>
<div class="kb-id-field">
<code class="kb-id-value" :title="props.kbId">{{ props.kbId }}</code>
<t-tooltip :content="$t('common.copy')" placement="top">
<t-button theme="default" size="small" variant="text" class="kb-id-copy"
@click="copyKbId">
<t-icon name="file-copy" />
</t-button>
</t-tooltip>
</div>
</div>
<div class="form-item">
<label class="form-label required">{{ $t('knowledgeEditor.basic.typeLabel') }}</label>
<t-radio-group
@@ -405,6 +419,30 @@ const emit = defineEmits<{
(e: 'success', kbId: string): void
}>()
const copyKbId = async () => {
const id = props.kbId
if (!id) return
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(id)
} else {
const textarea = document.createElement('textarea')
textarea.value = id
textarea.setAttribute('readonly', '')
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
MessagePlugin.success(t('common.copied'))
} catch {
MessagePlugin.error(t('common.copyFailed'))
}
}
const currentSection = ref<string>('basic')
const saving = ref(false)
const loading = ref(false)
@@ -1498,6 +1536,44 @@ watch(
color: var(--td-text-color-placeholder);
}
.kb-id-field {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
max-width: 480px;
margin-top: 8px;
padding: 6px 8px 6px 12px;
background: var(--td-bg-color-secondarycontainer);
border: 1px solid var(--td-component-stroke);
border-radius: 6px;
.kb-id-value {
flex: 1;
min-width: 0;
margin: 0;
padding: 0;
background: none;
border: none;
font-family: var(--app-font-family-mono);
font-size: 13px;
line-height: 1.5;
color: var(--td-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.kb-id-copy {
flex-shrink: 0;
color: var(--td-text-color-secondary);
&:hover {
color: var(--td-brand-color);
}
}
}
.granularity-radio-group {
margin-top: 4px;
}