mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-29 21:06:41 +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;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ const enUS = {
|
||||
progress: '{{current}} of {{total}}',
|
||||
next: 'Next',
|
||||
finish: 'Finish',
|
||||
skip: 'Skip',
|
||||
bot: {
|
||||
connection: {
|
||||
title: 'Choose a connection method',
|
||||
@@ -68,7 +69,7 @@ const enUS = {
|
||||
adapter: {
|
||||
title: 'Choose a platform adapter',
|
||||
description:
|
||||
'Choose the platform first. LangBot will then show whether this adapter supports Webhook, persistent connection, or both.',
|
||||
'Choose the platform adapter for this bot. Connection methods and platform parameters are configured after creation.',
|
||||
requirement: 'Select an adapter to continue.',
|
||||
},
|
||||
parameters: {
|
||||
@@ -81,12 +82,17 @@ const enUS = {
|
||||
routing: {
|
||||
title: 'Route incoming events',
|
||||
description:
|
||||
'Choose which processor handles each event. You can add more routes after the bot is created.',
|
||||
'Choose which processor handles each event received by this bot.',
|
||||
},
|
||||
save: {
|
||||
title: 'Save the bot configuration',
|
||||
description:
|
||||
'Save after checking the connection parameters and event routing. You can then enable the bot when it is ready.',
|
||||
},
|
||||
submit: {
|
||||
title: 'Create the bot',
|
||||
description:
|
||||
'Create the bot to apply the connection. Webhook URLs generated by LangBot are shown in the saved bot configuration.',
|
||||
'Create a disabled bot, then continue with its connection method and platform parameters on the bot page.',
|
||||
},
|
||||
},
|
||||
processorCreate: {
|
||||
@@ -136,9 +142,9 @@ const enUS = {
|
||||
requirement: 'Enter a knowledge base name to continue.',
|
||||
},
|
||||
engine: {
|
||||
title: 'Choose or install an engine',
|
||||
title: 'Review the knowledge engine',
|
||||
description:
|
||||
'Select an installed knowledge engine, or install one from the Marketplace section in this selector.',
|
||||
'Confirm the engine used by this knowledge base. Its parameters and retrieval settings are configured below.',
|
||||
requirement: 'Select a knowledge engine to continue.',
|
||||
action: 'Browse Knowledge Engine Marketplace',
|
||||
},
|
||||
@@ -154,6 +160,11 @@ const enUS = {
|
||||
'Set how this engine searches and returns relevant content to processors.',
|
||||
requirement: 'Complete every visible required retrieval parameter.',
|
||||
},
|
||||
save: {
|
||||
title: 'Save the knowledge base configuration',
|
||||
description:
|
||||
'Save after checking the engine parameters and retrieval settings.',
|
||||
},
|
||||
submit: {
|
||||
title: 'Create the knowledge base',
|
||||
description:
|
||||
@@ -570,7 +581,7 @@ const enUS = {
|
||||
getBotConfigError: 'Failed to get bot configuration: ',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Save failed: ',
|
||||
createSuccess: 'Created successfully. Please configure event routing',
|
||||
createSuccess: 'Created successfully. Continue configuring the bot',
|
||||
createError: 'Creation failed: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Delete failed: ',
|
||||
|
||||
@@ -48,6 +48,7 @@ const zhHans = {
|
||||
progress: '第 {{current}} 步,共 {{total}} 步',
|
||||
next: '下一步',
|
||||
finish: '完成引导',
|
||||
skip: '跳过',
|
||||
bot: {
|
||||
connection: {
|
||||
title: '选择接入方式',
|
||||
@@ -63,7 +64,7 @@ const zhHans = {
|
||||
adapter: {
|
||||
title: '选择平台适配器',
|
||||
description:
|
||||
'先选择平台,LangBot 会再判断该适配器支持 Webhook、长连接或两者都支持。',
|
||||
'选择机器人使用的平台适配器;接入方式和平台参数将在创建后配置。',
|
||||
requirement: '请选择一个适配器。',
|
||||
},
|
||||
parameters: {
|
||||
@@ -75,13 +76,16 @@ const zhHans = {
|
||||
},
|
||||
routing: {
|
||||
title: '设置事件路由',
|
||||
description:
|
||||
'选择各类事件交给哪个处理器;机器人创建后仍可继续添加路由。',
|
||||
description: '选择该机器人收到的各类事件交给哪个处理器。',
|
||||
},
|
||||
save: {
|
||||
title: '保存机器人配置',
|
||||
description: '确认接入参数和事件路由后保存;准备完成后即可启用机器人。',
|
||||
},
|
||||
submit: {
|
||||
title: '创建机器人',
|
||||
description:
|
||||
'创建后连接配置才会生效;LangBot 生成的 Webhook 地址会显示在已保存机器人的配置中。',
|
||||
'先创建未启用的机器人,再到机器人页面配置接入方式和平台参数。',
|
||||
},
|
||||
},
|
||||
processorCreate: {
|
||||
@@ -130,9 +134,8 @@ const zhHans = {
|
||||
requirement: '请先填写知识库名称。',
|
||||
},
|
||||
engine: {
|
||||
title: '选择或安装知识引擎',
|
||||
description:
|
||||
'选择已安装的知识引擎,也可以在此选择器的市场区域直接安装。',
|
||||
title: '确认知识引擎',
|
||||
description: '确认该知识库使用的引擎,并在下方配置引擎参数和检索方式。',
|
||||
requirement: '请选择一个知识引擎。',
|
||||
action: '浏览知识引擎市场',
|
||||
},
|
||||
@@ -146,6 +149,10 @@ const zhHans = {
|
||||
description: '设置引擎如何搜索内容,以及如何把相关结果返回给处理器。',
|
||||
requirement: '请填写当前可见的全部必填检索参数。',
|
||||
},
|
||||
save: {
|
||||
title: '保存知识库配置',
|
||||
description: '确认引擎参数和检索设置后保存配置。',
|
||||
},
|
||||
submit: {
|
||||
title: '创建知识库',
|
||||
description: '创建后即可添加文档,或连接所选引擎支持的外部知识源。',
|
||||
@@ -537,7 +544,7 @@ const zhHans = {
|
||||
getBotConfigError: '获取机器人配置失败:',
|
||||
saveSuccess: '保存成功',
|
||||
saveError: '保存失败:',
|
||||
createSuccess: '创建成功,请配置事件路由',
|
||||
createSuccess: '创建成功,请继续配置机器人',
|
||||
createError: '创建失败:',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteError: '删除失败:',
|
||||
|
||||
Reference in New Issue
Block a user