mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-21 03:46:11 +00:00
Fix/frontend optimizations (#2088)
* fix(web): auto-redirect to wizard on first visit and change sidebar icons to blue * refactor(wizard): use backend metadata table instead of localStorage for wizard completion state - Add wizard_completed field to system info API (read from metadata table) - Add POST /api/v1/system/wizard/completed endpoint to mark wizard done - Frontend home layout checks systemInfo.wizard_completed for auto-redirect - Wizard calls markWizardCompleted API on skip/finish - Ensures consistent behavior across all browsers on the same instance * fix(wizard): update systemInfo in memory before navigation to prevent redirect loop * fix(monitoring): prevent horizontal overflow and unify empty state styles * fix(wizard): use Object.assign for systemInfo and await wizard completion API - Replace systemInfo reassignment with Object.assign in all 3 locations to preserve object identity across module imports - Await markWizardCompleted() POST in wizard skip/finish handlers instead of fire-and-forget to ensure backend persistence - Always re-fetch systemInfo in home layout to get latest wizard_completed state from backend * fix(wizard): prevent redirect loop by blocking navigation on failed status save - Refactor wizard_completed (boolean) to wizard_status (string: none/skipped/completed) - Remove ALL localStorage usage from wizard page (form state persistence) - Replace AlertDialogAction with Button so skip dialog stays open during POST - Add loading spinners for skip and complete actions - If POST fails, show error toast and keep dialog/button active for retry - If POST succeeds, update in-memory state and navigate * fix(wizard): fix row[0].value bug causing GET /info to always return wizard_status=none conn.execute(select(Entity)) returns Row with raw column values, not ORM entities. row[0] is the key column (a string), so row[0].value raises AttributeError which was silently swallowed by except-pass, making the GET endpoint always return wizard_status=none regardless of DB state. * fix(wizard): replace AlertDialog with Dialog for skip confirmation to remove slide animation * chore: optimize toast in wizard * fix(wizard): set default token value for Telegram adapter and initialize adapter config in wizard * feat(web): move webhook URL to dynamic form system, add market category filter, fix layout overflow - Add 'webhook-url' dynamic form field type rendered as read-only input with copy button, defined in adapter YAML specs instead of hardcoded in BotForm. Supports show_if conditions for optional-webhook adapters. - Remove hardcoded webhook display logic from BotForm.tsx, pass webhook URLs via systemContext to DynamicFormComponent. - Fetch webhook URLs after bot creation in wizard and pass to Step 1. - Support ?category= query param on /home/market page for filtering by component type (mirrors langbot-space behavior). - Link 'install knowledge engine' hint to /home/market?category=KnowledgeEngine. - Fix SidebarInset missing min-w-0 causing content overflow when sidebar is expanded. - Add vertical divider between plugin detail config and readme panels. - Fix infinite re-render loop in DynamicFormComponent by memoizing editableItems array. * fix: lint * fix(web): change systemInfo to const to satisfy prefer-const lint rule * fix: update adapter descriptions for clarity and usage requirements
This commit is contained in:
+121
-118
@@ -18,6 +18,7 @@ import {
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import {
|
||||
userInfo,
|
||||
systemInfo,
|
||||
initializeUserInfo,
|
||||
initializeSystemInfo,
|
||||
} from '@/app/infra/http';
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
} from '@/app/infra/entities/pipeline';
|
||||
import {
|
||||
DynamicFormItemConfig,
|
||||
getDefaultValues,
|
||||
parseDynamicFormItemType,
|
||||
} from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
@@ -47,63 +49,20 @@ import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface WizardState {
|
||||
currentStep: number;
|
||||
selectedAdapter: string | null;
|
||||
selectedRunner: string | null;
|
||||
botName: string;
|
||||
botDescription: string;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
runnerConfig: Record<string, unknown>;
|
||||
createdBotUuid: string | null;
|
||||
}
|
||||
|
||||
const WIZARD_STORAGE_KEY = 'langbot_wizard_state';
|
||||
|
||||
const TOTAL_STEPS = 4;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistence helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadWizardState(): WizardState | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = localStorage.getItem(WIZARD_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as WizardState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveWizardState(state: WizardState): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
localStorage.setItem(WIZARD_STORAGE_KEY, JSON.stringify(state));
|
||||
} catch {
|
||||
// localStorage may be full - silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
function clearWizardState(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.removeItem(WIZARD_STORAGE_KEY);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main Wizard Page (full-screen, no sidebar)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -113,30 +72,19 @@ export default function WizardPage() {
|
||||
const router = useRouter();
|
||||
|
||||
// ---- Wizard state ----
|
||||
const restoredState = useRef(loadWizardState());
|
||||
const [currentStep, setCurrentStep] = useState(
|
||||
restoredState.current?.currentStep ?? 0,
|
||||
);
|
||||
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(
|
||||
restoredState.current?.selectedAdapter ?? null,
|
||||
);
|
||||
const [selectedRunner, setSelectedRunner] = useState<string | null>(
|
||||
restoredState.current?.selectedRunner ?? null,
|
||||
);
|
||||
const [botName, setBotName] = useState(restoredState.current?.botName ?? '');
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
||||
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
|
||||
const [botName, setBotName] = useState('');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [botDescription, _setBotDescription] = useState(
|
||||
restoredState.current?.botDescription ?? '',
|
||||
);
|
||||
const [botDescription, _setBotDescription] = useState('');
|
||||
const [adapterConfig, setAdapterConfig] = useState<Record<string, unknown>>(
|
||||
restoredState.current?.adapterConfig ?? {},
|
||||
);
|
||||
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>(
|
||||
restoredState.current?.runnerConfig ?? {},
|
||||
);
|
||||
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(
|
||||
restoredState.current?.createdBotUuid ?? null,
|
||||
{},
|
||||
);
|
||||
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({});
|
||||
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
|
||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
||||
|
||||
// ---- Remote data ----
|
||||
const [adapters, setAdapters] = useState<Adapter[]>([]);
|
||||
@@ -149,29 +97,6 @@ export default function WizardPage() {
|
||||
const [isSavingBot, setIsSavingBot] = useState(false);
|
||||
const [botSaved, setBotSaved] = useState(false);
|
||||
|
||||
// ---- Persist state on every change ----
|
||||
useEffect(() => {
|
||||
saveWizardState({
|
||||
currentStep,
|
||||
selectedAdapter,
|
||||
selectedRunner,
|
||||
botName,
|
||||
botDescription,
|
||||
adapterConfig,
|
||||
runnerConfig,
|
||||
createdBotUuid,
|
||||
});
|
||||
}, [
|
||||
currentStep,
|
||||
selectedAdapter,
|
||||
selectedRunner,
|
||||
botName,
|
||||
botDescription,
|
||||
adapterConfig,
|
||||
runnerConfig,
|
||||
createdBotUuid,
|
||||
]);
|
||||
|
||||
// ---- Fetch remote data ----
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -300,16 +225,34 @@ export default function WizardPage() {
|
||||
: selectedAdapter;
|
||||
setBotName(defaultName);
|
||||
|
||||
const defaultConfig = adapter
|
||||
? getDefaultValues(adapter.spec.config)
|
||||
: {};
|
||||
|
||||
const bot: Bot = {
|
||||
name: defaultName,
|
||||
description: '',
|
||||
adapter: selectedAdapter,
|
||||
adapter_config: {},
|
||||
adapter_config: defaultConfig,
|
||||
enable: false,
|
||||
};
|
||||
const resp = await httpClient.createBot(bot);
|
||||
setCreatedBotUuid(resp.uuid);
|
||||
toast.success(t('wizard.botCreateSuccess'));
|
||||
|
||||
// Fetch runtime info to get webhook URL(s)
|
||||
try {
|
||||
const botData = await httpClient.getBot(resp.uuid);
|
||||
const runtimeValues = botData.bot.adapter_runtime_values as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
setWebhookUrl((runtimeValues?.webhook_full_url as string) || '');
|
||||
setExtraWebhookUrl(
|
||||
(runtimeValues?.extra_webhook_full_url as string) || '',
|
||||
);
|
||||
} catch {
|
||||
// Non-critical — webhook URL display is optional
|
||||
}
|
||||
|
||||
// Advance to Step 1
|
||||
setCurrentStep(1);
|
||||
} catch (err) {
|
||||
@@ -338,6 +281,20 @@ export default function WizardPage() {
|
||||
enable: true,
|
||||
});
|
||||
setBotSaved(true);
|
||||
|
||||
// Re-fetch runtime info to get updated webhook URL(s)
|
||||
try {
|
||||
const botData = await httpClient.getBot(createdBotUuid);
|
||||
const runtimeValues = botData.bot.adapter_runtime_values as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
setWebhookUrl((runtimeValues?.webhook_full_url as string) || '');
|
||||
setExtraWebhookUrl(
|
||||
(runtimeValues?.extra_webhook_full_url as string) || '',
|
||||
);
|
||||
} catch {
|
||||
// Non-critical
|
||||
}
|
||||
} catch (err) {
|
||||
const apiErr = err as { msg?: string };
|
||||
toast.error(
|
||||
@@ -403,7 +360,6 @@ export default function WizardPage() {
|
||||
use_pipeline_uuid: pipelineResp.uuid,
|
||||
});
|
||||
|
||||
toast.success(t('wizard.createSuccess'));
|
||||
setCurrentStep(3);
|
||||
} catch (err) {
|
||||
const apiErr = err as { msg?: string };
|
||||
@@ -442,11 +398,24 @@ export default function WizardPage() {
|
||||
|
||||
// ---- Skip handler ----
|
||||
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
|
||||
const [isSkipping, setIsSkipping] = useState(false);
|
||||
|
||||
const handleSkipConfirm = useCallback(() => {
|
||||
clearWizardState();
|
||||
const handleSkipConfirm = useCallback(async () => {
|
||||
if (systemInfo.wizard_status === 'none') {
|
||||
setIsSkipping(true);
|
||||
try {
|
||||
await httpClient.updateWizardStatus('skipped');
|
||||
systemInfo.wizard_status = 'skipped';
|
||||
} catch {
|
||||
toast.error(t('wizard.skipSaveError'));
|
||||
setIsSkipping(false);
|
||||
return; // Dialog stays open — user can retry
|
||||
}
|
||||
setIsSkipping(false);
|
||||
}
|
||||
setShowSkipConfirm(false);
|
||||
router.push('/home');
|
||||
}, [router]);
|
||||
}, [router, t]);
|
||||
|
||||
// ---- Render ----
|
||||
|
||||
@@ -563,6 +532,8 @@ export default function WizardPage() {
|
||||
isSavingBot={isSavingBot}
|
||||
botSaved={botSaved}
|
||||
onSaveBot={handleSaveBot}
|
||||
webhookUrl={webhookUrl}
|
||||
extraWebhookUrl={extraWebhookUrl}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
@@ -623,21 +594,31 @@ export default function WizardPage() {
|
||||
)}
|
||||
|
||||
{/* Skip confirmation dialog */}
|
||||
<AlertDialog open={showSkipConfirm} onOpenChange={setShowSkipConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('wizard.skip')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Dialog open={showSkipConfirm} onOpenChange={setShowSkipConfirm}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('wizard.skip')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('wizard.skipConfirmMessage')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={handleSkipConfirm}>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowSkipConfirm(false)}
|
||||
disabled={isSkipping}
|
||||
>
|
||||
{t('wizard.prev')}
|
||||
</Button>
|
||||
<Button onClick={handleSkipConfirm} disabled={isSkipping}>
|
||||
{isSkipping && (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
)}
|
||||
{t('wizard.skipConfirmOk')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -722,6 +703,8 @@ function StepBotConfig({
|
||||
isSavingBot,
|
||||
botSaved,
|
||||
onSaveBot,
|
||||
webhookUrl,
|
||||
extraWebhookUrl,
|
||||
}: {
|
||||
adapterConfigItems: IDynamicFormItemSchema[];
|
||||
adapterConfigValues: Record<string, unknown>;
|
||||
@@ -732,6 +715,8 @@ function StepBotConfig({
|
||||
isSavingBot: boolean;
|
||||
botSaved: boolean;
|
||||
onSaveBot: () => void;
|
||||
webhookUrl: string;
|
||||
extraWebhookUrl: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -787,7 +772,11 @@ function StepBotConfig({
|
||||
itemConfigList={adapterConfigItems}
|
||||
initialValues={adapterConfigValues as Record<string, object>}
|
||||
onSubmit={stableAdapterConfigCb}
|
||||
systemContext={{ is_wizard: true }}
|
||||
systemContext={{
|
||||
is_wizard: true,
|
||||
webhook_url: webhookUrl,
|
||||
extra_webhook_url: extraWebhookUrl,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1037,10 +1026,23 @@ function StepDone() {
|
||||
})),
|
||||
);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
clearWizardState();
|
||||
const [isCompleting, setIsCompleting] = useState(false);
|
||||
|
||||
const handleBack = useCallback(async () => {
|
||||
if (systemInfo.wizard_status === 'none') {
|
||||
setIsCompleting(true);
|
||||
try {
|
||||
await httpClient.updateWizardStatus('completed');
|
||||
systemInfo.wizard_status = 'completed';
|
||||
} catch {
|
||||
toast.error(t('wizard.completeSaveError'));
|
||||
setIsCompleting(false);
|
||||
return; // Don't navigate — let user retry
|
||||
}
|
||||
setIsCompleting(false);
|
||||
}
|
||||
router.push('/home/bots');
|
||||
}, [router]);
|
||||
}, [router, t]);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full min-h-[400px]">
|
||||
@@ -1065,7 +1067,8 @@ function StepDone() {
|
||||
<p className="text-muted-foreground mt-2 text-center max-w-md">
|
||||
{t('wizard.done.description')}
|
||||
</p>
|
||||
<Button className="mt-6" onClick={handleBack}>
|
||||
<Button className="mt-6" onClick={handleBack} disabled={isCompleting}>
|
||||
{isCompleting && <Loader2 className="w-4 h-4 mr-1.5 animate-spin" />}
|
||||
{t('wizard.done.backToWorkbench')}
|
||||
</Button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user