From 22d9053bf11726e21a39ad15a496bbb0bfa09342 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Tue, 25 Aug 2026 23:02:33 +0800 Subject: [PATCH] feat(web): streamline management and knowledge settings --- .../agents/components/AgentFormComponent.tsx | 29 +++- .../app/home/knowledge/KBDetailContent.tsx | 64 ++++++- .../knowledge/components/kb-form/KBForm.tsx | 161 +++++++++--------- .../pipeline-form/PipelineFormComponent.tsx | 10 +- web/tests/e2e/crud-smoke.spec.ts | 27 +-- .../e2e/processor-detail-workbench.spec.ts | 19 ++- 6 files changed, 203 insertions(+), 107 deletions(-) diff --git a/web/src/app/home/agents/components/AgentFormComponent.tsx b/web/src/app/home/agents/components/AgentFormComponent.tsx index 43134ebcd..80435c5e4 100644 --- a/web/src/app/home/agents/components/AgentFormComponent.tsx +++ b/web/src/app/home/agents/components/AgentFormComponent.tsx @@ -132,7 +132,7 @@ function AgentFormComponent( const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [isSaving, setIsSaving] = useState(false); const [activeSection, setActiveSection] = - useState('basic'); + useState('events'); const isSavingRef = useRef(false); const hasUnsavedChangesRef = useRef(false); @@ -270,11 +270,6 @@ function AgentFormComponent( label: string; icon: React.ElementType; }> = [ - { - name: 'basic', - label: t('common.management'), - icon: Power, - }, { name: 'events', label: t('agents.bindableEvents'), @@ -293,6 +288,11 @@ function AgentFormComponent( icon: SlidersHorizontal, }, ]; + const managementSection = { + name: 'basic' as const, + label: t('common.management'), + icon: Power, + }; const runnerStatus = useMemo(() => { if (pluginStatusLoading) { @@ -568,7 +568,7 @@ function AgentFormComponent( } >
- + {primarySections.map((section) => { const Icon = section.icon; return ( @@ -580,6 +580,21 @@ function AgentFormComponent( })}
+
+ +
diff --git a/web/src/app/home/knowledge/KBDetailContent.tsx b/web/src/app/home/knowledge/KBDetailContent.tsx index d5affc18e..2814e595b 100644 --- a/web/src/app/home/knowledge/KBDetailContent.tsx +++ b/web/src/app/home/knowledge/KBDetailContent.tsx @@ -28,6 +28,10 @@ import { CustomApiError } from '@/app/infra/entities/common'; import { toast } from 'sonner'; import { FileText, FolderOpen, Search, Trash2 } from 'lucide-react'; import { useCurrentWorkspace } from '@/app/infra/http'; +import EntityBasicInfoDialog, { + EntityBasicInfoValues, +} from '@/app/home/components/entity-basic-info/EntityBasicInfoDialog'; +import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton'; export default function KBDetailContent({ id }: { id: string }) { const isCreateMode = id === 'new'; @@ -52,8 +56,10 @@ export default function KBDetailContent({ id }: { id: string }) { const [activeTab, setActiveTab] = useState('metadata'); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showBasicInfoDialog, setShowBasicInfoDialog] = useState(false); const [kbInfo, setKbInfo] = useState(null); const [formDirty, setFormDirty] = useState(false); + const [formVersion, setFormVersion] = useState(0); const loadKbInfo = useCallback( async (kbId: string) => { @@ -99,6 +105,34 @@ export default function KBDetailContent({ id }: { id: string }) { loadKbInfo(id); } + async function handleBasicInfoSave(values: EntityBasicInfoValues) { + if (!kbInfo) return; + + const updateData: KnowledgeBase = { + name: values.name, + description: values.description, + emoji: values.emoji || '📚', + knowledge_engine_plugin_id: kbInfo.knowledge_engine_plugin_id, + creation_settings: kbInfo.creation_settings, + retrieval_settings: kbInfo.retrieval_settings, + }; + + try { + await httpClient.updateKnowledgeBase(id, updateData); + setKbInfo({ ...kbInfo, ...updateData }); + setDetailEntityName(values.name); + setFormDirty(false); + setFormVersion((version) => version + 1); + refreshKnowledgeBases(); + toast.success(t('knowledge.updateKnowledgeBaseSuccess')); + } catch (err) { + toast.error( + t('knowledge.updateKnowledgeBaseFailed') + (err as CustomApiError).msg, + ); + throw err; + } + } + async function confirmDelete() { try { await httpClient.deleteKnowledgeBase(id); @@ -151,9 +185,18 @@ export default function KBDetailContent({ id }: { id: string }) {
{/* Sticky Header: title + save button */}
-

- {t('knowledge.editKnowledgeBase')} -

+
+

+ {kbInfo + ? `${kbInfo.emoji || '📚'} ${kbInfo.name}` + : t('knowledge.editKnowledgeBase')} +

+ {canManage && kbInfo && ( + setShowBasicInfoDialog(true)} + /> + )} +
{canManage && (
+ {kbInfo && ( + + )} + {/* Delete confirmation dialog */} diff --git a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx index c585e0ff9..1acfbf9a3 100644 --- a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx +++ b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Separator } from '@/components/ui/separator'; import { KnowledgeBase, KnowledgeEngine } from '@/app/infra/entities/api'; import { CustomApiError } from '@/app/infra/entities/common'; import { toast } from 'sonner'; @@ -100,7 +101,7 @@ export default function KBForm({ const [retrievalSettings, setRetrievalSettings] = useState< Record >({}); - const [isEditing, setIsEditing] = useState(false); + const [isEditing, setIsEditing] = useState(Boolean(initKbId)); const [loading, setLoading] = useState(true); // Dirty tracking: snapshot of saved state for comparison @@ -341,26 +342,59 @@ export default function KBForm({ id="kb-form" className="space-y-6" > - {/* Card 1: Basic Information */} - - - {t('knowledge.basicInfo')} - - {t('knowledge.basicInfoDescription')} - - - - {/* Name and Emoji in same row */} -
+ {/* Basic information is entered here only during creation. */} + {!isEditing && ( + + + {t('knowledge.basicInfo')} + + {t('knowledge.basicInfoDescription')} + + + + {/* Name and Emoji in same row */} +
+ ( + + + {t('knowledge.kbName')} + * + + + + + + + )} + /> + ( + + {t('common.icon')} + + + + + + )} + /> +
+ + {/* Description */} ( - - - {t('knowledge.kbName')} - * - + + {t('knowledge.kbDescription')} @@ -368,40 +402,19 @@ export default function KBForm({ )} /> - ( - - {t('common.icon')} - - - - - - )} - /> -
+
+
+ )} - {/* Description */} - ( - - {t('knowledge.kbDescription')} - - - - - - )} - /> - - {/* Knowledge Engine Selector */} + {/* Knowledge engine selection and settings stay together. */} + + + {t('knowledge.engineSettings')} + + {t('knowledge.engineSettingsDescription')} + + + )} /> + + {configFormItems.length > 0 && ( + <> + + } + onSubmit={(val) => + setConfigSettings(val as Record) + } + isEditing={isEditing} + externalDependentValues={retrievalSettings} + onValidate={(validateFn) => + (configValidateRef.current = validateFn) + } + /> + + )} - {/* Card 2: Engine Settings (dynamic form from creation_schema) */} - {configFormItems.length > 0 && ( - - - {t('knowledge.engineSettings')} - - {t('knowledge.engineSettingsDescription')} - - - - } - onSubmit={(val) => - setConfigSettings(val as Record) - } - isEditing={isEditing} - externalDependentValues={retrievalSettings} - onValidate={(validateFn) => - (configValidateRef.current = validateFn) - } - /> - - - )} - - {/* Card 3: Retrieval Settings (dynamic form from retrieval_schema) */} + {/* Retrieval Settings (dynamic form from retrieval_schema) */} {retrievalFormItems.length > 0 && ( diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index baa0c886e..278a484fd 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -187,9 +187,13 @@ const PipelineFormComponent = forwardRef< const primarySections = primarySectionNames .map((name) => formLabelList.find((section) => section.name === name)) .filter((section): section is SectionItem => Boolean(section)); - const secondarySections = formLabelList.filter( - (section) => !primarySectionNames.includes(section.name), - ); + const secondarySections = formLabelList + .filter((section) => !primarySectionNames.includes(section.name)) + .sort((left, right) => { + if (left.name === 'basic') return 1; + if (right.name === 'basic') return -1; + return 0; + }); const [aiConfigTabSchema, setAIConfigTabSchema] = useState(); diff --git a/web/tests/e2e/crud-smoke.spec.ts b/web/tests/e2e/crud-smoke.spec.ts index 79c7beeed..3a3b5fd69 100644 --- a/web/tests/e2e/crud-smoke.spec.ts +++ b/web/tests/e2e/crud-smoke.spec.ts @@ -244,18 +244,25 @@ test.describe('frontend CRUD smoke flows', () => { await expect(page).toHaveURL(/\/home\/knowledge\?id=knowledge-1$/); await page.reload(); - await expect(page.locator('input[name="name"]')).toHaveValue( - 'Support Knowledge', - ); - await page.waitForTimeout(600); + await expect( + page.getByRole('heading', { name: /Support Knowledge/ }), + ).toBeVisible(); + await expect(page.locator('input[name="name"]')).toHaveCount(0); + const engineSettings = page.locator('[data-slot="card"]').filter({ + has: page.getByText('Engine Settings', { exact: true }), + }); + await expect(engineSettings.getByRole('combobox')).toBeVisible(); - await page - .locator('input[name="description"]') + await page.getByRole('button', { name: 'Edit basic information' }).click(); + const kbInfoDialog = page.getByRole('dialog'); + await kbInfoDialog.getByLabel('Name').fill('Support Knowledge Updated'); + await kbInfoDialog + .getByLabel('Description') .fill('Updated source material for support answers.'); - await save(page); - await expect(page.locator('input[name="description"]')).toHaveValue( - 'Updated source material for support answers.', - ); + await kbInfoDialog.getByRole('button', { name: 'Save' }).click(); + await expect( + page.getByRole('heading', { name: /Support Knowledge Updated/ }), + ).toBeVisible(); await page.getByRole('button', { name: /^Delete$/ }).click(); await confirmDelete(page); diff --git a/web/tests/e2e/processor-detail-workbench.spec.ts b/web/tests/e2e/processor-detail-workbench.spec.ts index 65a9e3dc9..840514c9f 100644 --- a/web/tests/e2e/processor-detail-workbench.spec.ts +++ b/web/tests/e2e/processor-detail-workbench.spec.ts @@ -45,12 +45,16 @@ test.describe('processor detail workbench', () => { expect(debugBox!.y).toBeGreaterThanOrEqual(0); const flow = configPanel.getByRole('tablist'); - await expect(flow.getByRole('tab').nth(0)).toContainText('Management'); - await expect(flow.getByRole('tab').nth(1)).toContainText( + await expect(flow.getByRole('tab').nth(0)).toContainText( 'Bindable Event Range', ); - await expect(flow.getByRole('tab').nth(2)).toContainText('Runner'); - await expect(flow.getByRole('tab').nth(3)).toContainText('Local Agent'); + await expect(flow.getByRole('tab').nth(1)).toContainText('Runner'); + await expect(flow.getByRole('tab').nth(2)).toContainText('Local Agent'); + const agentManagement = configPanel.getByRole('button', { + name: 'Management', + }); + await expect(agentManagement).toBeVisible(); + await expect(flow.getByText('Management')).toHaveCount(0); await expect( page.getByRole('heading', { name: /agent-workbench/ }), @@ -73,11 +77,11 @@ test.describe('processor detail workbench', () => { .last(), ).toBeVisible(); - await flow.getByRole('tab').nth(1).click(); + await flow.getByRole('tab').nth(0).click(); await expect( configPanel.getByText('Bindable Event Range', { exact: true }).last(), ).toBeVisible(); - await flow.getByRole('tab').nth(3).click(); + await flow.getByRole('tab').nth(2).click(); await expect( configPanel.getByText('Local Agent', { exact: true }).last(), ).toBeVisible(); @@ -217,6 +221,9 @@ test.describe('processor detail workbench', () => { page.getByRole('heading', { name: /Renamed Pipeline/ }), ).toBeVisible(); + const secondaryNavigation = configPanel.locator('nav').getByRole('button'); + await expect(secondaryNavigation.last()).toHaveText('Management'); + const debugBox = await debugPanel.boundingBox(); const configBox = await configPanel.boundingBox(); expect(debugBox).not.toBeNull();