feat(web): streamline management and knowledge settings

This commit is contained in:
RockChinQ
2026-08-25 23:02:33 +08:00
parent db2a9155f8
commit 22d9053bf1
6 changed files with 203 additions and 107 deletions
@@ -132,7 +132,7 @@ function AgentFormComponent(
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [activeSection, setActiveSection] =
useState<AgentConfigSection>('basic');
useState<AgentConfigSection>('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<AgentRunnerStatus>(() => {
if (pluginStatusLoading) {
@@ -568,7 +568,7 @@ function AgentFormComponent(
}
>
<div className="overflow-x-auto">
<TabsList className="grid min-w-[44rem] w-full grid-cols-4">
<TabsList className="grid min-w-[34rem] w-full grid-cols-3">
{primarySections.map((section) => {
const Icon = section.icon;
return (
@@ -580,6 +580,21 @@ function AgentFormComponent(
})}
</TabsList>
</div>
<div className="flex justify-end pt-2">
<Button
type="button"
variant={
activeSection === managementSection.name
? 'secondary'
: 'ghost'
}
size="sm"
onClick={() => setActiveSection(managementSection.name)}
>
<Power />
{managementSection.label}
</Button>
</div>
</Tabs>
</nav>
+61 -3
View File
@@ -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<KnowledgeBase | null>(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 }) {
<div className="flex h-full flex-col">
{/* Sticky Header: title + save button */}
<div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">
{t('knowledge.editKnowledgeBase')}
</h1>
<div className="flex min-w-0 items-center gap-1">
<h1 className="truncate text-xl font-semibold">
{kbInfo
? `${kbInfo.emoji || '📚'} ${kbInfo.name}`
: t('knowledge.editKnowledgeBase')}
</h1>
{canManage && kbInfo && (
<EntityTitleEditButton
onClick={() => setShowBasicInfoDialog(true)}
/>
)}
</div>
{canManage && (
<Button
type="submit"
@@ -198,6 +241,7 @@ export default function KBDetailContent({ id }: { id: string }) {
<div className="mx-auto max-w-3xl space-y-6 pb-8">
<fieldset className="contents" disabled={!canManage}>
<KBForm
key={`${id}-${formVersion}`}
initKbId={id}
onNewKbCreated={handleNewKbCreated}
onKbUpdated={handleKbUpdated}
@@ -268,6 +312,20 @@ export default function KBDetailContent({ id }: { id: string }) {
</Tabs>
</div>
{kbInfo && (
<EntityBasicInfoDialog
open={showBasicInfoDialog}
onOpenChange={setShowBasicInfoDialog}
values={{
name: kbInfo.name,
description: kbInfo.description,
emoji: kbInfo.emoji,
}}
defaultEmoji="📚"
onSave={handleBasicInfoSave}
/>
)}
{/* Delete confirmation dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<DialogContent>
@@ -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<string, unknown>
>({});
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 */}
<Card>
<CardHeader>
<CardTitle>{t('knowledge.basicInfo')}</CardTitle>
<CardDescription>
{t('knowledge.basicInfoDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Name and Emoji in same row */}
<div className="flex gap-4 items-start">
{/* Basic information is entered here only during creation. */}
{!isEditing && (
<Card>
<CardHeader>
<CardTitle>{t('knowledge.basicInfo')}</CardTitle>
<CardDescription>
{t('knowledge.basicInfoDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Name and Emoji in same row */}
<div className="flex gap-4 items-start">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
{t('knowledge.kbName')}
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="emoji"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.icon')}</FormLabel>
<FormControl>
<EmojiPicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Description */}
<FormField
control={form.control}
name="name"
name="description"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
{t('knowledge.kbName')}
<span className="text-destructive">*</span>
</FormLabel>
<FormItem>
<FormLabel>{t('knowledge.kbDescription')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -368,40 +402,19 @@ export default function KBForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="emoji"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.icon')}</FormLabel>
<FormControl>
<EmojiPicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
)}
{/* Description */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('knowledge.kbDescription')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Knowledge Engine Selector */}
{/* Knowledge engine selection and settings stay together. */}
<Card>
<CardHeader>
<CardTitle>{t('knowledge.engineSettings')}</CardTitle>
<CardDescription>
{t('knowledge.engineSettingsDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<FormField
control={form.control}
name="ragEngineId"
@@ -484,36 +497,28 @@ export default function KBForm({
</FormItem>
)}
/>
{configFormItems.length > 0 && (
<>
<Separator />
<DynamicFormComponent
itemConfigList={configFormItems}
initialValues={configSettings as Record<string, object>}
onSubmit={(val) =>
setConfigSettings(val as Record<string, unknown>)
}
isEditing={isEditing}
externalDependentValues={retrievalSettings}
onValidate={(validateFn) =>
(configValidateRef.current = validateFn)
}
/>
</>
)}
</CardContent>
</Card>
{/* Card 2: Engine Settings (dynamic form from creation_schema) */}
{configFormItems.length > 0 && (
<Card>
<CardHeader>
<CardTitle>{t('knowledge.engineSettings')}</CardTitle>
<CardDescription>
{t('knowledge.engineSettingsDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<DynamicFormComponent
itemConfigList={configFormItems}
initialValues={configSettings as Record<string, object>}
onSubmit={(val) =>
setConfigSettings(val as Record<string, unknown>)
}
isEditing={isEditing}
externalDependentValues={retrievalSettings}
onValidate={(validateFn) =>
(configValidateRef.current = validateFn)
}
/>
</CardContent>
</Card>
)}
{/* Card 3: Retrieval Settings (dynamic form from retrieval_schema) */}
{/* Retrieval Settings (dynamic form from retrieval_schema) */}
{retrievalFormItems.length > 0 && (
<Card>
<CardHeader>
@@ -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<PipelineConfigTab>();