feat(wizard): start bot setup from scenarios

This commit is contained in:
huanghuoguoguo
2026-07-11 11:45:39 +08:00
parent a2b30957b6
commit 24322836ce
10 changed files with 963 additions and 208 deletions
+1
View File
@@ -445,6 +445,7 @@ export interface SystemLimitation {
export interface WizardProgress {
step: number;
selected_scenario?: string | null;
selected_adapter: string | null;
created_bot_uuid: string | null;
bot_saved: boolean;
+1
View File
@@ -1072,6 +1072,7 @@ export class BackendClient extends BaseHttpClient {
public saveWizardProgress(progress: {
step: number;
selected_scenario?: string | null;
selected_adapter: string | null;
created_bot_uuid: string | null;
bot_saved: boolean;
+442 -191
View File
@@ -12,6 +12,11 @@ import {
Sparkles,
PartyPopper,
Loader2,
MessageSquare,
ShieldCheck,
UserCheck,
UserMinus,
UserPlus,
X,
ExternalLink,
} from 'lucide-react';
@@ -75,6 +80,73 @@ import {
const TOTAL_STEPS = 4;
type WizardScenarioId =
| 'message_reply'
| 'welcome_members'
| 'handle_departures'
| 'review_friend_requests'
| 'handle_moderation';
const WIZARD_SCENARIOS = [
{
id: 'message_reply' as const,
eventType: 'message.received',
processorKind: 'pipeline' as const,
labelKey: 'wizard.scenario.messageReply',
descriptionKey: 'wizard.scenario.messageReplyDescription',
icon: MessageSquare,
emoji: '💬',
},
{
id: 'welcome_members' as const,
eventType: 'group.member_joined',
processorKind: 'agent' as const,
labelKey: 'wizard.scenario.welcomeMembers',
descriptionKey: 'wizard.scenario.welcomeMembersDescription',
icon: UserPlus,
emoji: '👋',
},
{
id: 'handle_departures' as const,
eventType: 'group.member_left',
processorKind: 'agent' as const,
labelKey: 'wizard.scenario.handleDepartures',
descriptionKey: 'wizard.scenario.handleDeparturesDescription',
icon: UserMinus,
emoji: '👤',
},
{
id: 'review_friend_requests' as const,
eventType: 'friend.request_received',
processorKind: 'agent' as const,
labelKey: 'wizard.scenario.reviewFriendRequests',
descriptionKey: 'wizard.scenario.reviewFriendRequestsDescription',
icon: UserCheck,
emoji: '✅',
},
{
id: 'handle_moderation' as const,
eventType: 'group.member_banned',
processorKind: 'agent' as const,
labelKey: 'wizard.scenario.handleModeration',
descriptionKey: 'wizard.scenario.handleModerationDescription',
icon: ShieldCheck,
emoji: '🛡️',
},
];
function adapterSupportsScenario(
adapter: Adapter,
scenarioId: WizardScenarioId,
) {
const scenario = WIZARD_SCENARIOS.find((item) => item.id === scenarioId);
if (!scenario) return false;
const supportedEvents = adapter.spec.supported_events?.length
? adapter.spec.supported_events
: ['message.received'];
return supportedEvents.includes(scenario.eventType);
}
// ---------------------------------------------------------------------------
// Main Wizard Page (full-screen, no sidebar)
// ---------------------------------------------------------------------------
@@ -85,6 +157,8 @@ export default function WizardPage() {
// ---- Wizard state ----
const [currentStep, setCurrentStep] = useState(0);
const [selectedScenario, setSelectedScenario] =
useState<WizardScenarioId | null>(null);
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
const [botName, setBotName] = useState('');
@@ -113,16 +187,36 @@ export default function WizardPage() {
(overrides: Partial<WizardProgress> = {}) => {
const progress: WizardProgress = {
step: overrides.step ?? currentStep,
selected_adapter: overrides.selected_adapter ?? selectedAdapter,
created_bot_uuid: overrides.created_bot_uuid ?? createdBotUuid,
selected_scenario:
overrides.selected_scenario !== undefined
? overrides.selected_scenario
: selectedScenario,
selected_adapter:
overrides.selected_adapter !== undefined
? overrides.selected_adapter
: selectedAdapter,
created_bot_uuid:
overrides.created_bot_uuid !== undefined
? overrides.created_bot_uuid
: createdBotUuid,
bot_saved: overrides.bot_saved ?? botSaved,
selected_runner: overrides.selected_runner ?? selectedRunner,
selected_runner:
overrides.selected_runner !== undefined
? overrides.selected_runner
: selectedRunner,
};
httpClient.saveWizardProgress(progress).catch((err) => {
console.error('Failed to save wizard progress', err);
});
},
[currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner],
[
currentStep,
selectedScenario,
selectedAdapter,
createdBotUuid,
botSaved,
selectedRunner,
],
);
// ---- Fetch remote data & restore progress ----
@@ -151,6 +245,10 @@ export default function WizardPage() {
if (cancelled) return;
setSelectedAdapter(progress.selected_adapter);
setSelectedScenario(
(progress.selected_scenario as WizardScenarioId | null) ??
'message_reply',
);
setCreatedBotUuid(progress.created_bot_uuid);
setBotSaved(progress.bot_saved ?? false);
setSelectedRunner(progress.selected_runner);
@@ -167,13 +265,14 @@ export default function WizardPage() {
(runtimeValues?.extra_webhook_full_url as string) || '',
);
// Restore step (cap at step 2 — step 3 means done)
setCurrentStep(Math.min(progress.step, 2));
// Step 3 is resumable so a refresh cannot create a duplicate processor.
setCurrentStep(Math.min(progress.step, 3));
} catch {
// Bot no longer exists — clear stale progress and start fresh
httpClient
.saveWizardProgress({
step: 0,
selected_scenario: null,
selected_adapter: null,
created_bot_uuid: null,
bot_saved: false,
@@ -213,6 +312,11 @@ export default function WizardPage() {
return aiConfigTab.stages.find((s) => s.name === selectedRunner);
}, [selectedRunner, aiConfigTab]);
const selectedScenarioDefinition = useMemo(
() => WIZARD_SCENARIOS.find((item) => item.id === selectedScenario),
[selectedScenario],
);
// Adapter spec config for the selected adapter
const selectedAdapterConfig: IDynamicFormItemSchema[] = useMemo(() => {
const adapter = adapters.find((a) => a.name === selectedAdapter);
@@ -270,7 +374,7 @@ export default function WizardPage() {
const canProceed = useCallback((): boolean => {
switch (currentStep) {
case 0:
return selectedAdapter !== null;
return selectedScenario !== null && selectedAdapter !== null;
case 1:
return createdBotUuid !== null && botSaved;
case 2:
@@ -278,7 +382,32 @@ export default function WizardPage() {
default:
return false;
}
}, [currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner]);
}, [
currentStep,
selectedScenario,
selectedAdapter,
createdBotUuid,
botSaved,
selectedRunner,
]);
const handleSelectScenario = useCallback(
(scenarioId: WizardScenarioId) => {
const adapter = adapters.find((item) => item.name === selectedAdapter);
const nextAdapter =
adapter && adapterSupportsScenario(adapter, scenarioId)
? selectedAdapter
: null;
setSelectedScenario(scenarioId);
setSelectedAdapter(nextAdapter);
saveProgress({
step: 0,
selected_scenario: scenarioId,
selected_adapter: nextAdapter,
});
},
[adapters, selectedAdapter, saveProgress],
);
const goNext = useCallback(() => {
if (currentStep < TOTAL_STEPS - 1 && canProceed()) {
@@ -345,6 +474,7 @@ export default function WizardPage() {
// Persist progress
saveProgress({
step: 1,
selected_scenario: selectedScenario,
selected_adapter: selectedAdapter,
created_bot_uuid: resp.uuid,
bot_saved: false,
@@ -358,7 +488,7 @@ export default function WizardPage() {
} finally {
setIsCreatingBot(false);
}
}, [selectedAdapter, adapters, t, saveProgress]);
}, [selectedScenario, selectedAdapter, adapters, t, saveProgress]);
// ---- Save Bot Config & Enable (Step 1) ----
// Updates the bot's adapter config and enables it.
@@ -414,62 +544,75 @@ export default function WizardPage() {
// ---- Create Pipeline & Link (Step 2 finish) ----
const handleFinish = useCallback(async () => {
if (!selectedRunner || !createdBotUuid) return;
if (!selectedRunner || !createdBotUuid || !selectedScenarioDefinition)
return;
setIsSubmitting(true);
let processorUuid = '';
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);
let targetType: 'agent' | 'pipeline';
// 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 as unknown as Record<
string,
unknown
>;
const fullAiConfig =
fullConfig.ai && typeof fullConfig.ai === 'object'
? (fullConfig.ai as Record<string, unknown>)
: {};
const existingRunner =
fullAiConfig.runner && typeof fullAiConfig.runner === 'object'
? (fullAiConfig.runner as Record<string, unknown>)
: {};
const existingRunnerConfigs =
fullAiConfig.runner_config &&
typeof fullAiConfig.runner_config === 'object'
? (fullAiConfig.runner_config as Record<string, unknown>)
: {};
if (selectedScenarioDefinition.processorKind === 'pipeline') {
const pipeline: Pipeline = {
name: `${botName} Pipeline`,
description: botDescription || '',
config: {},
};
const pipelineResp = await httpClient.createPipeline(pipeline);
processorUuid = pipelineResp.uuid;
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
const fullConfig = createdPipeline.pipeline.config as unknown as Record<
string,
unknown
>;
const fullAiConfig =
fullConfig.ai && typeof fullConfig.ai === 'object'
? (fullConfig.ai as Record<string, unknown>)
: {};
const existingRunner =
fullAiConfig.runner && typeof fullAiConfig.runner === 'object'
? (fullAiConfig.runner as Record<string, unknown>)
: {};
const existingRunnerConfigs =
fullAiConfig.runner_config &&
typeof fullAiConfig.runner_config === 'object'
? (fullAiConfig.runner_config as Record<string, unknown>)
: {};
const mergedConfig = {
...fullConfig,
ai: {
...fullAiConfig,
runner: {
...existingRunner,
id: selectedRunner,
await httpClient.updatePipeline(pipelineResp.uuid, {
name: `${botName} Pipeline`,
description: botDescription || '',
config: {
...fullConfig,
ai: {
...fullAiConfig,
runner: { ...existingRunner, id: selectedRunner },
runner_config: {
...existingRunnerConfigs,
[selectedRunner]: runnerConfig,
},
},
},
runner_config: {
...existingRunnerConfigs,
[selectedRunner]: runnerConfig,
});
targetType = 'pipeline';
} else {
const agentResp = await httpClient.createAgent({
kind: 'agent',
name: `${botName} - ${t(selectedScenarioDefinition.labelKey)}`,
description: botDescription || '',
emoji: selectedScenarioDefinition.emoji,
component_ref: selectedRunner,
config: {
runner: { id: selectedRunner, 'expire-time': 0 },
runner_config: { [selectedRunner]: runnerConfig },
},
},
};
enabled: true,
supported_event_patterns: [selectedScenarioDefinition.eventType],
});
processorUuid = agentResp.uuid;
targetType = 'agent';
}
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, {
@@ -480,9 +623,9 @@ export default function WizardPage() {
enable: existingBot.enable,
event_bindings: [
{
event_pattern: 'message.received',
target_type: 'pipeline',
target_uuid: pipelineResp.uuid,
event_pattern: selectedScenarioDefinition.eventType,
target_type: targetType,
target_uuid: processorUuid,
filters: [],
priority: 0,
enabled: true,
@@ -492,7 +635,15 @@ export default function WizardPage() {
});
setCurrentStep(3);
saveProgress({ step: 3 });
} catch (err) {
if (processorUuid) {
try {
await httpClient.deleteAgent(processorUuid);
} catch (rollbackError) {
console.warn('Failed to roll back wizard processor', rollbackError);
}
}
const apiErr = err as { msg?: string };
toast.error(
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
@@ -503,10 +654,12 @@ export default function WizardPage() {
}, [
selectedRunner,
createdBotUuid,
selectedScenarioDefinition,
botName,
botDescription,
runnerConfig,
t,
saveProgress,
]);
// ---- Space auth redirect ----
@@ -541,6 +694,7 @@ export default function WizardPage() {
// Always clear persisted progress so re-entering starts fresh
await httpClient.saveWizardProgress({
step: 0,
selected_scenario: null,
selected_adapter: null,
created_bot_uuid: null,
bot_saved: false,
@@ -568,7 +722,7 @@ export default function WizardPage() {
}
const stepLabels = [
t('wizard.step.platform'),
t('wizard.step.scenarioChannel'),
t('wizard.step.botConfig'),
t('wizard.step.aiEngine'),
t('wizard.step.done'),
@@ -657,6 +811,8 @@ export default function WizardPage() {
{currentStep === 0 && (
<StepPlatform
adapters={adapters}
selectedScenario={selectedScenario}
onSelectScenario={handleSelectScenario}
selected={selectedAdapter}
onSelect={setSelectedAdapter}
/>
@@ -769,10 +925,14 @@ export default function WizardPage() {
function StepPlatform({
adapters,
selectedScenario,
onSelectScenario,
selected,
onSelect,
}: {
adapters: Adapter[];
selectedScenario: WizardScenarioId | null;
onSelectScenario: (scenarioId: WizardScenarioId) => void;
selected: string | null;
onSelect: (name: string) => void;
}) {
@@ -780,12 +940,26 @@ function StepPlatform({
const [showLegacy, setShowLegacy] = useState(false);
const activeAdapters = useMemo(
() => adapters.filter((a) => !a.spec.legacy),
[adapters],
() =>
selectedScenario
? adapters.filter(
(adapter) =>
!adapter.spec.legacy &&
adapterSupportsScenario(adapter, selectedScenario),
)
: [],
[adapters, selectedScenario],
);
const legacyAdapters = useMemo(
() => adapters.filter((a) => a.spec.legacy),
[adapters],
() =>
selectedScenario
? adapters.filter(
(adapter) =>
adapter.spec.legacy &&
adapterSupportsScenario(adapter, selectedScenario),
)
: [],
[adapters, selectedScenario],
);
const groupedAdapters = useMemo(() => {
@@ -797,139 +971,215 @@ function StepPlatform({
}, [activeAdapters]);
return (
<div className="space-y-6 max-w-4xl mx-auto">
<div className="text-center">
<h2 className="text-xl font-semibold">{t('wizard.platform.title')}</h2>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.platform.description')}
</p>
</div>
{groupedAdapters.map((group) => (
<div key={group.categoryId ?? 'uncategorized'} className="space-y-3">
{group.categoryId && (
<h3 className="text-sm font-medium text-muted-foreground">
{getCategoryLabel(t, group.categoryId)}
</h3>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{group.items.map((adapter) => (
<Card
key={adapter.name}
<div className="mx-auto max-w-5xl space-y-8">
<section className="space-y-3">
<div>
<h2 className="text-xl font-semibold">
{t('wizard.scenario.title')}
</h2>
<p className="mt-1 text-sm text-muted-foreground">
{t('wizard.scenario.description')}
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{WIZARD_SCENARIOS.map((scenario) => {
const Icon = scenario.icon;
const isSelected = selectedScenario === scenario.id;
return (
<button
key={scenario.id}
type="button"
onClick={() => onSelectScenario(scenario.id)}
className={cn(
'cursor-pointer transition-all hover:shadow-md',
selected === adapter.name
? 'ring-2 ring-primary shadow-md'
: 'hover:border-primary/50',
'rounded-md border bg-card p-3 text-left transition-colors',
isSelected
? 'border-primary ring-2 ring-primary/20'
: 'hover:border-primary/60',
)}
onClick={() => onSelect(adapter.name)}
>
<CardHeader className="flex flex-row items-center gap-3 pb-2">
<img
src={httpClient.getAdapterIconURL(adapter.name)}
alt=""
className="w-10 h-10 rounded-lg shrink-0"
/>
<div className="min-w-0">
<CardTitle className="text-base truncate">
{extractI18nObject(adapter.label)}
</CardTitle>
</div>
{selected === adapter.name && (
<div className="ml-auto shrink-0">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<Check className="w-3 h-3 text-primary-foreground" />
</div>
</div>
)}
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground line-clamp-2">
{extractI18nObject(adapter.description)}
</p>
{(() => {
const docUrl = getAdapterDocUrl(
adapter.spec.help_links,
i18n.language,
);
return docUrl ? (
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center text-xs text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="mr-1 h-3 w-3" />
{t('bots.viewAdapterDocs')}
</a>
) : null;
})()}
</CardContent>
</Card>
))}
</div>
</div>
))}
{legacyAdapters.length > 0 && (
<div className="border-t pt-4 space-y-3">
<button
type="button"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"
onClick={() => setShowLegacy((v) => !v)}
>
{showLegacy ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
{t('bots.legacyAdapters')}
<span className="rounded bg-muted px-1.5 py-0.5 text-xs">
{legacyAdapters.length}
</span>
</button>
{showLegacy && (
<>
<p className="text-xs text-muted-foreground">
{t('bots.legacyAdaptersHint')}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 opacity-60">
{legacyAdapters.map((adapter) => (
<Card
key={adapter.name}
<div className="flex items-start gap-3">
<span
className={cn(
'cursor-pointer transition-all hover:shadow-md',
selected === adapter.name
? 'ring-2 ring-primary shadow-md'
: 'hover:border-primary/50',
'flex h-8 w-8 shrink-0 items-center justify-center rounded-md',
isSelected
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground',
)}
onClick={() => onSelect(adapter.name)}
>
<CardHeader className="flex flex-row items-center gap-3 pb-2">
<img
src={httpClient.getAdapterIconURL(adapter.name)}
alt=""
className="w-10 h-10 rounded-lg shrink-0 grayscale"
/>
<div className="min-w-0">
<CardTitle className="text-base truncate">
{extractI18nObject(adapter.label)}
</CardTitle>
</div>
{selected === adapter.name && (
<div className="ml-auto shrink-0">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<Check className="w-3 h-3 text-primary-foreground" />
</div>
</div>
)}
</CardHeader>
</Card>
))}
</div>
</>
)}
<Icon className="h-4 w-4" />
</span>
<span className="min-w-0 flex-1">
<span className="flex items-center justify-between gap-2">
<span className="font-medium">
{t(scenario.labelKey)}
</span>
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
{t(
scenario.processorKind === 'pipeline'
? 'wizard.scenario.pipelineBadge'
: 'wizard.scenario.agentBadge',
)}
</span>
</span>
<span className="mt-1 block text-sm leading-snug text-muted-foreground">
{t(scenario.descriptionKey)}
</span>
</span>
</div>
</button>
);
})}
</div>
)}
</section>
<section className="space-y-5 border-t pt-6">
<div className="text-center">
<h2 className="text-xl font-semibold">
{t('wizard.platform.title')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.platform.description')}
</p>
</div>
{!selectedScenario && (
<div className="rounded-md border border-dashed p-5 text-center text-sm text-muted-foreground">
{t('wizard.platform.chooseScenarioFirst')}
</div>
)}
{selectedScenario &&
activeAdapters.length === 0 &&
legacyAdapters.length === 0 && (
<div className="rounded-md border border-dashed p-5 text-center text-sm text-muted-foreground">
{t('wizard.platform.noCompatiblePlatforms')}
</div>
)}
{groupedAdapters.map((group) => (
<div key={group.categoryId ?? 'uncategorized'} className="space-y-3">
{group.categoryId && (
<h3 className="text-sm font-medium text-muted-foreground">
{getCategoryLabel(t, group.categoryId)}
</h3>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{group.items.map((adapter) => (
<Card
key={adapter.name}
className={cn(
'cursor-pointer transition-all hover:shadow-md',
selected === adapter.name
? 'ring-2 ring-primary shadow-md'
: 'hover:border-primary/50',
)}
onClick={() => onSelect(adapter.name)}
>
<CardHeader className="flex flex-row items-center gap-3 pb-2">
<img
src={httpClient.getAdapterIconURL(adapter.name)}
alt=""
className="w-10 h-10 rounded-lg shrink-0"
/>
<div className="min-w-0">
<CardTitle className="text-base truncate">
{extractI18nObject(adapter.label)}
</CardTitle>
</div>
{selected === adapter.name && (
<div className="ml-auto shrink-0">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<Check className="w-3 h-3 text-primary-foreground" />
</div>
</div>
)}
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground line-clamp-2">
{extractI18nObject(adapter.description)}
</p>
{(() => {
const docUrl = getAdapterDocUrl(
adapter.spec.help_links,
i18n.language,
);
return docUrl ? (
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center text-xs text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="mr-1 h-3 w-3" />
{t('bots.viewAdapterDocs')}
</a>
) : null;
})()}
</CardContent>
</Card>
))}
</div>
</div>
))}
{legacyAdapters.length > 0 && (
<div className="border-t pt-4 space-y-3">
<button
type="button"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"
onClick={() => setShowLegacy((v) => !v)}
>
{showLegacy ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
{t('bots.legacyAdapters')}
<span className="rounded bg-muted px-1.5 py-0.5 text-xs">
{legacyAdapters.length}
</span>
</button>
{showLegacy && (
<>
<p className="text-xs text-muted-foreground">
{t('bots.legacyAdaptersHint')}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 opacity-60">
{legacyAdapters.map((adapter) => (
<Card
key={adapter.name}
className={cn(
'cursor-pointer transition-all hover:shadow-md',
selected === adapter.name
? 'ring-2 ring-primary shadow-md'
: 'hover:border-primary/50',
)}
onClick={() => onSelect(adapter.name)}
>
<CardHeader className="flex flex-row items-center gap-3 pb-2">
<img
src={httpClient.getAdapterIconURL(adapter.name)}
alt=""
className="w-10 h-10 rounded-lg shrink-0 grayscale"
/>
<div className="min-w-0">
<CardTitle className="text-base truncate">
{extractI18nObject(adapter.label)}
</CardTitle>
</div>
{selected === adapter.name && (
<div className="ml-auto shrink-0">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<Check className="w-3 h-3 text-primary-foreground" />
</div>
</div>
)}
</CardHeader>
</Card>
))}
</div>
</>
)}
</div>
)}
</section>
</div>
);
}
@@ -1308,6 +1558,7 @@ function StepDone() {
// Always clear persisted progress so re-entering starts fresh
await httpClient.saveWizardProgress({
step: 0,
selected_scenario: null,
selected_adapter: null,
created_bot_uuid: null,
bot_saved: false,
+30 -4
View File
@@ -1913,7 +1913,7 @@ const enUS = {
next: 'Next',
finish: 'Create & Deploy',
confirmCreateBot: 'Confirm, Create Bot',
createSuccess: 'Pipeline created and linked to bot successfully!',
createSuccess: 'Processor created and linked to the bot successfully!',
botCreateSuccess: 'Bot created successfully!',
botSaveSuccess: 'Bot configuration saved and enabled!',
createError: 'Failed to create resources',
@@ -1922,13 +1922,39 @@ const enUS = {
completeSaveError: 'Failed to save completion status. Please try again.',
step: {
platform: 'Platform',
scenarioChannel: 'Scenario & Channel',
botConfig: 'Bot Setup',
aiEngine: 'AI Engine',
done: 'Done',
},
scenario: {
title: 'What should this bot do?',
description:
'Start with one outcome. You can add more behaviors after the bot is created.',
messageReply: 'Reply to messages',
messageReplyDescription:
'Answer incoming private or group messages with an AI Pipeline.',
welcomeMembers: 'Welcome new members',
welcomeMembersDescription: 'Run an Agent when someone joins a group.',
handleDepartures: 'Handle member departures',
handleDeparturesDescription:
'Run an Agent when someone leaves or is removed.',
reviewFriendRequests: 'Review friend requests',
reviewFriendRequestsDescription:
'Let an Agent decide how to handle new friend requests.',
handleModeration: 'Handle moderation events',
handleModerationDescription:
'Run an Agent when a group member is restricted.',
pipelineBadge: 'Pipeline',
agentBadge: 'Agent',
},
platform: {
title: 'Select a Platform',
description: 'Choose the messaging platform your bot will connect to.',
title: 'Select a Channel',
description:
'Only channels that support the selected scenario are shown.',
chooseScenarioFirst: 'Choose a scenario to see compatible channels.',
noCompatiblePlatforms:
'No installed channel currently supports this scenario.',
},
botConfig: {
title: 'Configure Your Bot',
@@ -1961,7 +1987,7 @@ const enUS = {
done: {
title: 'All Set!',
description:
'Your bot has been created and connected to the AI pipeline. You can now manage it from the workbench.',
'Your bot has been created and connected to its processor. You can now manage it from the workbench.',
backToWorkbench: 'Back to Workbench',
},
},
+31 -5
View File
@@ -1834,7 +1834,7 @@ const jaJP = {
next: '次へ',
finish: '作成&デプロイ',
confirmCreateBot: '確定、ボットを作成',
createSuccess: 'パイプラインが作成され、ボットにリンクされました!',
createSuccess: 'プロセッサーが作成され、ボットにリンクされました!',
botCreateSuccess: 'ボットが正常に作成されました!',
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
createError: 'リソースの作成に失敗しました',
@@ -1843,14 +1843,40 @@ const jaJP = {
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
step: {
platform: 'プラットフォーム',
scenarioChannel: 'シナリオとチャンネル',
botConfig: 'ボット設定',
aiEngine: 'AIエンジン',
done: '完了',
},
platform: {
title: 'プラットフォームを選択',
scenario: {
title: 'このボットで何を実現しますか?',
description:
'ボットが接続するメッセージングプラットフォームを選択してください。',
'まず主要な動作を1つ選びます。作成後に他の動作も追加できます。',
messageReply: '受信メッセージに返信',
messageReplyDescription:
'AI Pipeline でプライベートまたはグループメッセージに返信します。',
welcomeMembers: '新しいメンバーを歓迎',
welcomeMembersDescription:
'メンバーがグループに参加したときに Agent を実行します。',
handleDepartures: 'メンバーの退出を処理',
handleDeparturesDescription:
'メンバーが退出または削除されたときに Agent を実行します。',
reviewFriendRequests: '友だち申請を確認',
reviewFriendRequestsDescription:
'新しい申請の処理方法を Agent に判断させます。',
handleModeration: 'モデレーションイベントを処理',
handleModerationDescription:
'グループメンバーが制限されたときに Agent を実行します。',
pipelineBadge: 'Pipeline',
agentBadge: 'Agent',
},
platform: {
title: 'チャンネルを選択',
description: '選択したシナリオに対応するチャンネルのみ表示されます。',
chooseScenarioFirst:
'シナリオを選択すると、対応するチャンネルが表示されます。',
noCompatiblePlatforms:
'現在インストールされているチャンネルはこのシナリオに対応していません。',
},
botConfig: {
title: 'ボットを設定',
@@ -1884,7 +1910,7 @@ const jaJP = {
done: {
title: '完了しました!',
description:
'ボットが作成され、AIパイプラインに接続されました。ワークベンチから管理できます。',
'ボットが作成され、プロセッサーに接続されました。ワークベンチから管理できます。',
backToWorkbench: 'ワークベンチに戻る',
},
},
+23 -5
View File
@@ -1834,7 +1834,7 @@ const zhHans = {
next: '下一步',
finish: '创建并部署',
confirmCreateBot: '确定,创建机器人',
createSuccess: '流水线已创建并关联到机器人!',
createSuccess: '处理器已创建并关联到机器人!',
botCreateSuccess: '机器人创建成功!',
botSaveSuccess: '机器人配置已保存并启用!',
createError: '创建资源失败',
@@ -1843,13 +1843,32 @@ const zhHans = {
completeSaveError: '保存完成状态失败,请重试。',
step: {
platform: '平台接入',
scenarioChannel: '场景与频道',
botConfig: '机器人配置',
aiEngine: 'AI 引擎',
done: '完成',
},
scenario: {
title: '这个机器人要完成什么?',
description: '先选择一个主要结果,机器人创建后还可以继续添加其他行为。',
messageReply: '回复收到的消息',
messageReplyDescription: '使用 AI Pipeline 回复私聊或群聊消息。',
welcomeMembers: '欢迎新成员',
welcomeMembersDescription: '有人加入群组时运行 Agent。',
handleDepartures: '处理成员离群',
handleDeparturesDescription: '有人离开或被移出群组时运行 Agent。',
reviewFriendRequests: '审核好友请求',
reviewFriendRequestsDescription: '让 Agent 决定如何处理新的好友请求。',
handleModeration: '处理群管理事件',
handleModerationDescription: '群成员受到限制时运行 Agent。',
pipelineBadge: 'Pipeline',
agentBadge: 'Agent',
},
platform: {
title: '选择平台',
description: '选择机器人要接入的消息平台。',
title: '选择频道',
description: '这里只显示支持所选场景的频道。',
chooseScenarioFirst: '请先选择场景,再查看可用频道。',
noCompatiblePlatforms: '当前安装的频道都不支持这个场景。',
},
botConfig: {
title: '配置机器人',
@@ -1877,8 +1896,7 @@ const zhHans = {
},
done: {
title: '一切就绪!',
description:
'机器人已创建并连接到 AI 流水线。你现在可以在工作台中管理它。',
description: '机器人已创建并连接到处理器。你现在可以在工作台中管理它。',
backToWorkbench: '返回工作台',
},
},