mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-26 06:16:09 +00:00
feat(wizard): start bot setup from scenarios
This commit is contained in:
@@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import {
|
||||||
|
apiJson,
|
||||||
|
createBrowser,
|
||||||
|
ensureAuthenticatedBrowser,
|
||||||
|
ensureEvidence,
|
||||||
|
evidencePaths,
|
||||||
|
exitCode,
|
||||||
|
loadEnvFiles,
|
||||||
|
localIsoWithOffset,
|
||||||
|
safeScreenshot,
|
||||||
|
scanBrowserDiagnostics,
|
||||||
|
writeResult,
|
||||||
|
} from "./lib/langbot-e2e.mjs";
|
||||||
|
|
||||||
|
const caseId = "wizard-scenario-routing";
|
||||||
|
await loadEnvFiles();
|
||||||
|
const paths = evidencePaths(caseId);
|
||||||
|
await ensureEvidence(paths);
|
||||||
|
const messageScreenshot = paths.screenshot.replace(
|
||||||
|
/\.png$/,
|
||||||
|
"-message-scenario.png",
|
||||||
|
);
|
||||||
|
const mobileScreenshot = paths.screenshot.replace(/\.png$/, "-mobile.png");
|
||||||
|
|
||||||
|
const startedAt = new Date();
|
||||||
|
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
|
||||||
|
const backendUrl = process.env.LANGBOT_BACKEND_URL || "";
|
||||||
|
|
||||||
|
let browser;
|
||||||
|
let token = "";
|
||||||
|
let botId = "";
|
||||||
|
let agentId = "";
|
||||||
|
const result = {
|
||||||
|
source: "automation",
|
||||||
|
case_id: caseId,
|
||||||
|
run_id: paths.runId,
|
||||||
|
started_at: startedAt.toISOString(),
|
||||||
|
started_at_local: localIsoWithOffset(startedAt),
|
||||||
|
finished_at: "",
|
||||||
|
finished_at_local: "",
|
||||||
|
status: "fail",
|
||||||
|
reason: "",
|
||||||
|
url: "",
|
||||||
|
visible_signals: [],
|
||||||
|
api: {},
|
||||||
|
diagnostics: null,
|
||||||
|
cleanup: null,
|
||||||
|
evidence: {
|
||||||
|
console_log: paths.consoleLog,
|
||||||
|
network_log: paths.networkLog,
|
||||||
|
screenshot: paths.screenshot,
|
||||||
|
message_scenario_screenshot: messageScreenshot,
|
||||||
|
mobile_screenshot: mobileScreenshot,
|
||||||
|
automation_result_json: paths.automationResultJson,
|
||||||
|
result_json: paths.resultJson,
|
||||||
|
},
|
||||||
|
evidence_collected: ["ui", "screenshot", "console", "api_diagnostic"],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!frontendUrl) throw new Error("LANGBOT_FRONTEND_URL is not configured.");
|
||||||
|
if (!backendUrl) throw new Error("LANGBOT_BACKEND_URL is not configured.");
|
||||||
|
|
||||||
|
browser = await createBrowser(paths);
|
||||||
|
const { page } = browser;
|
||||||
|
await page.goto(frontendUrl, { waitUntil: "domcontentloaded" });
|
||||||
|
const auth = await ensureAuthenticatedBrowser(page, {
|
||||||
|
frontendUrl,
|
||||||
|
backendUrl,
|
||||||
|
});
|
||||||
|
if (auth.status !== "pass") {
|
||||||
|
result.status = auth.status;
|
||||||
|
throw new Error(auth.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
token = await page.evaluate(() => localStorage.getItem("token") || "");
|
||||||
|
if (!token) {
|
||||||
|
result.status = "blocked";
|
||||||
|
throw new Error("Authenticated browser has no reusable local token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapters = await apiJson(backendUrl, "/api/v1/platform/adapters", {
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
const catalog = adapters.json.data?.adapters || [];
|
||||||
|
const httpBot = catalog.find((adapter) => adapter.name === "http_bot");
|
||||||
|
const welcomeAdapters = catalog.filter((adapter) =>
|
||||||
|
(adapter.spec?.supported_events || []).includes("group.member_joined"),
|
||||||
|
);
|
||||||
|
result.api.adapters = {
|
||||||
|
http_status: adapters.status,
|
||||||
|
code: adapters.json.code ?? null,
|
||||||
|
http_bot_supports_message:
|
||||||
|
!httpBot?.spec?.supported_events?.length ||
|
||||||
|
httpBot.spec.supported_events.includes("message.received"),
|
||||||
|
http_bot_supports_member_joined:
|
||||||
|
httpBot?.spec?.supported_events?.includes("group.member_joined") || false,
|
||||||
|
welcome_adapter_count: welcomeAdapters.length,
|
||||||
|
};
|
||||||
|
if (adapters.status >= 400 || adapters.json.code !== 0) {
|
||||||
|
throw new Error(adapters.json.msg || "Adapter discovery failed.");
|
||||||
|
}
|
||||||
|
if (!httpBot || welcomeAdapters.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"The adapter catalog does not contain the fixtures required by this case.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (result.api.adapters.http_bot_supports_member_joined) {
|
||||||
|
throw new Error(
|
||||||
|
"HTTP Bot unexpectedly declares group.member_joined support; update the case expectation.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressUrl = `${backendUrl.replace(/\/$/, "")}/api/v1/system/wizard/progress`;
|
||||||
|
await page.route(progressUrl, async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ code: 0, msg: "ok", data: {} }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`${frontendUrl.replace(/\/$/, "")}/wizard`, {
|
||||||
|
waitUntil: "domcontentloaded",
|
||||||
|
});
|
||||||
|
result.url = page.url();
|
||||||
|
await page
|
||||||
|
.getByText(
|
||||||
|
/What should this bot do\?|这个机器人要完成什么?|このボットで何を実現しますか?/,
|
||||||
|
)
|
||||||
|
.waitFor({ timeout: 15_000 });
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole("button", {
|
||||||
|
name: /Reply to messages|回复收到的消息|受信メッセージに返信/,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByText(/HTTP Bot|HTTP 通用接入|HTTP ボット/, { exact: true })
|
||||||
|
.waitFor();
|
||||||
|
result.visible_signals.push("scenario-first", "message-http-compatible");
|
||||||
|
await safeScreenshot(page, messageScreenshot);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole("button", {
|
||||||
|
name: /Welcome new members|欢迎新成员|新しいメンバーを歓迎/,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByText(/Discord/, { exact: true })
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
const httpBotCount = await page
|
||||||
|
.getByText(/HTTP Bot|HTTP 通用接入|HTTP ボット/, { exact: true })
|
||||||
|
.count();
|
||||||
|
if (httpBotCount !== 0) {
|
||||||
|
throw new Error("HTTP Bot remained visible for group.member_joined.");
|
||||||
|
}
|
||||||
|
const selectedScenario = page.getByRole("button", {
|
||||||
|
name: /Welcome new members|欢迎新成员|新しいメンバーを歓迎/,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!(await selectedScenario.getByText("Agent", { exact: true }).isVisible())
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"The non-message scenario is not visibly labeled as Agent.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.visible_signals.push(
|
||||||
|
"welcome-compatible-channel",
|
||||||
|
"http-filtered",
|
||||||
|
"agent-behavior-label",
|
||||||
|
);
|
||||||
|
|
||||||
|
await safeScreenshot(page, paths.screenshot);
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const horizontalOverflow = await page.evaluate(
|
||||||
|
() => document.documentElement.scrollWidth - window.innerWidth,
|
||||||
|
);
|
||||||
|
if (horizontalOverflow > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`The mobile wizard overflows horizontally by ${horizontalOverflow}px.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await selectedScenario.scrollIntoViewIfNeeded();
|
||||||
|
await page
|
||||||
|
.getByText(/Discord/, { exact: true })
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
await safeScreenshot(page, mobileScreenshot);
|
||||||
|
result.visible_signals.push("mobile-layout");
|
||||||
|
|
||||||
|
const fixtureSuffix = paths.runId.slice(-48);
|
||||||
|
const agent = await apiJson(backendUrl, "/api/v1/agents", {
|
||||||
|
method: "POST",
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
kind: "agent",
|
||||||
|
name: `Wizard Scenario Agent ${fixtureSuffix}`,
|
||||||
|
description: "Temporary wizard scenario routing fixture",
|
||||||
|
emoji: "W",
|
||||||
|
enabled: true,
|
||||||
|
supported_event_patterns: ["group.member_joined"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
agentId = agent.json.data?.uuid || "";
|
||||||
|
result.api.create_agent = {
|
||||||
|
http_status: agent.status,
|
||||||
|
code: agent.json.code ?? null,
|
||||||
|
};
|
||||||
|
if (agent.status >= 400 || agent.json.code !== 0 || !agentId) {
|
||||||
|
throw new Error(agent.json.msg || "Failed to create the temporary Agent.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await apiJson(backendUrl, "/api/v1/platform/bots", {
|
||||||
|
method: "POST",
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
name: `Wizard Scenario Bot ${fixtureSuffix}`,
|
||||||
|
description: "Temporary disabled wizard scenario routing fixture",
|
||||||
|
adapter: "aiocqhttp",
|
||||||
|
adapter_config: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 2280,
|
||||||
|
"access-token": "",
|
||||||
|
},
|
||||||
|
enable: false,
|
||||||
|
event_bindings: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
botId = bot.json.data?.uuid || "";
|
||||||
|
result.api.create_bot = {
|
||||||
|
http_status: bot.status,
|
||||||
|
code: bot.json.code ?? null,
|
||||||
|
};
|
||||||
|
if (bot.status >= 400 || bot.json.code !== 0 || !botId) {
|
||||||
|
throw new Error(bot.json.msg || "Failed to create the temporary Bot.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = await apiJson(
|
||||||
|
backendUrl,
|
||||||
|
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
event_bindings: [
|
||||||
|
{
|
||||||
|
event_pattern: "group.member_joined",
|
||||||
|
target_type: "agent",
|
||||||
|
target_uuid: agentId,
|
||||||
|
filters: [],
|
||||||
|
priority: 0,
|
||||||
|
enabled: true,
|
||||||
|
description: "Welcome new members",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
result.api.bind_route = {
|
||||||
|
http_status: update.status,
|
||||||
|
code: update.json.code ?? null,
|
||||||
|
};
|
||||||
|
if (update.status >= 400 || update.json.code !== 0) {
|
||||||
|
throw new Error(update.json.msg || "Failed to bind the temporary route.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedBot = await apiJson(
|
||||||
|
backendUrl,
|
||||||
|
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
||||||
|
{ token },
|
||||||
|
);
|
||||||
|
const savedRoute = savedBot.json.data?.bot?.event_bindings?.[0];
|
||||||
|
result.api.read_route = {
|
||||||
|
http_status: savedBot.status,
|
||||||
|
code: savedBot.json.code ?? null,
|
||||||
|
event_pattern: savedRoute?.event_pattern || null,
|
||||||
|
target_type: savedRoute?.target_type || null,
|
||||||
|
target_matches: savedRoute?.target_uuid === agentId,
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
savedBot.status >= 400 ||
|
||||||
|
savedBot.json.code !== 0 ||
|
||||||
|
savedRoute?.event_pattern !== "group.member_joined" ||
|
||||||
|
savedRoute?.target_type !== "agent" ||
|
||||||
|
savedRoute?.target_uuid !== agentId
|
||||||
|
) {
|
||||||
|
throw new Error("The saved Bot did not return the expected Agent route.");
|
||||||
|
}
|
||||||
|
result.visible_signals.push("agent-route-persisted");
|
||||||
|
|
||||||
|
result.diagnostics = await scanBrowserDiagnostics(paths);
|
||||||
|
if (result.diagnostics.status !== "pass") {
|
||||||
|
throw new Error(result.diagnostics.reason);
|
||||||
|
}
|
||||||
|
result.status = "pass";
|
||||||
|
result.reason =
|
||||||
|
"Quick Start visibly filtered channels by scenario at desktop and mobile widths.";
|
||||||
|
} catch (error) {
|
||||||
|
if (!["blocked", "env_issue"].includes(result.status)) result.status = "fail";
|
||||||
|
result.reason = result.reason || error.message;
|
||||||
|
if (browser?.page) await safeScreenshot(browser.page, paths.screenshot);
|
||||||
|
} finally {
|
||||||
|
const cleanup = {};
|
||||||
|
if (botId && token && backendUrl) {
|
||||||
|
const deletedBot = await apiJson(
|
||||||
|
backendUrl,
|
||||||
|
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
||||||
|
{ method: "DELETE", token },
|
||||||
|
).catch((error) => ({
|
||||||
|
status: 0,
|
||||||
|
json: { code: null, msg: error.message },
|
||||||
|
}));
|
||||||
|
cleanup.bot_deleted = deletedBot.status < 400 && deletedBot.json.code === 0;
|
||||||
|
cleanup.bot_http_status = deletedBot.status;
|
||||||
|
}
|
||||||
|
if (agentId && token && backendUrl) {
|
||||||
|
const deletedAgent = await apiJson(
|
||||||
|
backendUrl,
|
||||||
|
`/api/v1/agents/${encodeURIComponent(agentId)}`,
|
||||||
|
{ method: "DELETE", token },
|
||||||
|
).catch((error) => ({
|
||||||
|
status: 0,
|
||||||
|
json: { code: null, msg: error.message },
|
||||||
|
}));
|
||||||
|
cleanup.agent_deleted =
|
||||||
|
deletedAgent.status < 400 && deletedAgent.json.code === 0;
|
||||||
|
cleanup.agent_http_status = deletedAgent.status;
|
||||||
|
}
|
||||||
|
result.cleanup = cleanup;
|
||||||
|
const cleanupFailed =
|
||||||
|
(botId && !cleanup.bot_deleted) || (agentId && !cleanup.agent_deleted);
|
||||||
|
if (cleanupFailed && result.status === "pass") {
|
||||||
|
result.status = "fail";
|
||||||
|
result.reason = "The temporary wizard routing fixtures were not deleted.";
|
||||||
|
}
|
||||||
|
if (browser) await browser.close().catch(() => {});
|
||||||
|
const finishedAt = new Date();
|
||||||
|
result.finished_at = finishedAt.toISOString();
|
||||||
|
result.finished_at_local = localIsoWithOffset(finishedAt);
|
||||||
|
await writeResult(paths, result);
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(exitCode(result.status));
|
||||||
@@ -190,7 +190,8 @@
|
|||||||
"sandbox-skill-authoring-e2e",
|
"sandbox-skill-authoring-e2e",
|
||||||
"sandbox-skill-authoring-edit-existing-e2e",
|
"sandbox-skill-authoring-edit-existing-e2e",
|
||||||
"skill-discovery-via-mcp-gateway",
|
"skill-discovery-via-mcp-gateway",
|
||||||
"webui-login-state"
|
"webui-login-state",
|
||||||
|
"wizard-scenario-routing"
|
||||||
],
|
],
|
||||||
"case_summaries": [
|
"case_summaries": [
|
||||||
{
|
{
|
||||||
@@ -1647,6 +1648,31 @@
|
|||||||
"screenshot",
|
"screenshot",
|
||||||
"console"
|
"console"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wizard-scenario-routing",
|
||||||
|
"title": "Quick Start filters channels by the selected bot scenario",
|
||||||
|
"mode": "agent-browser",
|
||||||
|
"area": "wizard",
|
||||||
|
"type": "feature",
|
||||||
|
"priority": "p0",
|
||||||
|
"risk": "medium",
|
||||||
|
"ci_eligible": false,
|
||||||
|
"tags": [
|
||||||
|
"wizard",
|
||||||
|
"eba",
|
||||||
|
"event-routing",
|
||||||
|
"productization"
|
||||||
|
],
|
||||||
|
"automation": "scripts/e2e/wizard-scenario-routing.mjs",
|
||||||
|
"setup_automation": [],
|
||||||
|
"setup_provides_env": [],
|
||||||
|
"evidence_required": [
|
||||||
|
"ui",
|
||||||
|
"screenshot",
|
||||||
|
"console",
|
||||||
|
"api_diagnostic"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"suites": [
|
"suites": [
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
id: wizard-scenario-routing
|
||||||
|
title: "Quick Start filters channels by the selected bot scenario"
|
||||||
|
mode: agent-browser
|
||||||
|
area: wizard
|
||||||
|
type: feature
|
||||||
|
priority: p0
|
||||||
|
risk: medium
|
||||||
|
ci_eligible: false
|
||||||
|
tags:
|
||||||
|
- wizard
|
||||||
|
- eba
|
||||||
|
- event-routing
|
||||||
|
- productization
|
||||||
|
skills:
|
||||||
|
- langbot-env-setup
|
||||||
|
- langbot-testing
|
||||||
|
env:
|
||||||
|
- LANGBOT_FRONTEND_URL
|
||||||
|
- LANGBOT_BACKEND_URL
|
||||||
|
- LANGBOT_BROWSER_PROFILE
|
||||||
|
automation: scripts/e2e/wizard-scenario-routing.mjs
|
||||||
|
automation_env:
|
||||||
|
- LANGBOT_FRONTEND_URL
|
||||||
|
- LANGBOT_BACKEND_URL
|
||||||
|
- LANGBOT_BROWSER_PROFILE
|
||||||
|
- LANGBOT_CHROMIUM_EXECUTABLE
|
||||||
|
preconditions:
|
||||||
|
- "The target is a local test instance where a disabled temporary Bot and Agent may be created and deleted."
|
||||||
|
steps:
|
||||||
|
- "Open Quick Start and select Reply to messages."
|
||||||
|
- "Confirm HTTP Bot is available for the message scenario."
|
||||||
|
- "Select Welcome new members."
|
||||||
|
- "Confirm a compatible channel remains visible and HTTP Bot is removed."
|
||||||
|
- "Create a disabled temporary Bot and Agent, persist a group.member_joined route, and read it back through the API."
|
||||||
|
- "Verify the scenario and channel layout at desktop and mobile widths."
|
||||||
|
checks:
|
||||||
|
- "UI: Scenario selection appears before channel selection."
|
||||||
|
- "UI: Reply to messages includes HTTP Bot."
|
||||||
|
- "UI: Welcome new members includes a compatible event-capable channel."
|
||||||
|
- "UI: Welcome new members excludes HTTP Bot because it does not declare group.member_joined support."
|
||||||
|
- "UI: The selected non-message scenario is labeled as an Agent behavior."
|
||||||
|
- "Console: No unexpected frontend errors appear during the flow."
|
||||||
|
- "Network: Adapter discovery returns without a 5xx response."
|
||||||
|
- "API: A non-message event route to an Agent is normalized and persisted."
|
||||||
|
- "Cleanup: The temporary Bot and Agent are deleted after evidence is collected."
|
||||||
|
- "Isolation: Wizard progress writes are stubbed in the browser and do not change backend state."
|
||||||
|
evidence_required:
|
||||||
|
- ui
|
||||||
|
- screenshot
|
||||||
|
- console
|
||||||
|
- api_diagnostic
|
||||||
|
diagnostics:
|
||||||
|
- "The temporary Bot remains disabled, so the case cannot connect to an external messaging platform."
|
||||||
|
- "A passing adapter API response without the visible filtered channel list is not a pass."
|
||||||
|
troubleshooting:
|
||||||
|
- backend-not-listening
|
||||||
|
- proxy-env-mismatch
|
||||||
@@ -105,8 +105,8 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
"""Save wizard progress to metadata table.
|
"""Save wizard progress to metadata table.
|
||||||
|
|
||||||
Accepts JSON body with wizard state fields:
|
Accepts JSON body with wizard state fields:
|
||||||
{ "step": int, "selected_adapter": str|null, "created_bot_uuid": str|null,
|
{ "step": int, "selected_scenario": str|null, "selected_adapter": str|null,
|
||||||
"bot_saved": bool, "selected_runner": str|null }
|
"created_bot_uuid": str|null, "bot_saved": bool, "selected_runner": str|null }
|
||||||
"""
|
"""
|
||||||
data = await quart.request.get_json(silent=True) or {}
|
data = await quart.request.get_json(silent=True) or {}
|
||||||
progress_json = json.dumps(data, ensure_ascii=False)
|
progress_json = json.dumps(data, ensure_ascii=False)
|
||||||
|
|||||||
@@ -445,6 +445,7 @@ export interface SystemLimitation {
|
|||||||
|
|
||||||
export interface WizardProgress {
|
export interface WizardProgress {
|
||||||
step: number;
|
step: number;
|
||||||
|
selected_scenario?: string | null;
|
||||||
selected_adapter: string | null;
|
selected_adapter: string | null;
|
||||||
created_bot_uuid: string | null;
|
created_bot_uuid: string | null;
|
||||||
bot_saved: boolean;
|
bot_saved: boolean;
|
||||||
|
|||||||
@@ -1072,6 +1072,7 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
|
|
||||||
public saveWizardProgress(progress: {
|
public saveWizardProgress(progress: {
|
||||||
step: number;
|
step: number;
|
||||||
|
selected_scenario?: string | null;
|
||||||
selected_adapter: string | null;
|
selected_adapter: string | null;
|
||||||
created_bot_uuid: string | null;
|
created_bot_uuid: string | null;
|
||||||
bot_saved: boolean;
|
bot_saved: boolean;
|
||||||
|
|||||||
+442
-191
@@ -12,6 +12,11 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
PartyPopper,
|
PartyPopper,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
MessageSquare,
|
||||||
|
ShieldCheck,
|
||||||
|
UserCheck,
|
||||||
|
UserMinus,
|
||||||
|
UserPlus,
|
||||||
X,
|
X,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -75,6 +80,73 @@ import {
|
|||||||
|
|
||||||
const TOTAL_STEPS = 4;
|
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)
|
// Main Wizard Page (full-screen, no sidebar)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -85,6 +157,8 @@ export default function WizardPage() {
|
|||||||
|
|
||||||
// ---- Wizard state ----
|
// ---- Wizard state ----
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [selectedScenario, setSelectedScenario] =
|
||||||
|
useState<WizardScenarioId | null>(null);
|
||||||
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
||||||
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
|
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
|
||||||
const [botName, setBotName] = useState('');
|
const [botName, setBotName] = useState('');
|
||||||
@@ -113,16 +187,36 @@ export default function WizardPage() {
|
|||||||
(overrides: Partial<WizardProgress> = {}) => {
|
(overrides: Partial<WizardProgress> = {}) => {
|
||||||
const progress: WizardProgress = {
|
const progress: WizardProgress = {
|
||||||
step: overrides.step ?? currentStep,
|
step: overrides.step ?? currentStep,
|
||||||
selected_adapter: overrides.selected_adapter ?? selectedAdapter,
|
selected_scenario:
|
||||||
created_bot_uuid: overrides.created_bot_uuid ?? createdBotUuid,
|
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,
|
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) => {
|
httpClient.saveWizardProgress(progress).catch((err) => {
|
||||||
console.error('Failed to save wizard progress', 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 ----
|
// ---- Fetch remote data & restore progress ----
|
||||||
@@ -151,6 +245,10 @@ export default function WizardPage() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
setSelectedAdapter(progress.selected_adapter);
|
setSelectedAdapter(progress.selected_adapter);
|
||||||
|
setSelectedScenario(
|
||||||
|
(progress.selected_scenario as WizardScenarioId | null) ??
|
||||||
|
'message_reply',
|
||||||
|
);
|
||||||
setCreatedBotUuid(progress.created_bot_uuid);
|
setCreatedBotUuid(progress.created_bot_uuid);
|
||||||
setBotSaved(progress.bot_saved ?? false);
|
setBotSaved(progress.bot_saved ?? false);
|
||||||
setSelectedRunner(progress.selected_runner);
|
setSelectedRunner(progress.selected_runner);
|
||||||
@@ -167,13 +265,14 @@ export default function WizardPage() {
|
|||||||
(runtimeValues?.extra_webhook_full_url as string) || '',
|
(runtimeValues?.extra_webhook_full_url as string) || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restore step (cap at step 2 — step 3 means done)
|
// Step 3 is resumable so a refresh cannot create a duplicate processor.
|
||||||
setCurrentStep(Math.min(progress.step, 2));
|
setCurrentStep(Math.min(progress.step, 3));
|
||||||
} catch {
|
} catch {
|
||||||
// Bot no longer exists — clear stale progress and start fresh
|
// Bot no longer exists — clear stale progress and start fresh
|
||||||
httpClient
|
httpClient
|
||||||
.saveWizardProgress({
|
.saveWizardProgress({
|
||||||
step: 0,
|
step: 0,
|
||||||
|
selected_scenario: null,
|
||||||
selected_adapter: null,
|
selected_adapter: null,
|
||||||
created_bot_uuid: null,
|
created_bot_uuid: null,
|
||||||
bot_saved: false,
|
bot_saved: false,
|
||||||
@@ -213,6 +312,11 @@ export default function WizardPage() {
|
|||||||
return aiConfigTab.stages.find((s) => s.name === selectedRunner);
|
return aiConfigTab.stages.find((s) => s.name === selectedRunner);
|
||||||
}, [selectedRunner, aiConfigTab]);
|
}, [selectedRunner, aiConfigTab]);
|
||||||
|
|
||||||
|
const selectedScenarioDefinition = useMemo(
|
||||||
|
() => WIZARD_SCENARIOS.find((item) => item.id === selectedScenario),
|
||||||
|
[selectedScenario],
|
||||||
|
);
|
||||||
|
|
||||||
// Adapter spec config for the selected adapter
|
// Adapter spec config for the selected adapter
|
||||||
const selectedAdapterConfig: IDynamicFormItemSchema[] = useMemo(() => {
|
const selectedAdapterConfig: IDynamicFormItemSchema[] = useMemo(() => {
|
||||||
const adapter = adapters.find((a) => a.name === selectedAdapter);
|
const adapter = adapters.find((a) => a.name === selectedAdapter);
|
||||||
@@ -270,7 +374,7 @@ export default function WizardPage() {
|
|||||||
const canProceed = useCallback((): boolean => {
|
const canProceed = useCallback((): boolean => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case 0:
|
case 0:
|
||||||
return selectedAdapter !== null;
|
return selectedScenario !== null && selectedAdapter !== null;
|
||||||
case 1:
|
case 1:
|
||||||
return createdBotUuid !== null && botSaved;
|
return createdBotUuid !== null && botSaved;
|
||||||
case 2:
|
case 2:
|
||||||
@@ -278,7 +382,32 @@ export default function WizardPage() {
|
|||||||
default:
|
default:
|
||||||
return false;
|
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(() => {
|
const goNext = useCallback(() => {
|
||||||
if (currentStep < TOTAL_STEPS - 1 && canProceed()) {
|
if (currentStep < TOTAL_STEPS - 1 && canProceed()) {
|
||||||
@@ -345,6 +474,7 @@ export default function WizardPage() {
|
|||||||
// Persist progress
|
// Persist progress
|
||||||
saveProgress({
|
saveProgress({
|
||||||
step: 1,
|
step: 1,
|
||||||
|
selected_scenario: selectedScenario,
|
||||||
selected_adapter: selectedAdapter,
|
selected_adapter: selectedAdapter,
|
||||||
created_bot_uuid: resp.uuid,
|
created_bot_uuid: resp.uuid,
|
||||||
bot_saved: false,
|
bot_saved: false,
|
||||||
@@ -358,7 +488,7 @@ export default function WizardPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsCreatingBot(false);
|
setIsCreatingBot(false);
|
||||||
}
|
}
|
||||||
}, [selectedAdapter, adapters, t, saveProgress]);
|
}, [selectedScenario, selectedAdapter, adapters, t, saveProgress]);
|
||||||
|
|
||||||
// ---- Save Bot Config & Enable (Step 1) ----
|
// ---- Save Bot Config & Enable (Step 1) ----
|
||||||
// Updates the bot's adapter config and enables it.
|
// Updates the bot's adapter config and enables it.
|
||||||
@@ -414,62 +544,75 @@ export default function WizardPage() {
|
|||||||
// ---- Create Pipeline & Link (Step 2 finish) ----
|
// ---- Create Pipeline & Link (Step 2 finish) ----
|
||||||
|
|
||||||
const handleFinish = useCallback(async () => {
|
const handleFinish = useCallback(async () => {
|
||||||
if (!selectedRunner || !createdBotUuid) return;
|
if (!selectedRunner || !createdBotUuid || !selectedScenarioDefinition)
|
||||||
|
return;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
let processorUuid = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Create pipeline (backend fills config from default template)
|
let targetType: 'agent' | 'pipeline';
|
||||||
const pipeline: Pipeline = {
|
|
||||||
name: `${botName} Pipeline`,
|
|
||||||
description: botDescription || '',
|
|
||||||
config: {},
|
|
||||||
};
|
|
||||||
const pipelineResp = await httpClient.createPipeline(pipeline);
|
|
||||||
|
|
||||||
// 2. Fetch the created pipeline to get the full default config
|
if (selectedScenarioDefinition.processorKind === 'pipeline') {
|
||||||
// (includes trigger, safety, ai, output sections).
|
const pipeline: Pipeline = {
|
||||||
// Then merge only the AI section with the wizard's runner config.
|
name: `${botName} Pipeline`,
|
||||||
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
|
description: botDescription || '',
|
||||||
const fullConfig = createdPipeline.pipeline.config as unknown as Record<
|
config: {},
|
||||||
string,
|
};
|
||||||
unknown
|
const pipelineResp = await httpClient.createPipeline(pipeline);
|
||||||
>;
|
processorUuid = pipelineResp.uuid;
|
||||||
const fullAiConfig =
|
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
|
||||||
fullConfig.ai && typeof fullConfig.ai === 'object'
|
const fullConfig = createdPipeline.pipeline.config as unknown as Record<
|
||||||
? (fullConfig.ai as Record<string, unknown>)
|
string,
|
||||||
: {};
|
unknown
|
||||||
const existingRunner =
|
>;
|
||||||
fullAiConfig.runner && typeof fullAiConfig.runner === 'object'
|
const fullAiConfig =
|
||||||
? (fullAiConfig.runner as Record<string, unknown>)
|
fullConfig.ai && typeof fullConfig.ai === 'object'
|
||||||
: {};
|
? (fullConfig.ai as Record<string, unknown>)
|
||||||
const existingRunnerConfigs =
|
: {};
|
||||||
fullAiConfig.runner_config &&
|
const existingRunner =
|
||||||
typeof fullAiConfig.runner_config === 'object'
|
fullAiConfig.runner && typeof fullAiConfig.runner === 'object'
|
||||||
? (fullAiConfig.runner_config as Record<string, unknown>)
|
? (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 = {
|
await httpClient.updatePipeline(pipelineResp.uuid, {
|
||||||
...fullConfig,
|
name: `${botName} Pipeline`,
|
||||||
ai: {
|
description: botDescription || '',
|
||||||
...fullAiConfig,
|
config: {
|
||||||
runner: {
|
...fullConfig,
|
||||||
...existingRunner,
|
ai: {
|
||||||
id: selectedRunner,
|
...fullAiConfig,
|
||||||
|
runner: { ...existingRunner, id: selectedRunner },
|
||||||
|
runner_config: {
|
||||||
|
...existingRunnerConfigs,
|
||||||
|
[selectedRunner]: runnerConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
runner_config: {
|
});
|
||||||
...existingRunnerConfigs,
|
targetType = 'pipeline';
|
||||||
[selectedRunner]: runnerConfig,
|
} 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 botData = await httpClient.getBot(createdBotUuid);
|
||||||
const existingBot = botData.bot;
|
const existingBot = botData.bot;
|
||||||
await httpClient.updateBot(createdBotUuid, {
|
await httpClient.updateBot(createdBotUuid, {
|
||||||
@@ -480,9 +623,9 @@ export default function WizardPage() {
|
|||||||
enable: existingBot.enable,
|
enable: existingBot.enable,
|
||||||
event_bindings: [
|
event_bindings: [
|
||||||
{
|
{
|
||||||
event_pattern: 'message.received',
|
event_pattern: selectedScenarioDefinition.eventType,
|
||||||
target_type: 'pipeline',
|
target_type: targetType,
|
||||||
target_uuid: pipelineResp.uuid,
|
target_uuid: processorUuid,
|
||||||
filters: [],
|
filters: [],
|
||||||
priority: 0,
|
priority: 0,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -492,7 +635,15 @@ export default function WizardPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setCurrentStep(3);
|
setCurrentStep(3);
|
||||||
|
saveProgress({ step: 3 });
|
||||||
} catch (err) {
|
} 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 };
|
const apiErr = err as { msg?: string };
|
||||||
toast.error(
|
toast.error(
|
||||||
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
|
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
|
||||||
@@ -503,10 +654,12 @@ export default function WizardPage() {
|
|||||||
}, [
|
}, [
|
||||||
selectedRunner,
|
selectedRunner,
|
||||||
createdBotUuid,
|
createdBotUuid,
|
||||||
|
selectedScenarioDefinition,
|
||||||
botName,
|
botName,
|
||||||
botDescription,
|
botDescription,
|
||||||
runnerConfig,
|
runnerConfig,
|
||||||
t,
|
t,
|
||||||
|
saveProgress,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ---- Space auth redirect ----
|
// ---- Space auth redirect ----
|
||||||
@@ -541,6 +694,7 @@ export default function WizardPage() {
|
|||||||
// Always clear persisted progress so re-entering starts fresh
|
// Always clear persisted progress so re-entering starts fresh
|
||||||
await httpClient.saveWizardProgress({
|
await httpClient.saveWizardProgress({
|
||||||
step: 0,
|
step: 0,
|
||||||
|
selected_scenario: null,
|
||||||
selected_adapter: null,
|
selected_adapter: null,
|
||||||
created_bot_uuid: null,
|
created_bot_uuid: null,
|
||||||
bot_saved: false,
|
bot_saved: false,
|
||||||
@@ -568,7 +722,7 @@ export default function WizardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stepLabels = [
|
const stepLabels = [
|
||||||
t('wizard.step.platform'),
|
t('wizard.step.scenarioChannel'),
|
||||||
t('wizard.step.botConfig'),
|
t('wizard.step.botConfig'),
|
||||||
t('wizard.step.aiEngine'),
|
t('wizard.step.aiEngine'),
|
||||||
t('wizard.step.done'),
|
t('wizard.step.done'),
|
||||||
@@ -657,6 +811,8 @@ export default function WizardPage() {
|
|||||||
{currentStep === 0 && (
|
{currentStep === 0 && (
|
||||||
<StepPlatform
|
<StepPlatform
|
||||||
adapters={adapters}
|
adapters={adapters}
|
||||||
|
selectedScenario={selectedScenario}
|
||||||
|
onSelectScenario={handleSelectScenario}
|
||||||
selected={selectedAdapter}
|
selected={selectedAdapter}
|
||||||
onSelect={setSelectedAdapter}
|
onSelect={setSelectedAdapter}
|
||||||
/>
|
/>
|
||||||
@@ -769,10 +925,14 @@ export default function WizardPage() {
|
|||||||
|
|
||||||
function StepPlatform({
|
function StepPlatform({
|
||||||
adapters,
|
adapters,
|
||||||
|
selectedScenario,
|
||||||
|
onSelectScenario,
|
||||||
selected,
|
selected,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
adapters: Adapter[];
|
adapters: Adapter[];
|
||||||
|
selectedScenario: WizardScenarioId | null;
|
||||||
|
onSelectScenario: (scenarioId: WizardScenarioId) => void;
|
||||||
selected: string | null;
|
selected: string | null;
|
||||||
onSelect: (name: string) => void;
|
onSelect: (name: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -780,12 +940,26 @@ function StepPlatform({
|
|||||||
const [showLegacy, setShowLegacy] = useState(false);
|
const [showLegacy, setShowLegacy] = useState(false);
|
||||||
|
|
||||||
const activeAdapters = useMemo(
|
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(
|
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(() => {
|
const groupedAdapters = useMemo(() => {
|
||||||
@@ -797,139 +971,215 @@ function StepPlatform({
|
|||||||
}, [activeAdapters]);
|
}, [activeAdapters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-4xl mx-auto">
|
<div className="mx-auto max-w-5xl space-y-8">
|
||||||
<div className="text-center">
|
<section className="space-y-3">
|
||||||
<h2 className="text-xl font-semibold">{t('wizard.platform.title')}</h2>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<h2 className="text-xl font-semibold">
|
||||||
{t('wizard.platform.description')}
|
{t('wizard.scenario.title')}
|
||||||
</p>
|
</h2>
|
||||||
</div>
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
{groupedAdapters.map((group) => (
|
{t('wizard.scenario.description')}
|
||||||
<div key={group.categoryId ?? 'uncategorized'} className="space-y-3">
|
</p>
|
||||||
{group.categoryId && (
|
</div>
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{getCategoryLabel(t, group.categoryId)}
|
{WIZARD_SCENARIOS.map((scenario) => {
|
||||||
</h3>
|
const Icon = scenario.icon;
|
||||||
)}
|
const isSelected = selectedScenario === scenario.id;
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
return (
|
||||||
{group.items.map((adapter) => (
|
<button
|
||||||
<Card
|
key={scenario.id}
|
||||||
key={adapter.name}
|
type="button"
|
||||||
|
onClick={() => onSelectScenario(scenario.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'cursor-pointer transition-all hover:shadow-md',
|
'rounded-md border bg-card p-3 text-left transition-colors',
|
||||||
selected === adapter.name
|
isSelected
|
||||||
? 'ring-2 ring-primary shadow-md'
|
? 'border-primary ring-2 ring-primary/20'
|
||||||
: 'hover:border-primary/50',
|
: 'hover:border-primary/60',
|
||||||
)}
|
)}
|
||||||
onClick={() => onSelect(adapter.name)}
|
|
||||||
>
|
>
|
||||||
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
<div className="flex items-start gap-3">
|
||||||
<img
|
<span
|
||||||
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(
|
className={cn(
|
||||||
'cursor-pointer transition-all hover:shadow-md',
|
'flex h-8 w-8 shrink-0 items-center justify-center rounded-md',
|
||||||
selected === adapter.name
|
isSelected
|
||||||
? 'ring-2 ring-primary shadow-md'
|
? 'bg-primary text-primary-foreground'
|
||||||
: 'hover:border-primary/50',
|
: 'bg-muted text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
onClick={() => onSelect(adapter.name)}
|
|
||||||
>
|
>
|
||||||
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
<Icon className="h-4 w-4" />
|
||||||
<img
|
</span>
|
||||||
src={httpClient.getAdapterIconURL(adapter.name)}
|
<span className="min-w-0 flex-1">
|
||||||
alt=""
|
<span className="flex items-center justify-between gap-2">
|
||||||
className="w-10 h-10 rounded-lg shrink-0 grayscale"
|
<span className="font-medium">
|
||||||
/>
|
{t(scenario.labelKey)}
|
||||||
<div className="min-w-0">
|
</span>
|
||||||
<CardTitle className="text-base truncate">
|
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
{extractI18nObject(adapter.label)}
|
{t(
|
||||||
</CardTitle>
|
scenario.processorKind === 'pipeline'
|
||||||
</div>
|
? 'wizard.scenario.pipelineBadge'
|
||||||
{selected === adapter.name && (
|
: 'wizard.scenario.agentBadge',
|
||||||
<div className="ml-auto shrink-0">
|
)}
|
||||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
|
</span>
|
||||||
<Check className="w-3 h-3 text-primary-foreground" />
|
</span>
|
||||||
</div>
|
<span className="mt-1 block text-sm leading-snug text-muted-foreground">
|
||||||
</div>
|
{t(scenario.descriptionKey)}
|
||||||
)}
|
</span>
|
||||||
</CardHeader>
|
</span>
|
||||||
</Card>
|
</div>
|
||||||
))}
|
</button>
|
||||||
</div>
|
);
|
||||||
</>
|
})}
|
||||||
)}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1308,6 +1558,7 @@ function StepDone() {
|
|||||||
// Always clear persisted progress so re-entering starts fresh
|
// Always clear persisted progress so re-entering starts fresh
|
||||||
await httpClient.saveWizardProgress({
|
await httpClient.saveWizardProgress({
|
||||||
step: 0,
|
step: 0,
|
||||||
|
selected_scenario: null,
|
||||||
selected_adapter: null,
|
selected_adapter: null,
|
||||||
created_bot_uuid: null,
|
created_bot_uuid: null,
|
||||||
bot_saved: false,
|
bot_saved: false,
|
||||||
|
|||||||
@@ -1913,7 +1913,7 @@ const enUS = {
|
|||||||
next: 'Next',
|
next: 'Next',
|
||||||
finish: 'Create & Deploy',
|
finish: 'Create & Deploy',
|
||||||
confirmCreateBot: 'Confirm, Create Bot',
|
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!',
|
botCreateSuccess: 'Bot created successfully!',
|
||||||
botSaveSuccess: 'Bot configuration saved and enabled!',
|
botSaveSuccess: 'Bot configuration saved and enabled!',
|
||||||
createError: 'Failed to create resources',
|
createError: 'Failed to create resources',
|
||||||
@@ -1922,13 +1922,39 @@ const enUS = {
|
|||||||
completeSaveError: 'Failed to save completion status. Please try again.',
|
completeSaveError: 'Failed to save completion status. Please try again.',
|
||||||
step: {
|
step: {
|
||||||
platform: 'Platform',
|
platform: 'Platform',
|
||||||
|
scenarioChannel: 'Scenario & Channel',
|
||||||
botConfig: 'Bot Setup',
|
botConfig: 'Bot Setup',
|
||||||
aiEngine: 'AI Engine',
|
aiEngine: 'AI Engine',
|
||||||
done: 'Done',
|
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: {
|
platform: {
|
||||||
title: 'Select a Platform',
|
title: 'Select a Channel',
|
||||||
description: 'Choose the messaging platform your bot will connect to.',
|
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: {
|
botConfig: {
|
||||||
title: 'Configure Your Bot',
|
title: 'Configure Your Bot',
|
||||||
@@ -1961,7 +1987,7 @@ const enUS = {
|
|||||||
done: {
|
done: {
|
||||||
title: 'All Set!',
|
title: 'All Set!',
|
||||||
description:
|
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',
|
backToWorkbench: 'Back to Workbench',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1834,7 +1834,7 @@ const jaJP = {
|
|||||||
next: '次へ',
|
next: '次へ',
|
||||||
finish: '作成&デプロイ',
|
finish: '作成&デプロイ',
|
||||||
confirmCreateBot: '確定、ボットを作成',
|
confirmCreateBot: '確定、ボットを作成',
|
||||||
createSuccess: 'パイプラインが作成され、ボットにリンクされました!',
|
createSuccess: 'プロセッサーが作成され、ボットにリンクされました!',
|
||||||
botCreateSuccess: 'ボットが正常に作成されました!',
|
botCreateSuccess: 'ボットが正常に作成されました!',
|
||||||
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
||||||
createError: 'リソースの作成に失敗しました',
|
createError: 'リソースの作成に失敗しました',
|
||||||
@@ -1843,14 +1843,40 @@ const jaJP = {
|
|||||||
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
|
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
|
||||||
step: {
|
step: {
|
||||||
platform: 'プラットフォーム',
|
platform: 'プラットフォーム',
|
||||||
|
scenarioChannel: 'シナリオとチャンネル',
|
||||||
botConfig: 'ボット設定',
|
botConfig: 'ボット設定',
|
||||||
aiEngine: 'AIエンジン',
|
aiEngine: 'AIエンジン',
|
||||||
done: '完了',
|
done: '完了',
|
||||||
},
|
},
|
||||||
platform: {
|
scenario: {
|
||||||
title: 'プラットフォームを選択',
|
title: 'このボットで何を実現しますか?',
|
||||||
description:
|
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: {
|
botConfig: {
|
||||||
title: 'ボットを設定',
|
title: 'ボットを設定',
|
||||||
@@ -1884,7 +1910,7 @@ const jaJP = {
|
|||||||
done: {
|
done: {
|
||||||
title: '完了しました!',
|
title: '完了しました!',
|
||||||
description:
|
description:
|
||||||
'ボットが作成され、AIパイプラインに接続されました。ワークベンチから管理できます。',
|
'ボットが作成され、プロセッサーに接続されました。ワークベンチから管理できます。',
|
||||||
backToWorkbench: 'ワークベンチに戻る',
|
backToWorkbench: 'ワークベンチに戻る',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1834,7 +1834,7 @@ const zhHans = {
|
|||||||
next: '下一步',
|
next: '下一步',
|
||||||
finish: '创建并部署',
|
finish: '创建并部署',
|
||||||
confirmCreateBot: '确定,创建机器人',
|
confirmCreateBot: '确定,创建机器人',
|
||||||
createSuccess: '流水线已创建并关联到机器人!',
|
createSuccess: '处理器已创建并关联到机器人!',
|
||||||
botCreateSuccess: '机器人创建成功!',
|
botCreateSuccess: '机器人创建成功!',
|
||||||
botSaveSuccess: '机器人配置已保存并启用!',
|
botSaveSuccess: '机器人配置已保存并启用!',
|
||||||
createError: '创建资源失败',
|
createError: '创建资源失败',
|
||||||
@@ -1843,13 +1843,32 @@ const zhHans = {
|
|||||||
completeSaveError: '保存完成状态失败,请重试。',
|
completeSaveError: '保存完成状态失败,请重试。',
|
||||||
step: {
|
step: {
|
||||||
platform: '平台接入',
|
platform: '平台接入',
|
||||||
|
scenarioChannel: '场景与频道',
|
||||||
botConfig: '机器人配置',
|
botConfig: '机器人配置',
|
||||||
aiEngine: 'AI 引擎',
|
aiEngine: 'AI 引擎',
|
||||||
done: '完成',
|
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: {
|
platform: {
|
||||||
title: '选择平台',
|
title: '选择频道',
|
||||||
description: '选择机器人要接入的消息平台。',
|
description: '这里只显示支持所选场景的频道。',
|
||||||
|
chooseScenarioFirst: '请先选择场景,再查看可用频道。',
|
||||||
|
noCompatiblePlatforms: '当前安装的频道都不支持这个场景。',
|
||||||
},
|
},
|
||||||
botConfig: {
|
botConfig: {
|
||||||
title: '配置机器人',
|
title: '配置机器人',
|
||||||
@@ -1877,8 +1896,7 @@ const zhHans = {
|
|||||||
},
|
},
|
||||||
done: {
|
done: {
|
||||||
title: '一切就绪!',
|
title: '一切就绪!',
|
||||||
description:
|
description: '机器人已创建并连接到处理器。你现在可以在工作台中管理它。',
|
||||||
'机器人已创建并连接到 AI 流水线。你现在可以在工作台中管理它。',
|
|
||||||
backToWorkbench: '返回工作台',
|
backToWorkbench: '返回工作台',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user