mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-20 15:37:27 +08:00
feat: simplify resource setup and detail guides
This commit is contained in:
@@ -115,7 +115,6 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
||||
if (isCreateMode) {
|
||||
return (
|
||||
<AgentCreateContent
|
||||
guideEnabled={canManage}
|
||||
onCreated={(newAgentId) => {
|
||||
refreshPipelines();
|
||||
navigate(`/home/agents?id=${encodeURIComponent(newAgentId)}`);
|
||||
|
||||
@@ -27,16 +27,11 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
import ProcessorTypeDiagram from './ProcessorTypeDiagram';
|
||||
import GuidedTour, {
|
||||
GuidedTourStep,
|
||||
} from '@/app/home/components/guided-tour/GuidedTour';
|
||||
|
||||
export default function AgentCreateContent({
|
||||
onCreated,
|
||||
guideEnabled = true,
|
||||
}: {
|
||||
onCreated: (agentId: string) => void;
|
||||
guideEnabled?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [kind, setKind] = useState<AgentKind>('agent');
|
||||
@@ -114,29 +109,6 @@ export default function AgentCreateContent({
|
||||
description: t('agents.eventProcessor.description'),
|
||||
},
|
||||
];
|
||||
const guideSteps: GuidedTourStep[] = [
|
||||
{
|
||||
id: 'type',
|
||||
target: '[data-guide="processor-type"]',
|
||||
title: t('guidedTour.processorCreate.type.title'),
|
||||
description: t('guidedTour.processorCreate.type.description'),
|
||||
},
|
||||
{
|
||||
id: 'basic',
|
||||
target: '[data-guide="processor-basic"]',
|
||||
title: t('guidedTour.processorCreate.basic.title'),
|
||||
description: t('guidedTour.processorCreate.basic.description'),
|
||||
complete: Boolean(form.watch('name')?.trim()),
|
||||
requirement: t('guidedTour.processorCreate.basic.requirement'),
|
||||
},
|
||||
{
|
||||
id: 'submit',
|
||||
target: '[data-guide="processor-submit"]',
|
||||
title: t('guidedTour.processorCreate.submit.title'),
|
||||
description: t('guidedTour.processorCreate.submit.description'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
@@ -147,7 +119,6 @@ export default function AgentCreateContent({
|
||||
type="submit"
|
||||
form="agent-create-form"
|
||||
disabled={form.formState.isSubmitting}
|
||||
data-guide="processor-submit"
|
||||
>
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
@@ -160,7 +131,6 @@ export default function AgentCreateContent({
|
||||
<section
|
||||
aria-labelledby="processor-kind-heading"
|
||||
className="space-y-3"
|
||||
data-guide="processor-type"
|
||||
>
|
||||
<div>
|
||||
<h2
|
||||
@@ -211,7 +181,7 @@ export default function AgentCreateContent({
|
||||
</ToggleGroup>
|
||||
</section>
|
||||
|
||||
<Card data-guide="processor-basic">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('agents.basicInfo')}</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -287,12 +257,6 @@ export default function AgentCreateContent({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GuidedTour
|
||||
enabled={guideEnabled}
|
||||
storageKey="langbot_processor_create_guide_v1"
|
||||
steps={guideSteps}
|
||||
testId="processor-create-guide"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ import RunnerSelect from './RunnerSelect';
|
||||
import GuidedTour, {
|
||||
GuidedTourStep,
|
||||
} from '@/app/home/components/guided-tour/GuidedTour';
|
||||
import { areRequiredDynamicFieldsComplete } from '@/app/home/components/guided-tour/dynamic-form-progress';
|
||||
import AgentApiToolPicker from './AgentApiToolPicker';
|
||||
|
||||
const OTHER_TOOL_SCOPES = [
|
||||
@@ -482,8 +481,6 @@ function AgentFormComponent(
|
||||
target: '[data-guide="runner-selector"]',
|
||||
title: t('guidedTour.runner.select.title'),
|
||||
description: t('guidedTour.runner.select.description'),
|
||||
complete: Boolean(currentRunner && selectedRunnerOption),
|
||||
requirement: t('guidedTour.runner.select.requirement'),
|
||||
action: {
|
||||
href: 'https://space.langbot.app/market?type=plugin&component=Runner&runner_usage=agent',
|
||||
label: t('guidedTour.runner.select.action'),
|
||||
@@ -497,11 +494,6 @@ function AgentFormComponent(
|
||||
target: '[data-guide="runner-parameters"]',
|
||||
title: t('guidedTour.runner.parameters.title'),
|
||||
description: t('guidedTour.runner.parameters.description'),
|
||||
complete: areRequiredDynamicFieldsComplete(
|
||||
activeRunnerStage.config,
|
||||
activeRunnerValues,
|
||||
),
|
||||
requirement: t('guidedTour.runner.parameters.requirement'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -510,18 +502,9 @@ function AgentFormComponent(
|
||||
target: '[data-guide="agent-sections"]',
|
||||
title: t('guidedTour.runner.events.title'),
|
||||
description: t('guidedTour.runner.events.description'),
|
||||
complete: activeSection === 'events_and_tools',
|
||||
requirement: t('guidedTour.runner.events.requirement'),
|
||||
});
|
||||
return steps;
|
||||
}, [
|
||||
activeRunnerStage,
|
||||
activeRunnerValues,
|
||||
activeSection,
|
||||
currentRunner,
|
||||
selectedRunnerOption,
|
||||
t,
|
||||
]);
|
||||
}, [activeRunnerStage, t]);
|
||||
|
||||
useEffect(() => {
|
||||
onRunnerStatusChange?.(runnerStatus);
|
||||
|
||||
@@ -348,7 +348,7 @@ export default function RunnerSelect({
|
||||
<SelectValue placeholder={t('common.select')} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-72 w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
|
||||
<SelectContent className="z-[70] max-h-72 w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
|
||||
<SelectGroup>
|
||||
<SelectLabel className="px-2 py-1 text-[11px] font-medium">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('bots.createBot')}</h1>
|
||||
{canManage && (
|
||||
{canManage && adapterLabel && (
|
||||
<Button type="submit" form="bot-form" data-guide="bot-submit">
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
@@ -171,13 +171,14 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
|
||||
{/* Content */}
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<div className="mx-auto w-full min-w-0 max-w-3xl pb-8">
|
||||
<div className="mx-auto w-full min-w-0 max-w-7xl pb-8">
|
||||
<fieldset className="contents" disabled={!canManage}>
|
||||
<BotForm
|
||||
initBotId={undefined}
|
||||
onFormSubmit={handleFormSubmit}
|
||||
onNewBotCreated={handleNewBotCreated}
|
||||
guideEnabled={canManage}
|
||||
onAdapterLabelChange={setAdapterLabel}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -236,6 +237,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
form="bot-form"
|
||||
disabled={!formDirty}
|
||||
className={activeTab !== 'config' ? 'invisible' : ''}
|
||||
data-guide="bot-config-save"
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
@@ -318,6 +320,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
onNewBotCreated={handleNewBotCreated}
|
||||
onDirtyChange={setFormDirty}
|
||||
onAdapterLabelChange={setAdapterLabel}
|
||||
guideEnabled={canManage}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Check, ChevronRight, ExternalLink, LockKeyhole } from 'lucide-react';
|
||||
import { Check, ChevronRight, ExternalLink, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
@@ -24,9 +24,6 @@ export interface GuidedTourStep {
|
||||
target: string;
|
||||
title: string;
|
||||
description: string;
|
||||
complete?: boolean;
|
||||
advanceOnComplete?: boolean;
|
||||
requirement?: string;
|
||||
action?: {
|
||||
href: string;
|
||||
label: string;
|
||||
@@ -78,14 +75,9 @@ export default function GuidedTour({
|
||||
useState<PopoverPosition | null>(null);
|
||||
const popoverRef = useRef<HTMLDivElement | null>(null);
|
||||
const previousStorageKeyRef = useRef(storageKey);
|
||||
const previousCompletionRef = useRef<{
|
||||
stepId: string;
|
||||
complete: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const activeIndex = steps.findIndex((step) => step.id === activeStepId);
|
||||
const activeStep = activeIndex >= 0 ? steps[activeIndex] : undefined;
|
||||
const isComplete = activeStep?.complete !== false;
|
||||
const isPopoverPositioned = popoverPosition !== null;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,15 +97,7 @@ export default function GuidedTour({
|
||||
}
|
||||
|
||||
const currentIndex = steps.findIndex((step) => step.id === activeStepId);
|
||||
const firstIncompleteIndex = steps.findIndex(
|
||||
(step) => step.complete === false,
|
||||
);
|
||||
const nextIndex =
|
||||
currentIndex < 0
|
||||
? 0
|
||||
: firstIncompleteIndex >= 0 && firstIncompleteIndex < currentIndex
|
||||
? firstIncompleteIndex
|
||||
: currentIndex;
|
||||
const nextIndex = currentIndex < 0 ? 0 : currentIndex;
|
||||
|
||||
if (nextIndex !== currentIndex) {
|
||||
const nextStep = steps[nextIndex];
|
||||
@@ -208,27 +192,33 @@ export default function GuidedTour({
|
||||
if (isPopoverPositioned) measure();
|
||||
}, [activeStep?.id, isPopoverPositioned, measure]);
|
||||
|
||||
const finishTour = useCallback(() => {
|
||||
setTargetRect(null);
|
||||
setPopoverPosition(null);
|
||||
storeProgress(storageKey, 'completed');
|
||||
setFinished(true);
|
||||
setActiveStepId(null);
|
||||
}, [storageKey]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (!activeStep || !isComplete) return;
|
||||
if (!activeStep) return;
|
||||
const nextStep = steps[activeIndex + 1];
|
||||
setTargetRect(null);
|
||||
setPopoverPosition(null);
|
||||
if (!nextStep) {
|
||||
storeProgress(storageKey, 'completed');
|
||||
setFinished(true);
|
||||
setActiveStepId(null);
|
||||
finishTour();
|
||||
return;
|
||||
}
|
||||
storeProgress(storageKey, nextStep.id);
|
||||
setActiveStepId(nextStep.id);
|
||||
}, [activeIndex, activeStep, isComplete, steps, storageKey]);
|
||||
}, [activeIndex, activeStep, finishTour, steps, storageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleNativeClick = (event: MouseEvent) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
const button = target.closest<HTMLButtonElement>(
|
||||
'[data-guided-tour-action="next"]',
|
||||
'[data-guided-tour-action]',
|
||||
);
|
||||
if (
|
||||
!button ||
|
||||
@@ -237,33 +227,18 @@ export default function GuidedTour({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
handleNext();
|
||||
if (button.dataset.guidedTourAction === 'skip') {
|
||||
finishTour();
|
||||
} else if (button.dataset.guidedTourAction === 'next') {
|
||||
handleNext();
|
||||
}
|
||||
};
|
||||
|
||||
// Translation extensions can rewrite nodes inside the popover and detach
|
||||
// React's delegated handler. Capture the command by its stable data marker.
|
||||
document.addEventListener('click', handleNativeClick, true);
|
||||
return () => document.removeEventListener('click', handleNativeClick, true);
|
||||
}, [handleNext, testId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeStep) return;
|
||||
const previous = previousCompletionRef.current;
|
||||
previousCompletionRef.current = {
|
||||
stepId: activeStep.id,
|
||||
complete: isComplete,
|
||||
};
|
||||
if (
|
||||
!activeStep.advanceOnComplete ||
|
||||
previous?.stepId !== activeStep.id ||
|
||||
previous.complete ||
|
||||
!isComplete
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(handleNext, 0);
|
||||
}, [activeStep, handleNext, isComplete]);
|
||||
}, [finishTour, handleNext, testId]);
|
||||
|
||||
if (
|
||||
!enabled ||
|
||||
@@ -282,7 +257,6 @@ export default function GuidedTour({
|
||||
<div
|
||||
data-testid={testId}
|
||||
data-active-step={activeStep.id}
|
||||
data-step-complete={String(isComplete)}
|
||||
translate="no"
|
||||
className="notranslate pointer-events-none"
|
||||
>
|
||||
@@ -301,19 +275,30 @@ export default function GuidedTour({
|
||||
ref={popoverRef}
|
||||
role="dialog"
|
||||
aria-labelledby={titleId}
|
||||
className="pointer-events-auto fixed z-[62] rounded-lg border bg-popover p-4 text-popover-foreground shadow-xl"
|
||||
className="pointer-events-auto fixed z-[80] rounded-lg border bg-popover p-4 text-popover-foreground shadow-xl"
|
||||
style={popoverPosition}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">
|
||||
{t('guidedTour.label')}
|
||||
</span>
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
<span className="ml-auto text-xs tabular-nums text-muted-foreground">
|
||||
{t('guidedTour.progress', {
|
||||
current: activeIndex + 1,
|
||||
total: steps.length,
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-guided-tour-action="skip"
|
||||
data-guided-tour-id={testId}
|
||||
className="-my-2 -mr-2 h-7 gap-1 px-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
{t('guidedTour.skip')}
|
||||
</Button>
|
||||
</div>
|
||||
<h2 id={titleId} className="text-base font-semibold">
|
||||
{activeStep.title}
|
||||
@@ -332,18 +317,11 @@ export default function GuidedTour({
|
||||
<ExternalLink className="size-3.5" />
|
||||
</a>
|
||||
)}
|
||||
{!isComplete && activeStep.requirement && (
|
||||
<div className="mt-3 flex items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs leading-5 text-muted-foreground">
|
||||
<LockKeyhole className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>{activeStep.requirement}</span>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
data-guided-tour-action="next"
|
||||
data-guided-tour-id={testId}
|
||||
className="mt-4 w-full"
|
||||
disabled={!isComplete}
|
||||
>
|
||||
{isLastStep ? (
|
||||
<Check className="size-4" />
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
IDynamicFormItemSchema,
|
||||
SYSTEM_FIELD_PREFIX,
|
||||
} from '@/app/infra/entities/form/dynamic';
|
||||
|
||||
function isVisible(
|
||||
item: IDynamicFormItemSchema,
|
||||
values: Record<string, unknown>,
|
||||
externalValues: Record<string, unknown> = {},
|
||||
) {
|
||||
if (!item.show_if || item.show_if.field.startsWith(SYSTEM_FIELD_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
const actual =
|
||||
values[item.show_if.field] ?? externalValues[item.show_if.field];
|
||||
if (item.show_if.operator === 'eq') return actual === item.show_if.value;
|
||||
if (item.show_if.operator === 'neq') return actual !== item.show_if.value;
|
||||
return (
|
||||
Array.isArray(item.show_if.value) && item.show_if.value.includes(actual)
|
||||
);
|
||||
}
|
||||
|
||||
function hasValue(value: unknown) {
|
||||
if (value === null || value === undefined) return false;
|
||||
if (typeof value === 'string') return value.trim().length > 0;
|
||||
if (Array.isArray(value)) return value.length > 0;
|
||||
if (typeof value === 'object' && 'primary' in value) {
|
||||
return String((value as { primary?: unknown }).primary ?? '').trim() !== '';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function areRequiredDynamicFieldsComplete(
|
||||
items: IDynamicFormItemSchema[],
|
||||
values: Record<string, unknown>,
|
||||
externalValues?: Record<string, unknown>,
|
||||
) {
|
||||
return items
|
||||
.filter(
|
||||
(item) =>
|
||||
item.required &&
|
||||
!item.name.startsWith(SYSTEM_FIELD_PREFIX) &&
|
||||
isVisible(item, values, externalValues),
|
||||
)
|
||||
.every((item) => hasValue(values[item.name]));
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export default function KBDetailContent({ id }: { id: string }) {
|
||||
</h1>
|
||||
{canManage && (
|
||||
<Button type="submit" form="kb-form" data-guide="knowledge-submit">
|
||||
{t('common.submit')}
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -215,6 +215,7 @@ export default function KBDetailContent({ id }: { id: string }) {
|
||||
form="kb-form"
|
||||
disabled={!formDirty}
|
||||
className={activeTab !== 'metadata' ? 'invisible' : ''}
|
||||
data-guide="knowledge-config-save"
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
@@ -233,16 +234,18 @@ export default function KBDetailContent({ id }: { id: string }) {
|
||||
<FileText className="size-3.5" />
|
||||
{t('knowledge.metadata')}
|
||||
</TabsTrigger>
|
||||
{hasDocumentCapability() && (
|
||||
{kbInfo.initialized !== false && hasDocumentCapability() && (
|
||||
<TabsTrigger value="documents" className="gap-1.5">
|
||||
<FolderOpen className="size-3.5" />
|
||||
{t('knowledge.documents')}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="retrieve" className="gap-1.5">
|
||||
<Search className="size-3.5" />
|
||||
{t('knowledge.retrieve')}
|
||||
</TabsTrigger>
|
||||
{kbInfo.initialized !== false && (
|
||||
<TabsTrigger value="retrieve" className="gap-1.5">
|
||||
<Search className="size-3.5" />
|
||||
{t('knowledge.retrieve')}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
{/* Tab: Metadata */}
|
||||
@@ -258,6 +261,7 @@ export default function KBDetailContent({ id }: { id: string }) {
|
||||
onNewKbCreated={handleNewKbCreated}
|
||||
onKbUpdated={handleKbUpdated}
|
||||
onDirtyChange={setFormDirty}
|
||||
guideEnabled={canManage}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
@@ -299,7 +303,7 @@ export default function KBDetailContent({ id }: { id: string }) {
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab: Documents */}
|
||||
{hasDocumentCapability() && (
|
||||
{kbInfo.initialized !== false && hasDocumentCapability() && (
|
||||
<TabsContent
|
||||
value="documents"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
@@ -315,12 +319,17 @@ export default function KBDetailContent({ id }: { id: string }) {
|
||||
)}
|
||||
|
||||
{/* Tab: Retrieve */}
|
||||
<TabsContent
|
||||
value="retrieve"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<KBRetrieveGeneric kbId={id} retrieveFunction={retrieveFunction} />
|
||||
</TabsContent>
|
||||
{kbInfo.initialized !== false && (
|
||||
<TabsContent
|
||||
value="retrieve"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<KBRetrieveGeneric
|
||||
kbId={id}
|
||||
retrieveFunction={retrieveFunction}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import KnowledgeEngineSelect from './KnowledgeEngineSelect';
|
||||
import GuidedTour, {
|
||||
GuidedTourStep,
|
||||
} from '@/app/home/components/guided-tour/GuidedTour';
|
||||
import { areRequiredDynamicFieldsComplete } from '@/app/home/components/guided-tour/dynamic-form-progress';
|
||||
|
||||
const KNOWLEDGE_ENGINE_MARKETPLACE_URL =
|
||||
'https://space.langbot.app/market?type=plugin&component=KnowledgeEngine';
|
||||
@@ -104,6 +103,7 @@ export default function KBForm({
|
||||
Record<string, unknown>
|
||||
>({});
|
||||
const [isEditing, setIsEditing] = useState(Boolean(initKbId));
|
||||
const [engineInitialized, setEngineInitialized] = useState(true);
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [loadAttempt, setLoadAttempt] = useState(0);
|
||||
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
|
||||
@@ -220,11 +220,13 @@ export default function KBForm({
|
||||
|
||||
setConfigSettings(kb.creation_settings || {});
|
||||
setRetrievalSettings(kb.retrieval_settings || {});
|
||||
setEngineInitialized(kb.initialized !== false);
|
||||
|
||||
// Capture snapshot after a tick so dynamic forms have emitted initial values
|
||||
setTimeout(() => {
|
||||
captureSnapshot();
|
||||
isInitializing.current = false;
|
||||
onDirtyChange?.(kb.initialized === false);
|
||||
}, 500);
|
||||
} catch (err) {
|
||||
isInitializing.current = false;
|
||||
@@ -269,8 +271,8 @@ export default function KBForm({
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
// Validate dynamic forms before submission
|
||||
if (configValidateRef.current) {
|
||||
// Engine parameters are configured only after the draft has been created.
|
||||
if (initKbId && configValidateRef.current) {
|
||||
const configValid = await configValidateRef.current();
|
||||
if (!configValid) {
|
||||
toast.error(t('knowledge.engineSettingsInvalid'));
|
||||
@@ -278,7 +280,7 @@ export default function KBForm({
|
||||
}
|
||||
}
|
||||
|
||||
if (retrievalValidateRef.current) {
|
||||
if (initKbId && retrievalValidateRef.current) {
|
||||
const retrievalValid = await retrievalValidateRef.current();
|
||||
if (!retrievalValid) {
|
||||
toast.error(t('knowledge.retrievalSettingsInvalid'));
|
||||
@@ -293,12 +295,16 @@ export default function KBForm({
|
||||
knowledge_engine_plugin_id: selectedEngineId,
|
||||
creation_settings: configSettings,
|
||||
retrieval_settings: retrievalSettings,
|
||||
...(initKbId
|
||||
? { initialize_engine: !engineInitialized }
|
||||
: { defer_initialization: true }),
|
||||
};
|
||||
|
||||
if (initKbId) {
|
||||
httpClient
|
||||
.updateKnowledgeBase(initKbId, kbData)
|
||||
.then((res) => {
|
||||
setEngineInitialized(true);
|
||||
captureSnapshot();
|
||||
onDirtyChange?.(false);
|
||||
onKbUpdated(res.uuid);
|
||||
@@ -339,21 +345,11 @@ export default function KBForm({
|
||||
|
||||
const guideSteps = useMemo<GuidedTourStep[]>(() => {
|
||||
const steps: GuidedTourStep[] = [
|
||||
{
|
||||
id: 'basic',
|
||||
target: '[data-guide="knowledge-basic"]',
|
||||
title: t('guidedTour.knowledge.basic.title'),
|
||||
description: t('guidedTour.knowledge.basic.description'),
|
||||
complete: Boolean(watchedFormValues.name?.trim()),
|
||||
requirement: t('guidedTour.knowledge.basic.requirement'),
|
||||
},
|
||||
{
|
||||
id: 'engine',
|
||||
target: '[data-guide="knowledge-engine"]',
|
||||
title: t('guidedTour.knowledge.engine.title'),
|
||||
description: t('guidedTour.knowledge.engine.description'),
|
||||
complete: Boolean(selectedEngineId),
|
||||
requirement: t('guidedTour.knowledge.engine.requirement'),
|
||||
action: {
|
||||
href: KNOWLEDGE_ENGINE_MARKETPLACE_URL,
|
||||
label: t('guidedTour.knowledge.engine.action'),
|
||||
@@ -367,12 +363,6 @@ export default function KBForm({
|
||||
target: '[data-guide="knowledge-engine-parameters"]',
|
||||
title: t('guidedTour.knowledge.parameters.title'),
|
||||
description: t('guidedTour.knowledge.parameters.description'),
|
||||
complete: areRequiredDynamicFieldsComplete(
|
||||
configFormItems,
|
||||
configSettings,
|
||||
retrievalSettings,
|
||||
),
|
||||
requirement: t('guidedTour.knowledge.parameters.requirement'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -382,31 +372,17 @@ export default function KBForm({
|
||||
target: '[data-guide="knowledge-retrieval"]',
|
||||
title: t('guidedTour.knowledge.retrieval.title'),
|
||||
description: t('guidedTour.knowledge.retrieval.description'),
|
||||
complete: areRequiredDynamicFieldsComplete(
|
||||
retrievalFormItems,
|
||||
retrievalSettings,
|
||||
configSettings,
|
||||
),
|
||||
requirement: t('guidedTour.knowledge.retrieval.requirement'),
|
||||
});
|
||||
}
|
||||
|
||||
steps.push({
|
||||
id: 'submit',
|
||||
target: '[data-guide="knowledge-submit"]',
|
||||
title: t('guidedTour.knowledge.submit.title'),
|
||||
description: t('guidedTour.knowledge.submit.description'),
|
||||
id: 'save',
|
||||
target: '[data-guide="knowledge-config-save"]',
|
||||
title: t('guidedTour.knowledge.save.title'),
|
||||
description: t('guidedTour.knowledge.save.description'),
|
||||
});
|
||||
return steps;
|
||||
}, [
|
||||
configFormItems,
|
||||
configSettings,
|
||||
retrievalFormItems,
|
||||
retrievalSettings,
|
||||
selectedEngineId,
|
||||
t,
|
||||
watchedFormValues.name,
|
||||
]);
|
||||
}, [configFormItems, retrievalFormItems, t]);
|
||||
|
||||
if (loadFailed)
|
||||
return (
|
||||
@@ -529,7 +505,7 @@ export default function KBForm({
|
||||
)}
|
||||
/>
|
||||
|
||||
{configFormItems.length > 0 && (
|
||||
{isEditing && configFormItems.length > 0 && (
|
||||
<div
|
||||
data-guide="knowledge-engine-parameters"
|
||||
className="space-y-6"
|
||||
@@ -541,7 +517,7 @@ export default function KBForm({
|
||||
onSubmit={(val) =>
|
||||
setConfigSettings(val as Record<string, unknown>)
|
||||
}
|
||||
isEditing={isEditing}
|
||||
isEditing={engineInitialized}
|
||||
externalDependentValues={retrievalSettings}
|
||||
onValidate={(validateFn) =>
|
||||
(configValidateRef.current = validateFn)
|
||||
@@ -553,7 +529,7 @@ export default function KBForm({
|
||||
</Card>
|
||||
|
||||
{/* Retrieval Settings (dynamic form from retrieval_schema) */}
|
||||
{retrievalFormItems.length > 0 && (
|
||||
{isEditing && retrievalFormItems.length > 0 && (
|
||||
<Card data-guide="knowledge-retrieval">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('knowledge.retrievalSettings')}</CardTitle>
|
||||
@@ -578,10 +554,10 @@ export default function KBForm({
|
||||
)}
|
||||
</form>
|
||||
<GuidedTour
|
||||
enabled={!isEditing && guideEnabled}
|
||||
storageKey="langbot_knowledge_create_guide_v1"
|
||||
enabled={isEditing && guideEnabled}
|
||||
storageKey="langbot_knowledge_detail_guide_v1"
|
||||
steps={guideSteps}
|
||||
testId="knowledge-create-guide"
|
||||
testId="knowledge-detail-guide"
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -431,6 +431,9 @@ export interface KnowledgeBase {
|
||||
knowledge_engine_plugin_id?: string;
|
||||
creation_settings?: Record<string, unknown>;
|
||||
retrieval_settings?: Record<string, unknown>;
|
||||
initialized?: boolean;
|
||||
defer_initialization?: boolean;
|
||||
initialize_engine?: boolean;
|
||||
knowledge_engine?: KnowledgeEngineInfo;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user