conversokit 0.1.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/LICENSE +201 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +22 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +263 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +75 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +86 -0
- package/dist/utils/copy.d.ts +8 -0
- package/dist/utils/copy.js +59 -0
- package/package.json +54 -0
- package/templates/base/.env.example +10 -0
- package/templates/base/README.md +36 -0
- package/templates/base/apps/mcp-server/package.json +27 -0
- package/templates/base/apps/mcp-server/src/index.ts +61 -0
- package/templates/base/apps/mcp-server/src/tools/index.ts +5 -0
- package/templates/base/apps/mcp-server/tsconfig.json +15 -0
- package/templates/base/apps/widget-ui/index.html +12 -0
- package/templates/base/apps/widget-ui/package.json +26 -0
- package/templates/base/apps/widget-ui/src/App.tsx +28 -0
- package/templates/base/apps/widget-ui/src/main.tsx +9 -0
- package/templates/base/apps/widget-ui/tsconfig.json +15 -0
- package/templates/base/apps/widget-ui/vite.config.ts +7 -0
- package/templates/base/package.json +19 -0
- package/templates/base/pnpm-workspace.yaml +2 -0
- package/templates/booking/apps/mcp-server/src/tools/cancelReservation.ts +28 -0
- package/templates/booking/apps/mcp-server/src/tools/createReservation.ts +38 -0
- package/templates/booking/apps/mcp-server/src/tools/getAvailability.ts +21 -0
- package/templates/booking/apps/mcp-server/src/tools/index.ts +10 -0
- package/templates/booking/apps/widget-ui/src/App.tsx +79 -0
- package/templates/commerce/apps/mcp-server/src/tools/index.ts +5 -0
- package/templates/commerce/apps/mcp-server/src/tools/searchProducts.ts +29 -0
- package/templates/commerce/apps/widget-ui/src/App.tsx +75 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getAlerts.ts +18 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getAnalyticsPanel.ts +17 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getKpis.ts +13 -0
- package/templates/dashboard/apps/mcp-server/src/tools/getTrendSeries.ts +20 -0
- package/templates/dashboard/apps/mcp-server/src/tools/index.ts +14 -0
- package/templates/dashboard/apps/widget-ui/src/App.tsx +66 -0
- package/templates/deploy/docker/.dockerignore +10 -0
- package/templates/deploy/docker/Dockerfile +27 -0
- package/templates/deploy/docker/docker-compose.yml +31 -0
- package/templates/deploy/railway/Procfile +1 -0
- package/templates/deploy/railway/railway.json +14 -0
- package/templates/deploy/vercel/api/mcp.ts +10 -0
- package/templates/deploy/vercel/vercel.json +19 -0
- package/templates/saas-onboarding/apps/mcp-server/src/tools/index.ts +4 -0
- package/templates/saas-onboarding/apps/mcp-server/src/tools/submitLead.ts +20 -0
- package/templates/saas-onboarding/apps/widget-ui/src/App.tsx +56 -0
- package/templates/travel/apps/mcp-server/src/tools/getItinerary.ts +17 -0
- package/templates/travel/apps/mcp-server/src/tools/index.ts +12 -0
- package/templates/travel/apps/mcp-server/src/tools/listDestinations.ts +28 -0
- package/templates/travel/apps/mcp-server/src/tools/searchFlights.ts +22 -0
- package/templates/travel/apps/mcp-server/src/tools/searchHotels.ts +29 -0
- package/templates/travel/apps/widget-ui/src/App.tsx +89 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AddToCartPanel,
|
|
4
|
+
CTABanner,
|
|
5
|
+
CheckoutSummary,
|
|
6
|
+
ConsentBanner,
|
|
7
|
+
ProductCarousel,
|
|
8
|
+
type ProductCardProps
|
|
9
|
+
} from '@conversokit/widgets';
|
|
10
|
+
import {
|
|
11
|
+
EXAMPLE_CHECKOUT_SUMMARY,
|
|
12
|
+
type CartItem
|
|
13
|
+
} from '@conversokit/shared';
|
|
14
|
+
import { ThemeProvider, commerceTheme } from '@conversokit/themes';
|
|
15
|
+
import { BridgeProvider, useBridge } from '@conversokit/bridge';
|
|
16
|
+
|
|
17
|
+
const Catalog: React.FC = () => {
|
|
18
|
+
const bridge = useBridge();
|
|
19
|
+
const [items, setItems] = useState<ProductCardProps[]>([]);
|
|
20
|
+
const [cart, setCart] = useState<CartItem[]>([]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
bridge
|
|
24
|
+
.callTool('search_products', { query: '', limit: 10 })
|
|
25
|
+
.then((r) => setItems((r as { items: ProductCardProps[] }).items))
|
|
26
|
+
.catch(console.error);
|
|
27
|
+
}, [bridge]);
|
|
28
|
+
|
|
29
|
+
const checkout = async () => {
|
|
30
|
+
await bridge.callTool('set_cart', { items: cart, currency: 'USD' });
|
|
31
|
+
const result = (await bridge.callTool('create_checkout', {
|
|
32
|
+
successUrl: window.location.origin + '/?status=success',
|
|
33
|
+
cancelUrl: window.location.origin + '/?status=cancelled'
|
|
34
|
+
})) as { url: string };
|
|
35
|
+
window.location.href = result.url;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<ProductCarousel items={items} />
|
|
41
|
+
{items[0] && (
|
|
42
|
+
<AddToCartPanel
|
|
43
|
+
product={items[0]}
|
|
44
|
+
onAdd={(item) => setCart((c) => [...c, item])}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
{cart.length > 0 && (
|
|
48
|
+
<CheckoutSummary
|
|
49
|
+
summary={{ ...EXAMPLE_CHECKOUT_SUMMARY, items: cart }}
|
|
50
|
+
onCheckout={checkout}
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const App: React.FC = () => (
|
|
58
|
+
<BridgeProvider baseUrl="http://localhost:3000">
|
|
59
|
+
<ThemeProvider
|
|
60
|
+
theme={commerceTheme}
|
|
61
|
+
style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
|
|
62
|
+
>
|
|
63
|
+
<h1>Welcome to <% projectName %></h1>
|
|
64
|
+
<CTABanner
|
|
65
|
+
title="Commerce demo"
|
|
66
|
+
description="Search → add to cart → checkout via Stripe (or MockPaymentProvider in dev)."
|
|
67
|
+
/>
|
|
68
|
+
<ConsentBanner scopes={['analytics']}>
|
|
69
|
+
<Catalog />
|
|
70
|
+
</ConsentBanner>
|
|
71
|
+
</ThemeProvider>
|
|
72
|
+
</BridgeProvider>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export default App;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { EXAMPLE_ALERTS, alertSchema, defineTool } from '@conversokit/shared';
|
|
3
|
+
|
|
4
|
+
export const getAlertsTool = defineTool({
|
|
5
|
+
name: 'get_alerts',
|
|
6
|
+
description: 'Return active operational alerts.',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
severity: z.enum(['info', 'warning', 'critical']).optional()
|
|
9
|
+
}),
|
|
10
|
+
outputSchema: z.object({ items: z.array(alertSchema) }),
|
|
11
|
+
permissions: { requiresAuth: false },
|
|
12
|
+
async handler(input) {
|
|
13
|
+
const items = input.severity
|
|
14
|
+
? EXAMPLE_ALERTS.filter((a) => a.severity === input.severity)
|
|
15
|
+
: EXAMPLE_ALERTS;
|
|
16
|
+
return { items };
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
EXAMPLE_ANALYTICS_PANEL,
|
|
4
|
+
analyticsPanelSchema,
|
|
5
|
+
defineTool
|
|
6
|
+
} from '@conversokit/shared';
|
|
7
|
+
|
|
8
|
+
export const getAnalyticsPanelTool = defineTool({
|
|
9
|
+
name: 'get_analytics_panel',
|
|
10
|
+
description: 'Return the assembled analytics panel (KPIs + trend series).',
|
|
11
|
+
inputSchema: z.object({}),
|
|
12
|
+
outputSchema: z.object({ panel: analyticsPanelSchema }),
|
|
13
|
+
permissions: { requiresAuth: false },
|
|
14
|
+
async handler() {
|
|
15
|
+
return { panel: EXAMPLE_ANALYTICS_PANEL };
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { EXAMPLE_KPIS, defineTool, kpiSchema } from '@conversokit/shared';
|
|
3
|
+
|
|
4
|
+
export const getKpisTool = defineTool({
|
|
5
|
+
name: 'get_kpis',
|
|
6
|
+
description: 'Return the headline KPIs for the dashboard.',
|
|
7
|
+
inputSchema: z.object({}),
|
|
8
|
+
outputSchema: z.object({ items: z.array(kpiSchema) }),
|
|
9
|
+
permissions: { requiresAuth: false },
|
|
10
|
+
async handler() {
|
|
11
|
+
return { items: EXAMPLE_KPIS };
|
|
12
|
+
}
|
|
13
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
EXAMPLE_TREND_SERIES,
|
|
4
|
+
defineTool,
|
|
5
|
+
trendSeriesSchema
|
|
6
|
+
} from '@conversokit/shared';
|
|
7
|
+
|
|
8
|
+
export const getTrendSeriesTool = defineTool({
|
|
9
|
+
name: 'get_trend_series',
|
|
10
|
+
description: 'Return a time-series trend for a given metric.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
metric: z.string().optional(),
|
|
13
|
+
range: z.enum(['7d', '30d', '12w', '12m']).optional()
|
|
14
|
+
}),
|
|
15
|
+
outputSchema: z.object({ series: trendSeriesSchema }),
|
|
16
|
+
permissions: { requiresAuth: false },
|
|
17
|
+
async handler() {
|
|
18
|
+
return { series: EXAMPLE_TREND_SERIES };
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Tool } from '@conversokit/shared';
|
|
2
|
+
import { getKpisTool } from './getKpis.js';
|
|
3
|
+
import { getTrendSeriesTool } from './getTrendSeries.js';
|
|
4
|
+
import { getAnalyticsPanelTool } from './getAnalyticsPanel.js';
|
|
5
|
+
import { getAlertsTool } from './getAlerts.js';
|
|
6
|
+
|
|
7
|
+
// Starter overlay: dashboard tools are unauthenticated for the demo.
|
|
8
|
+
// In production, set permissions.requiresAuth = true on each and wire JWT/Clerk.
|
|
9
|
+
export const tools: Tool[] = [
|
|
10
|
+
getKpisTool,
|
|
11
|
+
getTrendSeriesTool,
|
|
12
|
+
getAnalyticsPanelTool,
|
|
13
|
+
getAlertsTool
|
|
14
|
+
];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AlertFeed,
|
|
4
|
+
AnalyticsPanel,
|
|
5
|
+
CTABanner
|
|
6
|
+
} from '@conversokit/widgets';
|
|
7
|
+
import {
|
|
8
|
+
type Alert,
|
|
9
|
+
type AnalyticsPanel as AnalyticsPanelData
|
|
10
|
+
} from '@conversokit/shared';
|
|
11
|
+
import { ThemeProvider, enterpriseTheme } from '@conversokit/themes';
|
|
12
|
+
import { BridgeProvider, useBridge } from '@conversokit/bridge';
|
|
13
|
+
|
|
14
|
+
const Dashboard: React.FC = () => {
|
|
15
|
+
const bridge = useBridge();
|
|
16
|
+
const [panel, setPanel] = useState<AnalyticsPanelData | null>(null);
|
|
17
|
+
const [alerts, setAlerts] = useState<Alert[]>([]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
Promise.all([
|
|
21
|
+
bridge.callTool('get_analytics_panel', {}) as Promise<{
|
|
22
|
+
panel: AnalyticsPanelData;
|
|
23
|
+
}>,
|
|
24
|
+
bridge.callTool('get_alerts', {}) as Promise<{ items: Alert[] }>
|
|
25
|
+
])
|
|
26
|
+
.then(([p, a]) => {
|
|
27
|
+
setPanel(p.panel);
|
|
28
|
+
setAlerts(a.items);
|
|
29
|
+
})
|
|
30
|
+
.catch(console.error);
|
|
31
|
+
}, [bridge]);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{panel && <AnalyticsPanel panel={panel} />}
|
|
36
|
+
<h3>Alerts</h3>
|
|
37
|
+
<AlertFeed
|
|
38
|
+
alerts={alerts}
|
|
39
|
+
onAcknowledge={(a) =>
|
|
40
|
+
setAlerts((current) => current.filter((x) => x.id !== a.id))
|
|
41
|
+
}
|
|
42
|
+
/>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const App: React.FC = () => {
|
|
48
|
+
const apiKey = (import.meta.env?.VITE_CONVERSOKIT_API_KEY as string) || undefined;
|
|
49
|
+
return (
|
|
50
|
+
<BridgeProvider baseUrl="http://localhost:3000" apiKey={apiKey}>
|
|
51
|
+
<ThemeProvider
|
|
52
|
+
theme={enterpriseTheme}
|
|
53
|
+
style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
|
|
54
|
+
>
|
|
55
|
+
<h1>Welcome to <% projectName %></h1>
|
|
56
|
+
<CTABanner
|
|
57
|
+
title="Internal dashboard"
|
|
58
|
+
description="Set CONVERSOKIT_API_KEYS in apps/mcp-server/.env and pass it via VITE_CONVERSOKIT_API_KEY."
|
|
59
|
+
/>
|
|
60
|
+
<Dashboard />
|
|
61
|
+
</ThemeProvider>
|
|
62
|
+
</BridgeProvider>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default App;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
FROM node:20-alpine AS base
|
|
4
|
+
RUN corepack enable && corepack prepare pnpm@9 --activate
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
FROM base AS deps
|
|
8
|
+
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml turbo.json ./
|
|
9
|
+
COPY apps/mcp-server/package.json ./apps/mcp-server/
|
|
10
|
+
COPY packages ./packages
|
|
11
|
+
RUN pnpm install --frozen-lockfile --prod=false
|
|
12
|
+
|
|
13
|
+
FROM deps AS build
|
|
14
|
+
COPY . .
|
|
15
|
+
RUN pnpm -w build
|
|
16
|
+
|
|
17
|
+
FROM base AS runtime
|
|
18
|
+
ENV NODE_ENV=production
|
|
19
|
+
ENV PORT=3000
|
|
20
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
21
|
+
COPY --from=build /app/packages ./packages
|
|
22
|
+
COPY --from=build /app/apps/mcp-server/dist ./apps/mcp-server/dist
|
|
23
|
+
COPY --from=build /app/apps/mcp-server/package.json ./apps/mcp-server/
|
|
24
|
+
EXPOSE 3000
|
|
25
|
+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
|
26
|
+
CMD wget -q -O - http://localhost:3000/health || exit 1
|
|
27
|
+
CMD ["node", "apps/mcp-server/dist/index.js"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
services:
|
|
2
|
+
mcp-server:
|
|
3
|
+
build: .
|
|
4
|
+
image: <% projectName %>:latest
|
|
5
|
+
ports:
|
|
6
|
+
- "3000:3000"
|
|
7
|
+
env_file:
|
|
8
|
+
- .env
|
|
9
|
+
environment:
|
|
10
|
+
NODE_ENV: production
|
|
11
|
+
PORT: 3000
|
|
12
|
+
restart: unless-stopped
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:3000/health"]
|
|
15
|
+
interval: 30s
|
|
16
|
+
timeout: 5s
|
|
17
|
+
retries: 3
|
|
18
|
+
# Uncomment to add a Postgres for users not on Supabase.
|
|
19
|
+
# postgres:
|
|
20
|
+
# image: postgres:16-alpine
|
|
21
|
+
# environment:
|
|
22
|
+
# POSTGRES_USER: conversokit
|
|
23
|
+
# POSTGRES_PASSWORD: conversokit
|
|
24
|
+
# POSTGRES_DB: conversokit
|
|
25
|
+
# ports:
|
|
26
|
+
# - "5432:5432"
|
|
27
|
+
# volumes:
|
|
28
|
+
# - postgres_data:/var/lib/postgresql/data
|
|
29
|
+
|
|
30
|
+
# volumes:
|
|
31
|
+
# postgres_data:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: pnpm --filter mcp-server start
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://railway.app/railway.schema.json",
|
|
3
|
+
"build": {
|
|
4
|
+
"builder": "NIXPACKS",
|
|
5
|
+
"buildCommand": "pnpm install --frozen-lockfile && pnpm -w build"
|
|
6
|
+
},
|
|
7
|
+
"deploy": {
|
|
8
|
+
"startCommand": "pnpm --filter mcp-server start",
|
|
9
|
+
"healthcheckPath": "/health",
|
|
10
|
+
"healthcheckTimeout": 30,
|
|
11
|
+
"restartPolicyType": "ON_FAILURE",
|
|
12
|
+
"restartPolicyMaxRetries": 3
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Vercel serverless adapter for the ConversoKit MCP server.
|
|
2
|
+
// Re-exports the express app from `apps/mcp-server` so all routes (/tools,
|
|
3
|
+
// /auth, /webhooks, /userdata, /admin, /health) are answered by one function.
|
|
4
|
+
//
|
|
5
|
+
// Make sure `apps/mcp-server/src/index.ts` exports the app:
|
|
6
|
+
// export { app };
|
|
7
|
+
// then this file imports it as `app`.
|
|
8
|
+
import app from '../apps/mcp-server/dist/index.js';
|
|
9
|
+
|
|
10
|
+
export default app;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"buildCommand": "pnpm -w build",
|
|
4
|
+
"installCommand": "pnpm -w install --frozen-lockfile",
|
|
5
|
+
"outputDirectory": "apps/widget-ui/dist",
|
|
6
|
+
"rewrites": [
|
|
7
|
+
{ "source": "/tools/:path*", "destination": "/api/mcp" },
|
|
8
|
+
{ "source": "/auth/:path*", "destination": "/api/mcp" },
|
|
9
|
+
{ "source": "/userdata/:path*", "destination": "/api/mcp" },
|
|
10
|
+
{ "source": "/webhooks/:path*", "destination": "/api/mcp" },
|
|
11
|
+
{ "source": "/admin/:path*", "destination": "/api/mcp" },
|
|
12
|
+
{ "source": "/health", "destination": "/api/mcp" }
|
|
13
|
+
],
|
|
14
|
+
"functions": {
|
|
15
|
+
"api/mcp.ts": {
|
|
16
|
+
"runtime": "nodejs20.x"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool, leadSubmissionSchema } from '@conversokit/shared';
|
|
3
|
+
|
|
4
|
+
export const submitLeadTool = defineTool({
|
|
5
|
+
name: 'submit_lead',
|
|
6
|
+
description:
|
|
7
|
+
'Submit a qualified lead. Replace this stub with a CRM upsert (HubSpot, Salesforce, etc.) when wiring real persistence.',
|
|
8
|
+
inputSchema: leadSubmissionSchema,
|
|
9
|
+
outputSchema: z.object({
|
|
10
|
+
leadId: z.string(),
|
|
11
|
+
provider: z.string()
|
|
12
|
+
}),
|
|
13
|
+
permissions: { requiresAuth: false, requiresConsent: true },
|
|
14
|
+
async handler() {
|
|
15
|
+
return {
|
|
16
|
+
leadId: `lead_${Math.random().toString(36).slice(2)}`,
|
|
17
|
+
provider: 'mock'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
CTABanner,
|
|
4
|
+
ConsentBanner,
|
|
5
|
+
MultiStepForm
|
|
6
|
+
} from '@conversokit/widgets';
|
|
7
|
+
import { EXAMPLE_LEAD_FORM } from '@conversokit/shared';
|
|
8
|
+
import { ThemeProvider, modernSaasTheme } from '@conversokit/themes';
|
|
9
|
+
import { BridgeProvider, useBridge } from '@conversokit/bridge';
|
|
10
|
+
|
|
11
|
+
const Onboarding: React.FC = () => {
|
|
12
|
+
const bridge = useBridge();
|
|
13
|
+
const [done, setDone] = useState<{ leadId: string; provider: string } | null>(
|
|
14
|
+
null
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (done) {
|
|
18
|
+
return (
|
|
19
|
+
<CTABanner
|
|
20
|
+
title="Thanks!"
|
|
21
|
+
description={`Lead ${done.leadId} synced via ${done.provider}.`}
|
|
22
|
+
variant="success"
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<MultiStepForm
|
|
29
|
+
form={EXAMPLE_LEAD_FORM}
|
|
30
|
+
onComplete={async (values) => {
|
|
31
|
+
const r = (await bridge.callTool('submit_lead', {
|
|
32
|
+
formId: EXAMPLE_LEAD_FORM.id,
|
|
33
|
+
values,
|
|
34
|
+
submittedAt: new Date().toISOString()
|
|
35
|
+
})) as { leadId: string; provider: string };
|
|
36
|
+
setDone(r);
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const App: React.FC = () => (
|
|
43
|
+
<BridgeProvider baseUrl="http://localhost:3000">
|
|
44
|
+
<ThemeProvider
|
|
45
|
+
theme={modernSaasTheme}
|
|
46
|
+
style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
|
|
47
|
+
>
|
|
48
|
+
<h1>Welcome to <% projectName %></h1>
|
|
49
|
+
<ConsentBanner scopes={['personalData', 'marketing']}>
|
|
50
|
+
<Onboarding />
|
|
51
|
+
</ConsentBanner>
|
|
52
|
+
</ThemeProvider>
|
|
53
|
+
</BridgeProvider>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export default App;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
EXAMPLE_ITINERARY,
|
|
4
|
+
defineTool,
|
|
5
|
+
itinerarySchema
|
|
6
|
+
} from '@conversokit/shared';
|
|
7
|
+
|
|
8
|
+
export const getItineraryTool = defineTool({
|
|
9
|
+
name: 'get_itinerary',
|
|
10
|
+
description: 'Return a saved travel itinerary by id.',
|
|
11
|
+
inputSchema: z.object({ itineraryId: z.string() }),
|
|
12
|
+
outputSchema: z.object({ itinerary: itinerarySchema }),
|
|
13
|
+
permissions: { requiresAuth: false },
|
|
14
|
+
async handler() {
|
|
15
|
+
return { itinerary: EXAMPLE_ITINERARY };
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Tool } from '@conversokit/shared';
|
|
2
|
+
import { listDestinationsTool } from './listDestinations.js';
|
|
3
|
+
import { searchHotelsTool } from './searchHotels.js';
|
|
4
|
+
import { searchFlightsTool } from './searchFlights.js';
|
|
5
|
+
import { getItineraryTool } from './getItinerary.js';
|
|
6
|
+
|
|
7
|
+
export const tools: Tool[] = [
|
|
8
|
+
listDestinationsTool,
|
|
9
|
+
searchHotelsTool,
|
|
10
|
+
searchFlightsTool,
|
|
11
|
+
getItineraryTool
|
|
12
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
EXAMPLE_DESTINATIONS,
|
|
4
|
+
defineTool,
|
|
5
|
+
destinationSchema
|
|
6
|
+
} from '@conversokit/shared';
|
|
7
|
+
|
|
8
|
+
export const listDestinationsTool = defineTool({
|
|
9
|
+
name: 'list_destinations',
|
|
10
|
+
description: 'Return curated destination recommendations.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
query: z.string().optional(),
|
|
13
|
+
limit: z.number().int().min(1).max(50).optional()
|
|
14
|
+
}),
|
|
15
|
+
outputSchema: z.object({ items: z.array(destinationSchema) }),
|
|
16
|
+
permissions: { requiresAuth: false },
|
|
17
|
+
async handler(input) {
|
|
18
|
+
const lower = input.query?.toLowerCase();
|
|
19
|
+
const matches = lower
|
|
20
|
+
? EXAMPLE_DESTINATIONS.filter(
|
|
21
|
+
(d) =>
|
|
22
|
+
d.name.toLowerCase().includes(lower) ||
|
|
23
|
+
(d.country?.toLowerCase().includes(lower) ?? false)
|
|
24
|
+
)
|
|
25
|
+
: EXAMPLE_DESTINATIONS;
|
|
26
|
+
return { items: matches.slice(0, input.limit ?? 20) };
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
EXAMPLE_FLIGHT,
|
|
4
|
+
defineTool,
|
|
5
|
+
flightSummarySchema
|
|
6
|
+
} from '@conversokit/shared';
|
|
7
|
+
|
|
8
|
+
export const searchFlightsTool = defineTool({
|
|
9
|
+
name: 'search_flights',
|
|
10
|
+
description: 'Search flights by IATA origin and destination.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
origin: z.string(),
|
|
13
|
+
destination: z.string(),
|
|
14
|
+
departDate: z.string().optional(),
|
|
15
|
+
returnDate: z.string().optional()
|
|
16
|
+
}),
|
|
17
|
+
outputSchema: z.object({ items: z.array(flightSummarySchema) }),
|
|
18
|
+
permissions: { requiresAuth: false },
|
|
19
|
+
async handler() {
|
|
20
|
+
return { items: [EXAMPLE_FLIGHT] };
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
EXAMPLE_HOTELS,
|
|
4
|
+
defineTool,
|
|
5
|
+
hotelSchema
|
|
6
|
+
} from '@conversokit/shared';
|
|
7
|
+
|
|
8
|
+
export const searchHotelsTool = defineTool({
|
|
9
|
+
name: 'search_hotels',
|
|
10
|
+
description: 'Search hotels by free-text query (matches city or hotel name).',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
query: z.string(),
|
|
13
|
+
limit: z.number().int().min(1).max(50).optional()
|
|
14
|
+
}),
|
|
15
|
+
outputSchema: z.object({ items: z.array(hotelSchema) }),
|
|
16
|
+
permissions: { requiresAuth: false },
|
|
17
|
+
async handler(input) {
|
|
18
|
+
const lower = input.query.toLowerCase();
|
|
19
|
+
const matches = lower
|
|
20
|
+
? EXAMPLE_HOTELS.filter(
|
|
21
|
+
(h) =>
|
|
22
|
+
h.name.toLowerCase().includes(lower) ||
|
|
23
|
+
h.city.toLowerCase().includes(lower) ||
|
|
24
|
+
(h.country?.toLowerCase().includes(lower) ?? false)
|
|
25
|
+
)
|
|
26
|
+
: EXAMPLE_HOTELS;
|
|
27
|
+
return { items: matches.slice(0, input.limit ?? 10) };
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
CTABanner,
|
|
4
|
+
ConsentBanner,
|
|
5
|
+
DestinationRecommendations,
|
|
6
|
+
FlightSummary,
|
|
7
|
+
HotelCard,
|
|
8
|
+
ItineraryTimeline
|
|
9
|
+
} from '@conversokit/widgets';
|
|
10
|
+
import {
|
|
11
|
+
type Destination,
|
|
12
|
+
type FlightSummary as FlightSummaryData,
|
|
13
|
+
type Hotel,
|
|
14
|
+
type Itinerary
|
|
15
|
+
} from '@conversokit/shared';
|
|
16
|
+
import { ThemeProvider, travelTheme } from '@conversokit/themes';
|
|
17
|
+
import { BridgeProvider, useBridge } from '@conversokit/bridge';
|
|
18
|
+
|
|
19
|
+
const TravelPlanner: React.FC = () => {
|
|
20
|
+
const bridge = useBridge();
|
|
21
|
+
const [destinations, setDestinations] = useState<Destination[]>([]);
|
|
22
|
+
const [hotels, setHotels] = useState<Hotel[]>([]);
|
|
23
|
+
const [flight, setFlight] = useState<FlightSummaryData | null>(null);
|
|
24
|
+
const [itinerary, setItinerary] = useState<Itinerary | null>(null);
|
|
25
|
+
const [picked, setPicked] = useState<Destination | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
bridge
|
|
29
|
+
.callTool('list_destinations', {})
|
|
30
|
+
.then((r) => setDestinations((r as { items: Destination[] }).items))
|
|
31
|
+
.catch(console.error);
|
|
32
|
+
}, [bridge]);
|
|
33
|
+
|
|
34
|
+
const pick = async (dest: Destination) => {
|
|
35
|
+
setPicked(dest);
|
|
36
|
+
const [h, f] = await Promise.all([
|
|
37
|
+
bridge.callTool('search_hotels', { query: dest.name }) as Promise<{
|
|
38
|
+
items: Hotel[];
|
|
39
|
+
}>,
|
|
40
|
+
bridge.callTool('search_flights', {
|
|
41
|
+
origin: 'BER',
|
|
42
|
+
destination: dest.name.slice(0, 3).toUpperCase()
|
|
43
|
+
}) as Promise<{ items: FlightSummaryData[] }>
|
|
44
|
+
]);
|
|
45
|
+
setHotels(h.items);
|
|
46
|
+
setFlight(f.items[0] ?? null);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const buildItinerary = async () => {
|
|
50
|
+
const r = (await bridge.callTool('get_itinerary', {
|
|
51
|
+
itineraryId: 'demo'
|
|
52
|
+
})) as { itinerary: Itinerary };
|
|
53
|
+
setItinerary(r.itinerary);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<DestinationRecommendations destinations={destinations} onSelect={pick} />
|
|
59
|
+
{picked && (
|
|
60
|
+
<CTABanner
|
|
61
|
+
title={`Plan a trip to ${picked.name}`}
|
|
62
|
+
primaryLabel="Build itinerary"
|
|
63
|
+
onPrimary={buildItinerary}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
{hotels.map((h) => (
|
|
67
|
+
<HotelCard key={h.id} hotel={h} />
|
|
68
|
+
))}
|
|
69
|
+
{flight && <FlightSummary flight={flight} />}
|
|
70
|
+
{itinerary && <ItineraryTimeline itinerary={itinerary} />}
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const App: React.FC = () => (
|
|
76
|
+
<BridgeProvider baseUrl="http://localhost:3000">
|
|
77
|
+
<ThemeProvider
|
|
78
|
+
theme={travelTheme}
|
|
79
|
+
style={{ minHeight: '100vh', padding: 'var(--ck-spacing-4)' }}
|
|
80
|
+
>
|
|
81
|
+
<h1>Welcome to <% projectName %></h1>
|
|
82
|
+
<ConsentBanner scopes={['analytics']}>
|
|
83
|
+
<TravelPlanner />
|
|
84
|
+
</ConsentBanner>
|
|
85
|
+
</ThemeProvider>
|
|
86
|
+
</BridgeProvider>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
export default App;
|