@12-apps/mcp 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +28 -16
- package/src/dispatch/proxy.test.ts +61 -10
- package/src/guide.ts +253 -0
- package/src/index.ts +17 -0
- package/src/openapi/generate.test.ts +123 -11
- package/src/openapi/generate.ts +66 -8
- package/src/react/ai-capabilities.tsx +99 -0
- package/src/react/ai-connection-utils.ts +96 -0
- package/src/react/ai-flow-steps.tsx +290 -0
- package/src/react/ai-icons.tsx +70 -0
- package/src/react/ai-landing.tsx +125 -0
- package/src/react/ai-onboarding.tsx +147 -0
- package/src/react/ai-status-board.tsx +124 -0
- package/src/react/ai-steps.tsx +167 -0
- package/src/react/feature-badge.tsx +46 -0
- package/src/react/host-connect-guide.tsx +225 -0
- package/src/react/host-select-step.tsx +109 -0
- package/src/react/index.ts +43 -0
- package/src/react/mcp-endpoint-url.tsx +24 -0
- package/src/server/manifest.test.ts +29 -9
- package/src/server/redact.test.ts +47 -0
- package/src/server/redact.ts +47 -0
- package/src/server/registry.test.ts +113 -4
- package/src/server/registry.ts +64 -6
- package/src/types.ts +29 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import BoltOutlinedIcon from "@mui/icons-material/BoltOutlined";
|
|
4
|
+
import LanguageOutlinedIcon from "@mui/icons-material/LanguageOutlined";
|
|
5
|
+
import LockOpenOutlinedIcon from "@mui/icons-material/LockOpenOutlined";
|
|
6
|
+
import VerifiedUserOutlinedIcon from "@mui/icons-material/VerifiedUserOutlined";
|
|
7
|
+
|
|
8
|
+
import { Button } from "@12-apps/ui/form/Button";
|
|
9
|
+
import { Box } from "@12-apps/ui/mui/Box";
|
|
10
|
+
import { Stack } from "@12-apps/ui/mui/Stack";
|
|
11
|
+
import { Text } from "@12-apps/ui/typography/Text";
|
|
12
|
+
|
|
13
|
+
import { AI_CAPABILITIES, AI_PERMISSION_MODEL, type AiCapability } from "../guide";
|
|
14
|
+
import { AiCapabilities } from "./ai-capabilities";
|
|
15
|
+
import { FeatureBadge, type FeatureBadgeItem } from "./feature-badge";
|
|
16
|
+
|
|
17
|
+
const FEATURES: FeatureBadgeItem[] = [
|
|
18
|
+
{ icon: <LockOpenOutlinedIcon />, label: "Usa o seu próprio login", caption: "Sem chaves ou credenciais extras" },
|
|
19
|
+
{ icon: <BoltOutlinedIcon />, label: "Sem instalar nada", caption: "Conecta em poucos minutos" },
|
|
20
|
+
{ icon: <VerifiedUserOutlinedIcon />, label: "Só o que você pode", caption: "As suas permissões, nada além" },
|
|
21
|
+
{ icon: <LanguageOutlinedIcon />, label: "Navegador ou app", caption: "Claude, ChatGPT, Codex" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** The single-column marketing hero: eyebrow + headline + description + CTA. */
|
|
25
|
+
function Hero({ onStart }: { onStart: () => void }): React.JSX.Element {
|
|
26
|
+
return (
|
|
27
|
+
<Stack spacing={2.5} sx={{ maxWidth: 680 }}>
|
|
28
|
+
<Box
|
|
29
|
+
sx={{
|
|
30
|
+
color: "primary.main",
|
|
31
|
+
fontSize: 12,
|
|
32
|
+
fontWeight: 700,
|
|
33
|
+
letterSpacing: 1.2,
|
|
34
|
+
textTransform: "uppercase",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
Integração com IA
|
|
38
|
+
</Box>
|
|
39
|
+
<Text variant="heading" size="xl" weight="bold" as="h1">
|
|
40
|
+
Conecte{" "}
|
|
41
|
+
<Box component="span" sx={{ color: "primary.main" }}>
|
|
42
|
+
assistentes de IA
|
|
43
|
+
</Box>{" "}
|
|
44
|
+
à sua loja
|
|
45
|
+
</Text>
|
|
46
|
+
<Text variant="body" color="secondary" as="p">
|
|
47
|
+
Deixe o Claude, o ChatGPT e outros assistentes responderem sobre o seu cardápio, estoque e
|
|
48
|
+
pedidos — e executarem ações por você, direto na conversa. Com segurança e sem instalar
|
|
49
|
+
nada.
|
|
50
|
+
</Text>
|
|
51
|
+
<Box sx={{ pt: 1 }}>
|
|
52
|
+
<Button onClick={onStart} data-testid="ai-landing-start">
|
|
53
|
+
Ver como conectar
|
|
54
|
+
</Button>
|
|
55
|
+
</Box>
|
|
56
|
+
</Stack>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Homepage-style marketing landing for the AI integration (shown before the
|
|
62
|
+
* owner starts): a hero, the permission reassurance, a trust/feature strip, and
|
|
63
|
+
* the capability highlights. `onStart` begins the guided flow. `permissionModel`
|
|
64
|
+
* and `capabilities` default to the shared copy; apps can override.
|
|
65
|
+
*/
|
|
66
|
+
export function AiLanding({
|
|
67
|
+
onStart,
|
|
68
|
+
permissionModel = AI_PERMISSION_MODEL,
|
|
69
|
+
capabilities = AI_CAPABILITIES,
|
|
70
|
+
}: {
|
|
71
|
+
onStart: () => void;
|
|
72
|
+
permissionModel?: string;
|
|
73
|
+
capabilities?: readonly AiCapability[];
|
|
74
|
+
}): React.JSX.Element {
|
|
75
|
+
return (
|
|
76
|
+
<Stack spacing={6} data-testid="ai-landing">
|
|
77
|
+
<Hero onStart={onStart} />
|
|
78
|
+
|
|
79
|
+
<Stack
|
|
80
|
+
direction="row"
|
|
81
|
+
spacing={1.5}
|
|
82
|
+
alignItems="flex-start"
|
|
83
|
+
data-testid="ai-permission-callout"
|
|
84
|
+
sx={{
|
|
85
|
+
p: 2,
|
|
86
|
+
borderRadius: 2,
|
|
87
|
+
bgcolor: "action.hover",
|
|
88
|
+
border: 1,
|
|
89
|
+
borderColor: "divider",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<VerifiedUserOutlinedIcon sx={{ color: "success.main", mt: 0.25 }} />
|
|
93
|
+
<Text variant="body" as="p">
|
|
94
|
+
{permissionModel}
|
|
95
|
+
</Text>
|
|
96
|
+
</Stack>
|
|
97
|
+
|
|
98
|
+
<Box
|
|
99
|
+
sx={{
|
|
100
|
+
p: { xs: 2.5, md: 3 },
|
|
101
|
+
borderRadius: 3,
|
|
102
|
+
bgcolor: "action.hover",
|
|
103
|
+
border: 1,
|
|
104
|
+
borderColor: "divider",
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<Box
|
|
108
|
+
sx={{
|
|
109
|
+
display: "grid",
|
|
110
|
+
gap: 3,
|
|
111
|
+
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr", md: "repeat(4, 1fr)" },
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{FEATURES.map((f) => (
|
|
115
|
+
<FeatureBadge key={f.label} icon={f.icon} label={f.label} caption={f.caption} />
|
|
116
|
+
))}
|
|
117
|
+
</Box>
|
|
118
|
+
</Box>
|
|
119
|
+
|
|
120
|
+
<Box>
|
|
121
|
+
<AiCapabilities capabilities={capabilities} />
|
|
122
|
+
</Box>
|
|
123
|
+
</Stack>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
GuidedSection,
|
|
5
|
+
OnboardingProvider,
|
|
6
|
+
useOnboarding,
|
|
7
|
+
type OnboardingStateSnapshot,
|
|
8
|
+
type OnboardingStore,
|
|
9
|
+
} from "@12-apps/onboarding";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
AI_CAPABILITIES,
|
|
13
|
+
AI_CONNECT_PROMPT,
|
|
14
|
+
AI_HOST_GUIDES,
|
|
15
|
+
AI_PERMISSION_MODEL,
|
|
16
|
+
type AiCapability,
|
|
17
|
+
type AiHostGuide,
|
|
18
|
+
} from "../guide";
|
|
19
|
+
import { AiLanding } from "./ai-landing";
|
|
20
|
+
import {
|
|
21
|
+
buildFlowSteps,
|
|
22
|
+
connectedSummary,
|
|
23
|
+
connectedTitle,
|
|
24
|
+
StatusBoard,
|
|
25
|
+
type AiConnection,
|
|
26
|
+
} from "./ai-steps";
|
|
27
|
+
|
|
28
|
+
export type { AiConnection } from "./ai-steps";
|
|
29
|
+
|
|
30
|
+
const DEFAULT_FEATURE_KEY = "ai_integration";
|
|
31
|
+
|
|
32
|
+
/** Props for the reusable AI-connect onboarding flow. */
|
|
33
|
+
export interface AiIntegrationOnboardingProps {
|
|
34
|
+
/** Persistence seam — the app wires this to its own backend (server actions). */
|
|
35
|
+
store: OnboardingStore;
|
|
36
|
+
/** The MCP endpoint URL to paste (derived by the app from its public origin). */
|
|
37
|
+
endpointUrl: string;
|
|
38
|
+
/** The owner's saved progress (null → first run / landing). */
|
|
39
|
+
initialState: OnboardingStateSnapshot | null;
|
|
40
|
+
/** The live MCP connections (one per connected assistant; empty when none). */
|
|
41
|
+
connections: readonly AiConnection[];
|
|
42
|
+
/** Onboarding feature key (persistence namespace). @default "ai_integration" */
|
|
43
|
+
featureKey?: string;
|
|
44
|
+
/** Show the dev-only "reset onboarding" button. @default false */
|
|
45
|
+
devReset?: boolean;
|
|
46
|
+
/** Assistants offered in the flow. @default the shared AI_HOST_GUIDES */
|
|
47
|
+
hosts?: readonly AiHostGuide[];
|
|
48
|
+
/** Capability cards on the landing. @default the shared AI_CAPABILITIES */
|
|
49
|
+
capabilities?: readonly AiCapability[];
|
|
50
|
+
/** Permission reassurance copy on the landing. @default AI_PERMISSION_MODEL */
|
|
51
|
+
permissionModel?: string;
|
|
52
|
+
/** Message pasted into the assistant on the Conectar step. @default AI_CONNECT_PROMPT */
|
|
53
|
+
connectPrompt?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Re-check the live connection on the verify step's "Testar conexão" button —
|
|
56
|
+
* apps pass a router refresh (e.g. Next's `router.refresh`). @default a full
|
|
57
|
+
* `window.location.reload()`.
|
|
58
|
+
*/
|
|
59
|
+
onRetest?: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolved (defaults-applied) props threaded to the in-provider flow body. */
|
|
63
|
+
interface FlowProps {
|
|
64
|
+
endpointUrl: string;
|
|
65
|
+
connections: readonly AiConnection[];
|
|
66
|
+
hosts: readonly AiHostGuide[];
|
|
67
|
+
capabilities: readonly AiCapability[];
|
|
68
|
+
permissionModel: string;
|
|
69
|
+
connectPrompt: string;
|
|
70
|
+
onRetest: () => void;
|
|
71
|
+
devReset: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The flow body — inside the OnboardingProvider so it can read the selected host
|
|
76
|
+
* and pick the path. A host with a published `pluginUrl` gets the simplified
|
|
77
|
+
* Escolher → Instalar → Confirmar flow (no URL to copy); otherwise the full
|
|
78
|
+
* Escolher → Copiar URL → Configurar → Conectar → Confirmar flow.
|
|
79
|
+
*/
|
|
80
|
+
function AiOnboardingFlow(props: FlowProps): React.JSX.Element {
|
|
81
|
+
const { endpointUrl, connections, hosts, capabilities, permissionModel, connectPrompt, onRetest, devReset } =
|
|
82
|
+
props;
|
|
83
|
+
const { state } = useOnboarding();
|
|
84
|
+
const selectedHost = hosts.find((h) => h.id === state.data.selectedHost) ?? hosts[0]!;
|
|
85
|
+
|
|
86
|
+
const steps = buildFlowSteps({ host: selectedHost, hosts, endpointUrl, connectPrompt, connections, onRetest });
|
|
87
|
+
const connectedHostId = (state.data.connectedHost ?? state.data.selectedHost) as string | undefined;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<GuidedSection
|
|
91
|
+
steps={steps}
|
|
92
|
+
title="Conecte assistentes de IA à sua loja"
|
|
93
|
+
startLabel="Ver como conectar"
|
|
94
|
+
renderLanding={(start) => (
|
|
95
|
+
<AiLanding onStart={start} permissionModel={permissionModel} capabilities={capabilities} />
|
|
96
|
+
)}
|
|
97
|
+
configuredTitle={connectedTitle(connections, hosts, connectedHostId)}
|
|
98
|
+
configuredSummary={connectedSummary(connections)}
|
|
99
|
+
editLabel="Conectar IA"
|
|
100
|
+
completedContent={(nav) => <StatusBoard nav={nav} connections={connections} hosts={hosts} />}
|
|
101
|
+
devReset={devReset}
|
|
102
|
+
dataTestId="ai-onboarding"
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* URL-free, persisted onboarding for the AI/MCP integration — a five-step wizard
|
|
109
|
+
* (pick an assistant, copy the store URL, configure the connector, connect, then
|
|
110
|
+
* verify) whose position + chosen assistant are saved via `@12-apps/onboarding`, so
|
|
111
|
+
* a refresh resumes exactly where the owner left off. The live MCP connection
|
|
112
|
+
* signal drives the verify step and the completed status board.
|
|
113
|
+
*
|
|
114
|
+
* App-agnostic: the app supplies the persistence `store`, the `endpointUrl`, and
|
|
115
|
+
* the live `connections` (one per connected assistant); content (hosts,
|
|
116
|
+
* capabilities, copy) defaults to the shared guide but can be overridden per app.
|
|
117
|
+
*/
|
|
118
|
+
export function AiIntegrationOnboarding({
|
|
119
|
+
store,
|
|
120
|
+
endpointUrl,
|
|
121
|
+
initialState,
|
|
122
|
+
connections,
|
|
123
|
+
featureKey = DEFAULT_FEATURE_KEY,
|
|
124
|
+
devReset = false,
|
|
125
|
+
hosts = AI_HOST_GUIDES,
|
|
126
|
+
capabilities = AI_CAPABILITIES,
|
|
127
|
+
permissionModel = AI_PERMISSION_MODEL,
|
|
128
|
+
connectPrompt = AI_CONNECT_PROMPT,
|
|
129
|
+
onRetest = () => {
|
|
130
|
+
if (typeof window !== "undefined") window.location.reload();
|
|
131
|
+
},
|
|
132
|
+
}: AiIntegrationOnboardingProps): React.JSX.Element {
|
|
133
|
+
return (
|
|
134
|
+
<OnboardingProvider featureKey={featureKey} store={store} initialState={initialState}>
|
|
135
|
+
<AiOnboardingFlow
|
|
136
|
+
endpointUrl={endpointUrl}
|
|
137
|
+
connections={connections}
|
|
138
|
+
hosts={hosts}
|
|
139
|
+
capabilities={capabilities}
|
|
140
|
+
permissionModel={permissionModel}
|
|
141
|
+
connectPrompt={connectPrompt}
|
|
142
|
+
onRetest={onRetest}
|
|
143
|
+
devReset={devReset}
|
|
144
|
+
/>
|
|
145
|
+
</OnboardingProvider>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import AddLinkOutlinedIcon from "@mui/icons-material/AddLinkOutlined";
|
|
4
|
+
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
5
|
+
|
|
6
|
+
import { Button } from "@12-apps/ui/form/Button";
|
|
7
|
+
import { Box } from "@12-apps/ui/mui/Box";
|
|
8
|
+
import { Stack } from "@12-apps/ui/mui/Stack";
|
|
9
|
+
import { Text } from "@12-apps/ui/typography/Text";
|
|
10
|
+
|
|
11
|
+
import type { AiHostGuide } from "../guide";
|
|
12
|
+
import { HostBrandAvatar } from "./ai-icons";
|
|
13
|
+
|
|
14
|
+
/** One assistant's connection state. */
|
|
15
|
+
export interface HostStatus {
|
|
16
|
+
host: AiHostGuide;
|
|
17
|
+
connected: boolean;
|
|
18
|
+
/** Freeform activity line for a connected host (e.g. "ativo há 3 min"). */
|
|
19
|
+
detail?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A single green (connected) / red (to connect) status box for one assistant. */
|
|
23
|
+
function StatusBox({
|
|
24
|
+
status,
|
|
25
|
+
onConnect,
|
|
26
|
+
}: {
|
|
27
|
+
status: HostStatus;
|
|
28
|
+
onConnect: (hostId: string) => void;
|
|
29
|
+
}): React.JSX.Element {
|
|
30
|
+
const { host, connected, detail } = status;
|
|
31
|
+
return (
|
|
32
|
+
<Box
|
|
33
|
+
data-testid={`ai-status-${host.id}`}
|
|
34
|
+
data-connected={connected}
|
|
35
|
+
sx={{
|
|
36
|
+
display: "flex",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: 1.5,
|
|
39
|
+
p: 2,
|
|
40
|
+
borderRadius: 3,
|
|
41
|
+
border: 1,
|
|
42
|
+
borderColor: connected ? "success.main" : "error.main",
|
|
43
|
+
bgcolor: "background.paper",
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<HostBrandAvatar brand={host.brand} size={36} />
|
|
47
|
+
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
|
|
48
|
+
<Stack direction="row" spacing={0.75} alignItems="center">
|
|
49
|
+
<Text variant="body" weight="bold" as="span">
|
|
50
|
+
{host.label}
|
|
51
|
+
</Text>
|
|
52
|
+
{connected ? <CheckCircleIcon sx={{ fontSize: 18, color: "success.main" }} /> : null}
|
|
53
|
+
</Stack>
|
|
54
|
+
<Text variant="caption" as="p" color="secondary">
|
|
55
|
+
{connected ? (detail ?? "Conectado") : "Ainda não conectado"}
|
|
56
|
+
</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
{connected ? (
|
|
59
|
+
<Box
|
|
60
|
+
sx={{
|
|
61
|
+
px: 1,
|
|
62
|
+
py: 0.25,
|
|
63
|
+
borderRadius: 999,
|
|
64
|
+
bgcolor: "success.main",
|
|
65
|
+
color: "success.contrastText",
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<Text variant="caption" weight="bold" as="span">
|
|
69
|
+
Conectado
|
|
70
|
+
</Text>
|
|
71
|
+
</Box>
|
|
72
|
+
) : (
|
|
73
|
+
<Button
|
|
74
|
+
variant="outline"
|
|
75
|
+
size="sm"
|
|
76
|
+
onClick={() => onConnect(host.id)}
|
|
77
|
+
data-testid={`ai-status-connect-${host.id}`}
|
|
78
|
+
>
|
|
79
|
+
<AddLinkOutlinedIcon sx={{ fontSize: 16, mr: 0.5 }} />
|
|
80
|
+
Conectar
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</Box>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The completed-state status board for the AI integration: every assistant as a
|
|
89
|
+
* box — green when connected, red when still to connect (with a "Conectar"
|
|
90
|
+
* button that re-enters the guided flow for that host). Each connection is
|
|
91
|
+
* attributed to its provider (derived from the OAuth client + confirmed by
|
|
92
|
+
* `announceAiConnection`), so several assistants can show connected at once.
|
|
93
|
+
*/
|
|
94
|
+
export function AiStatusBoard({
|
|
95
|
+
statuses,
|
|
96
|
+
onConnect,
|
|
97
|
+
}: {
|
|
98
|
+
statuses: readonly HostStatus[];
|
|
99
|
+
onConnect: (hostId: string) => void;
|
|
100
|
+
}): React.JSX.Element {
|
|
101
|
+
return (
|
|
102
|
+
<Stack spacing={2} data-testid="ai-status-board">
|
|
103
|
+
<Box>
|
|
104
|
+
<Text variant="heading" size="sm" as="h2">
|
|
105
|
+
Assistentes conectados
|
|
106
|
+
</Text>
|
|
107
|
+
<Text variant="caption" as="p" color="secondary">
|
|
108
|
+
Em verde os que já operam a sua loja; em vermelho os que faltam conectar.
|
|
109
|
+
</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
<Box
|
|
112
|
+
sx={{
|
|
113
|
+
display: "grid",
|
|
114
|
+
gap: 2,
|
|
115
|
+
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{statuses.map((status) => (
|
|
119
|
+
<StatusBox key={status.host.id} status={status} onConnect={onConnect} />
|
|
120
|
+
))}
|
|
121
|
+
</Box>
|
|
122
|
+
</Stack>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type GuidedNav, type GuidedStep } from "@12-apps/onboarding";
|
|
4
|
+
|
|
5
|
+
import { providerForHostId, type AiHostGuide } from "../guide";
|
|
6
|
+
import { activeAgo, resolveHost, type AiConnection } from "./ai-connection-utils";
|
|
7
|
+
import {
|
|
8
|
+
ConfigureStageStep,
|
|
9
|
+
ConfigureStep,
|
|
10
|
+
ConfirmStep,
|
|
11
|
+
ConnectStep,
|
|
12
|
+
CopyUrlStep,
|
|
13
|
+
InstallStep,
|
|
14
|
+
} from "./ai-flow-steps";
|
|
15
|
+
import { AiStatusBoard, type HostStatus } from "./ai-status-board";
|
|
16
|
+
import { HostSelectStep } from "./host-select-step";
|
|
17
|
+
|
|
18
|
+
export type { AiConnection } from "./ai-connection-utils";
|
|
19
|
+
export { connectedSummary, connectedTitle } from "./ai-connection-utils";
|
|
20
|
+
|
|
21
|
+
/** The completed status board: green for each connected host, red for the rest. */
|
|
22
|
+
export function StatusBoard({
|
|
23
|
+
nav,
|
|
24
|
+
connections,
|
|
25
|
+
hosts,
|
|
26
|
+
}: {
|
|
27
|
+
nav: GuidedNav;
|
|
28
|
+
connections: readonly AiConnection[];
|
|
29
|
+
hosts: readonly AiHostGuide[];
|
|
30
|
+
}): React.JSX.Element {
|
|
31
|
+
const legacyHostId = (nav.data.connectedHost ?? nav.data.selectedHost) as string | undefined;
|
|
32
|
+
|
|
33
|
+
const statuses: HostStatus[] = hosts.map((host) => {
|
|
34
|
+
const provider = providerForHostId(host.id);
|
|
35
|
+
// Prefer a provider-attributed connection; fall back to a legacy null-host
|
|
36
|
+
// connection only for the host the owner completed the flow with.
|
|
37
|
+
const connection =
|
|
38
|
+
(provider ? connections.find((c) => c.host === provider) : undefined) ??
|
|
39
|
+
(host.id === legacyHostId ? connections.find((c) => c.host === null) : undefined) ??
|
|
40
|
+
null;
|
|
41
|
+
return {
|
|
42
|
+
host,
|
|
43
|
+
connected: connection !== null,
|
|
44
|
+
detail: connection ? activeAgo(connection.lastActiveAt) : undefined,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<AiStatusBoard
|
|
50
|
+
statuses={statuses}
|
|
51
|
+
onConnect={(hostId) =>
|
|
52
|
+
nav.goTo(hosts.find((h) => h.id === hostId)?.pluginUrl ? "install" : "copy", {
|
|
53
|
+
selectedHost: hostId,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The first step (host picker) — picking a card advances to the right path. */
|
|
61
|
+
function selectStep(hosts: readonly AiHostGuide[]): GuidedStep {
|
|
62
|
+
return {
|
|
63
|
+
id: "select",
|
|
64
|
+
label: "Escolher",
|
|
65
|
+
render: (nav: GuidedNav) => (
|
|
66
|
+
<HostSelectStep
|
|
67
|
+
hosts={hosts}
|
|
68
|
+
selectedId={(nav.data.selectedHost as string | undefined) ?? null}
|
|
69
|
+
onSelect={(hostId) =>
|
|
70
|
+
nav.goTo(hosts.find((h) => h.id === hostId)?.pluginUrl ? "install" : "copy", {
|
|
71
|
+
selectedHost: hostId,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The Copiar-URL step, shared by every manual flow. */
|
|
80
|
+
function copyStep(endpointUrl: string): GuidedStep {
|
|
81
|
+
return {
|
|
82
|
+
id: "copy",
|
|
83
|
+
label: "Copiar URL",
|
|
84
|
+
render: (nav: GuidedNav) => <CopyUrlStep nav={nav} endpointUrl={endpointUrl} />,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The middle steps for the full manual flow. A host with `configureStages`
|
|
90
|
+
* (e.g. ChatGPT) gets one step per stage — each with its own deep link — after
|
|
91
|
+
* Copiar URL; otherwise the default copy → configure → connect split.
|
|
92
|
+
*/
|
|
93
|
+
function manualMiddle(
|
|
94
|
+
host: AiHostGuide,
|
|
95
|
+
hosts: readonly AiHostGuide[],
|
|
96
|
+
endpointUrl: string,
|
|
97
|
+
connectPrompt: string,
|
|
98
|
+
): GuidedStep[] {
|
|
99
|
+
if (host.configureStages && host.configureStages.length > 0) {
|
|
100
|
+
return [
|
|
101
|
+
copyStep(endpointUrl),
|
|
102
|
+
...host.configureStages.map((stage) => ({
|
|
103
|
+
id: stage.id,
|
|
104
|
+
label: stage.label,
|
|
105
|
+
render: (nav: GuidedNav) => (
|
|
106
|
+
<ConfigureStageStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} stage={stage} />
|
|
107
|
+
),
|
|
108
|
+
})),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
copyStep(endpointUrl),
|
|
113
|
+
{
|
|
114
|
+
id: "configure",
|
|
115
|
+
label: "Configurar",
|
|
116
|
+
render: (nav: GuidedNav) => <ConfigureStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} />,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "connect",
|
|
120
|
+
label: "Conectar",
|
|
121
|
+
render: (nav: GuidedNav) => (
|
|
122
|
+
<ConnectStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} connectPrompt={connectPrompt} />
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The single middle step for the simplified plugin flow: install. */
|
|
129
|
+
function simpleMiddle(hosts: readonly AiHostGuide[], connectPrompt: string): GuidedStep[] {
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
id: "install",
|
|
133
|
+
label: "Instalar",
|
|
134
|
+
render: (nav: GuidedNav) => (
|
|
135
|
+
<InstallStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} connectPrompt={connectPrompt} />
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the wizard's steps for the selected host's path: the simplified
|
|
143
|
+
* `install` flow when the host has a published `pluginUrl`, the per-stage flow
|
|
144
|
+
* when it declares `configureStages`, otherwise the full copy → configure →
|
|
145
|
+
* connect flow. `select` and `confirm` bookend all of them.
|
|
146
|
+
*/
|
|
147
|
+
export function buildFlowSteps(opts: {
|
|
148
|
+
host: AiHostGuide;
|
|
149
|
+
hosts: readonly AiHostGuide[];
|
|
150
|
+
endpointUrl: string;
|
|
151
|
+
connectPrompt: string;
|
|
152
|
+
connections: readonly AiConnection[];
|
|
153
|
+
onRetest: () => void;
|
|
154
|
+
}): GuidedStep[] {
|
|
155
|
+
const { host, hosts, endpointUrl, connectPrompt, connections, onRetest } = opts;
|
|
156
|
+
const middle = host.pluginUrl
|
|
157
|
+
? simpleMiddle(hosts, connectPrompt)
|
|
158
|
+
: manualMiddle(host, hosts, endpointUrl, connectPrompt);
|
|
159
|
+
const confirm: GuidedStep = {
|
|
160
|
+
id: "confirm",
|
|
161
|
+
label: "Confirmar",
|
|
162
|
+
render: (nav: GuidedNav) => (
|
|
163
|
+
<ConfirmStep nav={nav} connections={connections} hosts={hosts} onRetest={onRetest} />
|
|
164
|
+
),
|
|
165
|
+
};
|
|
166
|
+
return [selectStep(hosts), ...middle, confirm];
|
|
167
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Box } from "@12-apps/ui/mui/Box";
|
|
2
|
+
import { Stack } from "@12-apps/ui/mui/Stack";
|
|
3
|
+
import { Text } from "@12-apps/ui/typography/Text";
|
|
4
|
+
|
|
5
|
+
/** One entry of a landing's trust/feature strip. */
|
|
6
|
+
export interface FeatureBadgeItem {
|
|
7
|
+
icon: React.ReactNode;
|
|
8
|
+
label: string;
|
|
9
|
+
caption: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One item of an onboarding landing's trust/feature strip: a paper icon tile
|
|
14
|
+
* (primary-coloured glyph) with a two-line label + caption.
|
|
15
|
+
*/
|
|
16
|
+
export function FeatureBadge({ icon, label, caption }: FeatureBadgeItem): React.JSX.Element {
|
|
17
|
+
return (
|
|
18
|
+
<Stack direction="row" spacing={1.5} alignItems="flex-start">
|
|
19
|
+
<Box
|
|
20
|
+
sx={{
|
|
21
|
+
width: 36,
|
|
22
|
+
height: 36,
|
|
23
|
+
borderRadius: 2,
|
|
24
|
+
display: "flex",
|
|
25
|
+
alignItems: "center",
|
|
26
|
+
justifyContent: "center",
|
|
27
|
+
bgcolor: "background.paper",
|
|
28
|
+
color: "primary.main",
|
|
29
|
+
border: 1,
|
|
30
|
+
borderColor: "divider",
|
|
31
|
+
flexShrink: 0,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{icon}
|
|
35
|
+
</Box>
|
|
36
|
+
<Box>
|
|
37
|
+
<Text variant="body" size="sm" weight="bold" as="p">
|
|
38
|
+
{label}
|
|
39
|
+
</Text>
|
|
40
|
+
<Text variant="caption" color="secondary" as="p">
|
|
41
|
+
{caption}
|
|
42
|
+
</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
</Stack>
|
|
45
|
+
);
|
|
46
|
+
}
|