mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-22 18:27:12 +00:00
feat(wizard): rework agent onboarding flow
This commit is contained in:
@@ -20,6 +20,7 @@ export function BotLogListComponent({
|
||||
autoExpandImages = false,
|
||||
hideDetailedLogsLink = false,
|
||||
hideToolbar = false,
|
||||
onMessageReceived,
|
||||
}: {
|
||||
botId: string;
|
||||
/** When true, log entries with images are rendered expanded by default */
|
||||
@@ -28,6 +29,8 @@ export function BotLogListComponent({
|
||||
hideDetailedLogsLink?: boolean;
|
||||
/** When true, hides the entire toolbar (auto-refresh, level filter, detailed logs link) */
|
||||
hideToolbar?: boolean;
|
||||
/** Called after an inbound person/group message appears in the bot log. */
|
||||
onMessageReceived?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -41,6 +44,8 @@ export function BotLogListComponent({
|
||||
]);
|
||||
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||
const onMessageReceivedRef = useRef(onMessageReceived);
|
||||
onMessageReceivedRef.current = onMessageReceived;
|
||||
|
||||
const logLevels = [
|
||||
{ value: 'error', label: 'ERROR' },
|
||||
@@ -108,6 +113,9 @@ export function BotLogListComponent({
|
||||
manager.subscribeLogPush(handleBotLogPush);
|
||||
manager.loadFirstPage().then((response) => {
|
||||
setBotLogList(response.reverse());
|
||||
if (response.some((log) => Boolean(log.message_session_id))) {
|
||||
onMessageReceivedRef.current?.();
|
||||
}
|
||||
});
|
||||
listenScroll();
|
||||
}
|
||||
@@ -138,6 +146,9 @@ export function BotLogListComponent({
|
||||
|
||||
function handleBotLogPush(response: BotLog[]) {
|
||||
setBotLogList(response.reverse());
|
||||
if (response.some((log) => Boolean(log.message_session_id))) {
|
||||
onMessageReceivedRef.current?.();
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = useCallback(
|
||||
|
||||
@@ -363,7 +363,9 @@ export interface WizardProgress {
|
||||
step: number;
|
||||
selected_adapter: string | null;
|
||||
created_bot_uuid: string | null;
|
||||
created_pipeline_uuid?: string | null;
|
||||
bot_saved: boolean;
|
||||
message_received?: boolean;
|
||||
selected_runner: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1046,12 +1046,21 @@ export class BackendClient extends BaseHttpClient {
|
||||
step: number;
|
||||
selected_adapter: string | null;
|
||||
created_bot_uuid: string | null;
|
||||
created_pipeline_uuid?: string | null;
|
||||
bot_saved: boolean;
|
||||
message_received?: boolean;
|
||||
selected_runner: string | null;
|
||||
}): Promise<void> {
|
||||
return this.put('/api/v1/system/wizard/progress', progress);
|
||||
}
|
||||
|
||||
public getWizardRecommendedModel(): Promise<{
|
||||
uuid: string;
|
||||
name: string;
|
||||
}> {
|
||||
return this.get('/api/v1/system/wizard/recommended-model');
|
||||
}
|
||||
|
||||
public getAsyncTasks(params?: {
|
||||
type?: string;
|
||||
kind?: string;
|
||||
|
||||
+267
-163
@@ -8,10 +8,12 @@ import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
Sparkles,
|
||||
PartyPopper,
|
||||
Loader2,
|
||||
X,
|
||||
ExternalLink,
|
||||
Cable,
|
||||
Settings2,
|
||||
Blocks,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
@@ -21,12 +23,7 @@ import {
|
||||
bootstrapWorkspaceSession,
|
||||
initializeSystemInfo,
|
||||
} from '@/app/infra/http';
|
||||
import {
|
||||
Adapter,
|
||||
Bot,
|
||||
Pipeline,
|
||||
WizardProgress,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Adapter, Bot, WizardProgress } from '@/app/infra/entities/api';
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import {
|
||||
PipelineConfigTab,
|
||||
@@ -71,7 +68,7 @@ import {
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TOTAL_STEPS = 4;
|
||||
const TOTAL_STEPS = 3;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main Wizard Page (full-screen, no sidebar)
|
||||
@@ -93,6 +90,9 @@ export default function WizardPage() {
|
||||
);
|
||||
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({});
|
||||
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
|
||||
const [createdPipelineUuid, setCreatedPipelineUuid] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
||||
|
||||
@@ -106,6 +106,10 @@ export default function WizardPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSavingBot, setIsSavingBot] = useState(false);
|
||||
const [botSaved, setBotSaved] = useState(false);
|
||||
const [messageReceived, setMessageReceived] = useState(false);
|
||||
const [aiChoice, setAiChoice] = useState<
|
||||
'external' | 'own-model' | 'more-features' | null
|
||||
>(null);
|
||||
|
||||
// ---- Helper: persist wizard progress to backend (fire-and-forget) ----
|
||||
const saveProgress = useCallback(
|
||||
@@ -114,14 +118,25 @@ export default function WizardPage() {
|
||||
step: overrides.step ?? currentStep,
|
||||
selected_adapter: overrides.selected_adapter ?? selectedAdapter,
|
||||
created_bot_uuid: overrides.created_bot_uuid ?? createdBotUuid,
|
||||
created_pipeline_uuid:
|
||||
overrides.created_pipeline_uuid ?? createdPipelineUuid,
|
||||
bot_saved: overrides.bot_saved ?? botSaved,
|
||||
message_received: overrides.message_received ?? messageReceived,
|
||||
selected_runner: overrides.selected_runner ?? selectedRunner,
|
||||
};
|
||||
httpClient.saveWizardProgress(progress).catch((err) => {
|
||||
console.error('Failed to save wizard progress', err);
|
||||
});
|
||||
},
|
||||
[currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner],
|
||||
[
|
||||
currentStep,
|
||||
selectedAdapter,
|
||||
createdBotUuid,
|
||||
createdPipelineUuid,
|
||||
botSaved,
|
||||
messageReceived,
|
||||
selectedRunner,
|
||||
],
|
||||
);
|
||||
|
||||
// ---- Fetch remote data & restore progress ----
|
||||
@@ -159,7 +174,9 @@ export default function WizardPage() {
|
||||
|
||||
setSelectedAdapter(progress.selected_adapter);
|
||||
setCreatedBotUuid(progress.created_bot_uuid);
|
||||
setCreatedPipelineUuid(progress.created_pipeline_uuid ?? null);
|
||||
setBotSaved(progress.bot_saved ?? false);
|
||||
setMessageReceived(progress.message_received ?? false);
|
||||
setSelectedRunner(progress.selected_runner);
|
||||
|
||||
// Restore bot name from fetched bot data
|
||||
@@ -183,7 +200,9 @@ export default function WizardPage() {
|
||||
step: 0,
|
||||
selected_adapter: null,
|
||||
created_bot_uuid: null,
|
||||
created_pipeline_uuid: null,
|
||||
bot_saved: false,
|
||||
message_received: false,
|
||||
selected_runner: null,
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -211,7 +230,9 @@ export default function WizardPage() {
|
||||
const runnerOptions = useMemo(() => {
|
||||
if (!runnerStage) return [];
|
||||
const runnerField = runnerStage.config.find((c) => c.name === 'runner');
|
||||
return runnerField?.options ?? [];
|
||||
return (runnerField?.options ?? []).filter(
|
||||
(option) => option.name !== 'local-agent',
|
||||
);
|
||||
}, [runnerStage]);
|
||||
|
||||
const selectedRunnerConfigStage: PipelineConfigStage | undefined =
|
||||
@@ -285,13 +306,20 @@ export default function WizardPage() {
|
||||
case 0:
|
||||
return selectedAdapter !== null;
|
||||
case 1:
|
||||
return createdBotUuid !== null && botSaved;
|
||||
return createdBotUuid !== null && botSaved && messageReceived;
|
||||
case 2:
|
||||
return selectedRunner !== null;
|
||||
return aiChoice !== null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner]);
|
||||
}, [
|
||||
currentStep,
|
||||
selectedAdapter,
|
||||
createdBotUuid,
|
||||
botSaved,
|
||||
messageReceived,
|
||||
aiChoice,
|
||||
]);
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (currentStep < TOTAL_STEPS - 1 && canProceed()) {
|
||||
@@ -360,7 +388,9 @@ export default function WizardPage() {
|
||||
step: 1,
|
||||
selected_adapter: selectedAdapter,
|
||||
created_bot_uuid: resp.uuid,
|
||||
created_pipeline_uuid: null,
|
||||
bot_saved: false,
|
||||
message_received: false,
|
||||
selected_runner: null,
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -374,21 +404,61 @@ export default function WizardPage() {
|
||||
}, [selectedAdapter, adapters, t, saveProgress]);
|
||||
|
||||
// ---- Save Bot Config & Enable (Step 1) ----
|
||||
// Updates the bot's adapter config and enables it.
|
||||
// Creates a recommended Local Agent pipeline, binds it, and enables the bot.
|
||||
|
||||
const handleSaveBot = useCallback(async () => {
|
||||
if (!createdBotUuid || !selectedAdapter) return;
|
||||
setIsSavingBot(true);
|
||||
|
||||
let createdPipelineThisAttempt: string | null = null;
|
||||
try {
|
||||
let pipelineUuid = createdPipelineUuid;
|
||||
if (!pipelineUuid) {
|
||||
const recommendedModel = await httpClient.getWizardRecommendedModel();
|
||||
const pipelineResp = await httpClient.createPipeline({
|
||||
name: `${botName} Agent`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
});
|
||||
pipelineUuid = pipelineResp.uuid;
|
||||
createdPipelineThisAttempt = pipelineUuid;
|
||||
const createdPipeline = await httpClient.getPipeline(pipelineUuid);
|
||||
const aiConfig = createdPipeline.pipeline.config.ai as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const localAgentConfig = (aiConfig['local-agent'] ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
await httpClient.updatePipeline(pipelineUuid, {
|
||||
name: `${botName} Agent`,
|
||||
description: botDescription || '',
|
||||
config: {
|
||||
...createdPipeline.pipeline.config,
|
||||
ai: {
|
||||
...aiConfig,
|
||||
runner: { runner: 'local-agent' },
|
||||
'local-agent': {
|
||||
...localAgentConfig,
|
||||
model: { primary: recommendedModel.uuid, fallbacks: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setCreatedPipelineUuid(pipelineUuid);
|
||||
}
|
||||
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: botName,
|
||||
description: botDescription || '',
|
||||
adapter: selectedAdapter,
|
||||
adapter_config: adapterConfig,
|
||||
enable: true,
|
||||
use_pipeline_uuid: pipelineUuid,
|
||||
});
|
||||
setBotSaved(true);
|
||||
setMessageReceived(false);
|
||||
|
||||
// Re-fetch runtime info to get updated webhook URL(s)
|
||||
try {
|
||||
@@ -405,8 +475,19 @@ export default function WizardPage() {
|
||||
}
|
||||
|
||||
// Persist progress
|
||||
saveProgress({ step: 1, bot_saved: true });
|
||||
saveProgress({
|
||||
step: 1,
|
||||
bot_saved: true,
|
||||
message_received: false,
|
||||
created_pipeline_uuid: pipelineUuid,
|
||||
});
|
||||
} catch (err) {
|
||||
if (createdPipelineThisAttempt) {
|
||||
await httpClient
|
||||
.deletePipeline(createdPipelineThisAttempt)
|
||||
.catch(() => {});
|
||||
setCreatedPipelineUuid(null);
|
||||
}
|
||||
const apiErr = err as { msg?: string };
|
||||
toast.error(
|
||||
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
|
||||
@@ -420,60 +501,80 @@ export default function WizardPage() {
|
||||
botName,
|
||||
botDescription,
|
||||
adapterConfig,
|
||||
createdPipelineUuid,
|
||||
t,
|
||||
saveProgress,
|
||||
]);
|
||||
|
||||
// ---- Create Pipeline & Link (Step 2 finish) ----
|
||||
const handleMessageReceived = useCallback(() => {
|
||||
if (messageReceived) return;
|
||||
setMessageReceived(true);
|
||||
saveProgress({ step: 1, message_received: true });
|
||||
}, [messageReceived, saveProgress]);
|
||||
|
||||
const completeWizard = useCallback(async () => {
|
||||
await httpClient.updateWizardStatus('completed');
|
||||
systemInfo.wizard_status = 'completed';
|
||||
systemInfo.wizard_progress = null;
|
||||
}, []);
|
||||
|
||||
// ---- Complete the optional AI Engine step ----
|
||||
|
||||
const handleFinish = useCallback(async () => {
|
||||
if (!selectedRunner || !createdBotUuid) return;
|
||||
if (!aiChoice || !createdBotUuid || !createdPipelineUuid) return;
|
||||
if (aiChoice === 'external' && !selectedRunner) return;
|
||||
setIsSubmitting(true);
|
||||
let externalPipelineUuid: string | null = null;
|
||||
let externalPipelineBound = false;
|
||||
|
||||
try {
|
||||
// 1. Create pipeline (backend fills config from default template)
|
||||
const pipeline: Pipeline = {
|
||||
name: `${botName} Pipeline`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
};
|
||||
const pipelineResp = await httpClient.createPipeline(pipeline);
|
||||
if (aiChoice === 'external' && selectedRunner) {
|
||||
const pipelineResp = await httpClient.createPipeline({
|
||||
name: `${botName} External Agent`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
});
|
||||
externalPipelineUuid = pipelineResp.uuid;
|
||||
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
|
||||
const fullConfig = createdPipeline.pipeline.config;
|
||||
await httpClient.updatePipeline(pipelineResp.uuid, {
|
||||
name: `${botName} External Agent`,
|
||||
description: botDescription || '',
|
||||
config: {
|
||||
...fullConfig,
|
||||
ai: {
|
||||
...fullConfig.ai,
|
||||
runner: { runner: selectedRunner },
|
||||
[selectedRunner]: runnerConfig,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Fetch the created pipeline to get the full default config
|
||||
// (includes trigger, safety, ai, output sections).
|
||||
// Then merge only the AI section with the wizard's runner config.
|
||||
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
|
||||
const fullConfig = createdPipeline.pipeline.config;
|
||||
const botData = await httpClient.getBot(createdBotUuid);
|
||||
const existingBot = botData.bot;
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: existingBot.name,
|
||||
description: existingBot.description,
|
||||
adapter: existingBot.adapter,
|
||||
adapter_config: existingBot.adapter_config,
|
||||
enable: existingBot.enable,
|
||||
use_pipeline_uuid: pipelineResp.uuid,
|
||||
});
|
||||
externalPipelineBound = true;
|
||||
}
|
||||
|
||||
const mergedConfig = {
|
||||
...fullConfig,
|
||||
ai: {
|
||||
...fullConfig.ai,
|
||||
runner: { runner: selectedRunner },
|
||||
[selectedRunner]: runnerConfig,
|
||||
},
|
||||
};
|
||||
|
||||
await httpClient.updatePipeline(pipelineResp.uuid, {
|
||||
name: `${botName} Pipeline`,
|
||||
description: botDescription || '',
|
||||
config: mergedConfig,
|
||||
});
|
||||
|
||||
// 3. Link pipeline to the bot created in Step 1
|
||||
const botData = await httpClient.getBot(createdBotUuid);
|
||||
const existingBot = botData.bot;
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: existingBot.name,
|
||||
description: existingBot.description,
|
||||
adapter: existingBot.adapter,
|
||||
adapter_config: existingBot.adapter_config,
|
||||
enable: existingBot.enable,
|
||||
use_pipeline_uuid: pipelineResp.uuid,
|
||||
});
|
||||
|
||||
setCurrentStep(3);
|
||||
await completeWizard();
|
||||
if (aiChoice === 'own-model') {
|
||||
navigate(`/home/pipelines?id=${createdPipelineUuid}`, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
navigate('/home', { replace: true });
|
||||
}
|
||||
} catch (err) {
|
||||
if (externalPipelineUuid && !externalPipelineBound) {
|
||||
await httpClient.deletePipeline(externalPipelineUuid).catch(() => {});
|
||||
}
|
||||
const apiErr = err as { msg?: string };
|
||||
toast.error(
|
||||
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
|
||||
@@ -484,9 +585,13 @@ export default function WizardPage() {
|
||||
}, [
|
||||
selectedRunner,
|
||||
createdBotUuid,
|
||||
createdPipelineUuid,
|
||||
aiChoice,
|
||||
botName,
|
||||
botDescription,
|
||||
runnerConfig,
|
||||
completeWizard,
|
||||
navigate,
|
||||
t,
|
||||
]);
|
||||
|
||||
@@ -524,7 +629,9 @@ export default function WizardPage() {
|
||||
step: 0,
|
||||
selected_adapter: null,
|
||||
created_bot_uuid: null,
|
||||
created_pipeline_uuid: null,
|
||||
bot_saved: false,
|
||||
message_received: false,
|
||||
selected_runner: null,
|
||||
});
|
||||
systemInfo.wizard_progress = null;
|
||||
@@ -552,7 +659,6 @@ export default function WizardPage() {
|
||||
t('wizard.step.platform'),
|
||||
t('wizard.step.botConfig'),
|
||||
t('wizard.step.aiEngine'),
|
||||
t('wizard.step.done'),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -567,7 +673,7 @@ export default function WizardPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSelector />
|
||||
{currentStep < 3 && (
|
||||
{currentStep < TOTAL_STEPS && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -652,6 +758,8 @@ export default function WizardPage() {
|
||||
createdBotUuid={createdBotUuid}
|
||||
isSavingBot={isSavingBot}
|
||||
botSaved={botSaved}
|
||||
messageReceived={messageReceived}
|
||||
onMessageReceived={handleMessageReceived}
|
||||
onSaveBot={handleSaveBot}
|
||||
webhookUrl={webhookUrl}
|
||||
extraWebhookUrl={extraWebhookUrl}
|
||||
@@ -660,6 +768,8 @@ export default function WizardPage() {
|
||||
{currentStep === 2 && (
|
||||
<StepAIEngine
|
||||
runnerOptions={runnerOptions}
|
||||
choice={aiChoice}
|
||||
onChoiceChange={setAiChoice}
|
||||
selected={selectedRunner}
|
||||
onSelect={handleSelectRunner}
|
||||
isLocalAccount={isLocalAccount}
|
||||
@@ -669,11 +779,10 @@ export default function WizardPage() {
|
||||
onRunnerConfigChange={setRunnerConfig}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && <StepDone />}
|
||||
</div>
|
||||
|
||||
{/* Footer navigation */}
|
||||
{currentStep < 3 && (
|
||||
{currentStep < TOTAL_STEPS && (
|
||||
<div className="shrink-0 flex justify-between items-center px-4 sm:px-6 py-3 sm:py-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -703,12 +812,20 @@ export default function WizardPage() {
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleFinish}
|
||||
disabled={!canProceed() || isSubmitting}
|
||||
disabled={
|
||||
!canProceed() ||
|
||||
isSubmitting ||
|
||||
(aiChoice === 'external' && !selectedRunner)
|
||||
}
|
||||
>
|
||||
{isSubmitting && (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
)}
|
||||
{t('wizard.finish')}
|
||||
{aiChoice === 'external'
|
||||
? t('wizard.aiEngine.createExternal')
|
||||
: aiChoice === 'own-model'
|
||||
? t('wizard.aiEngine.configurePipeline')
|
||||
: t('wizard.aiEngine.openWorkbench')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -858,6 +975,8 @@ function StepBotConfig({
|
||||
createdBotUuid,
|
||||
isSavingBot,
|
||||
botSaved,
|
||||
messageReceived,
|
||||
onMessageReceived,
|
||||
onSaveBot,
|
||||
webhookUrl,
|
||||
extraWebhookUrl,
|
||||
@@ -870,6 +989,8 @@ function StepBotConfig({
|
||||
createdBotUuid: string | null;
|
||||
isSavingBot: boolean;
|
||||
botSaved: boolean;
|
||||
messageReceived: boolean;
|
||||
onMessageReceived: () => void;
|
||||
onSaveBot: () => void;
|
||||
webhookUrl: string;
|
||||
extraWebhookUrl: string;
|
||||
@@ -962,14 +1083,16 @@ function StepBotConfig({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Bot saved indicator */}
|
||||
{/* Bot and inbound-message verification status */}
|
||||
{botSaved && (
|
||||
<div className="flex items-center gap-2 px-4 py-3 rounded-lg border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30">
|
||||
<div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center shrink-0">
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="text-sm text-green-700 dark:text-green-300">
|
||||
{t('wizard.botConfig.botSaved')}
|
||||
{messageReceived
|
||||
? t('wizard.botConfig.messageReceived')
|
||||
: t('wizard.botConfig.waitingForMessage')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -989,6 +1112,7 @@ function StepBotConfig({
|
||||
botId={createdBotUuid}
|
||||
autoExpandImages
|
||||
hideToolbar
|
||||
onMessageReceived={onMessageReceived}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1004,6 +1128,8 @@ function StepBotConfig({
|
||||
|
||||
function StepAIEngine({
|
||||
runnerOptions,
|
||||
choice,
|
||||
onChoiceChange,
|
||||
selected,
|
||||
onSelect,
|
||||
isLocalAccount,
|
||||
@@ -1013,6 +1139,10 @@ function StepAIEngine({
|
||||
onRunnerConfigChange,
|
||||
}: {
|
||||
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
|
||||
choice: 'external' | 'own-model' | 'more-features' | null;
|
||||
onChoiceChange: (
|
||||
choice: 'external' | 'own-model' | 'more-features' | null,
|
||||
) => void;
|
||||
selected: string | null;
|
||||
onSelect: (name: string) => void;
|
||||
isLocalAccount: boolean;
|
||||
@@ -1036,6 +1166,63 @@ function StepAIEngine({
|
||||
return r ? extractI18nObject(r.label) : (selected ?? '');
|
||||
}, [runnerOptions, selected]);
|
||||
|
||||
const choices = [
|
||||
{
|
||||
id: 'external' as const,
|
||||
icon: Cable,
|
||||
title: t('wizard.aiEngine.externalTitle'),
|
||||
description: t('wizard.aiEngine.externalDescription'),
|
||||
},
|
||||
{
|
||||
id: 'own-model' as const,
|
||||
icon: Settings2,
|
||||
title: t('wizard.aiEngine.ownModelTitle'),
|
||||
description: t('wizard.aiEngine.ownModelDescription'),
|
||||
},
|
||||
{
|
||||
id: 'more-features' as const,
|
||||
icon: Blocks,
|
||||
title: t('wizard.aiEngine.moreFeaturesTitle'),
|
||||
description: t('wizard.aiEngine.moreFeaturesDescription'),
|
||||
},
|
||||
];
|
||||
|
||||
if (choice !== 'external') {
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.aiEngine.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.aiEngine.optionalDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{choices.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'cursor-pointer transition-all hover:border-primary/50',
|
||||
choice === item.id && 'ring-2 ring-primary',
|
||||
)}
|
||||
onClick={() => onChoiceChange(item.id)}
|
||||
>
|
||||
<CardHeader>
|
||||
<Icon className="size-6 text-primary" />
|
||||
<CardTitle className="text-base">{item.title}</CardTitle>
|
||||
<CardDescription>{item.description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Before any runner is selected: centered grid layout
|
||||
if (!selected) {
|
||||
return (
|
||||
@@ -1045,9 +1232,13 @@ function StepAIEngine({
|
||||
{t('wizard.aiEngine.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.aiEngine.description')}
|
||||
{t('wizard.aiEngine.runnerDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onChoiceChange(null)}>
|
||||
<ArrowLeft className="size-4 mr-1.5" />
|
||||
{t('wizard.aiEngine.backToChoices')}
|
||||
</Button>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{runnerOptions.map((opt) => (
|
||||
<Card
|
||||
@@ -1084,6 +1275,16 @@ function StepAIEngine({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="self-start mb-3"
|
||||
onClick={() => onChoiceChange(null)}
|
||||
>
|
||||
<ArrowLeft className="size-4 mr-1.5" />
|
||||
{t('wizard.aiEngine.backToChoices')}
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:justify-center gap-6 lg:flex-1 lg:min-h-0 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
{/* Left: runner list */}
|
||||
<div className="w-full lg:w-[280px] shrink-0 lg:overflow-y-auto lg:pr-3">
|
||||
@@ -1179,100 +1380,3 @@ function StepAIEngine({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 3: Done
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function StepDone() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [particles] = useState(() =>
|
||||
Array.from({ length: 30 }, (_, i) => ({
|
||||
id: i,
|
||||
left: Math.random() * 100,
|
||||
delay: Math.random() * 2,
|
||||
duration: 2 + Math.random() * 2,
|
||||
size: 4 + Math.random() * 6,
|
||||
color: [
|
||||
'bg-purple-400',
|
||||
'bg-pink-400',
|
||||
'bg-orange-400',
|
||||
'bg-blue-400',
|
||||
'bg-green-400',
|
||||
'bg-yellow-400',
|
||||
][Math.floor(Math.random() * 6)],
|
||||
})),
|
||||
);
|
||||
|
||||
const [isCompleting, setIsCompleting] = useState(false);
|
||||
|
||||
const handleBack = useCallback(async () => {
|
||||
setIsCompleting(true);
|
||||
try {
|
||||
if (systemInfo.wizard_status === 'none') {
|
||||
await httpClient.updateWizardStatus('completed');
|
||||
systemInfo.wizard_status = 'completed';
|
||||
}
|
||||
// Always clear persisted progress so re-entering starts fresh
|
||||
await httpClient.saveWizardProgress({
|
||||
step: 0,
|
||||
selected_adapter: null,
|
||||
created_bot_uuid: null,
|
||||
bot_saved: false,
|
||||
selected_runner: null,
|
||||
});
|
||||
systemInfo.wizard_progress = null;
|
||||
} catch {
|
||||
toast.error(t('wizard.completeSaveError'));
|
||||
setIsCompleting(false);
|
||||
return;
|
||||
}
|
||||
setIsCompleting(false);
|
||||
navigate('/home/bots');
|
||||
}, [navigate, t]);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full min-h-[400px]">
|
||||
{/* Confetti particles */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
{particles.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={cn('absolute rounded-full opacity-0', p.color)}
|
||||
style={{
|
||||
left: `${p.left}%`,
|
||||
width: p.size,
|
||||
height: p.size,
|
||||
animation: `wizardConfetti ${p.duration}s ease-out ${p.delay}s forwards`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<PartyPopper className="w-16 h-16 text-primary mb-4" />
|
||||
<h2 className="text-2xl font-bold">{t('wizard.done.title')}</h2>
|
||||
<p className="text-muted-foreground mt-2 text-center max-w-md">
|
||||
{t('wizard.done.description')}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<style>{`
|
||||
@keyframes wizardConfetti {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-20vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user