mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-27 20:57:13 +00:00
feat(web): streamline management and knowledge settings
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user