@speak2web/create-chat 1.0.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 +21 -0
- package/README.md +68 -0
- package/index.js +188 -0
- package/package.json +35 -0
- package/templates/next-app/README.md +42 -0
- package/templates/next-app/app/chat-widget.tsx +62 -0
- package/templates/next-app/app/layout.tsx +23 -0
- package/templates/next-app/app/page.tsx +43 -0
- package/templates/next-app/gitignore +12 -0
- package/templates/next-app/lib/config.ts +20 -0
- package/templates/next-app/next-env.d.ts +5 -0
- package/templates/next-app/next.config.mjs +8 -0
- package/templates/next-app/package.json +27 -0
- package/templates/next-app/tsconfig.json +23 -0
- package/templates/vite-react/README.md +28 -0
- package/templates/vite-react/gitignore +10 -0
- package/templates/vite-react/index.html +12 -0
- package/templates/vite-react/package.json +24 -0
- package/templates/vite-react/src/main.jsx +36 -0
- package/templates/vite-react/vite.config.js +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 speak2web
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @speak2web/create-chat
|
|
2
|
+
|
|
3
|
+
Scaffold a **pre-wired Speak2Web AIN/AIC chat app** against your headless bridge — so day-one is
|
|
4
|
+
a working app, not hand-wiring `createClient` + provider + widget + env vars (SPEAK-234).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm create @speak2web/chat my-app
|
|
8
|
+
# or
|
|
9
|
+
npx @speak2web/create-chat my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Interactive by default — it prompts for the template and your bridge values, then writes them to
|
|
13
|
+
the project's env file. Or pass flags for a non-interactive run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @speak2web/create-chat my-app \
|
|
17
|
+
--template next-app \
|
|
18
|
+
--api-base https://your-site.com/wp-json/s2w/v1 \
|
|
19
|
+
--site-id pub_abc123 \
|
|
20
|
+
--yes
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Options
|
|
24
|
+
|
|
25
|
+
| Flag | Description |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `<project-name>` | Positional. Directory to create (must not exist). |
|
|
28
|
+
| `-t, --template` | `next-app` (default) or `vite-react`. |
|
|
29
|
+
| `--api-base` | Bridge REST base, e.g. `https://site.com/wp-json/s2w/v1`. |
|
|
30
|
+
| `--token-endpoint` | JWT issuer. Defaults to `<api-base>/auth/jwt`. |
|
|
31
|
+
| `--site-id` | Public site id from **AI-Gateway → Headless / JWT** (`pub_…`). |
|
|
32
|
+
| `-y, --yes` | Skip prompts; use flags/defaults (also auto-on when there's no TTY). |
|
|
33
|
+
| `-h, --help` | Show help. |
|
|
34
|
+
|
|
35
|
+
## Templates
|
|
36
|
+
|
|
37
|
+
| Template | Stack | Source | Env file / prefix |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `next-app` | Next.js (App Router) | `ecis2-next-pilot` | `.env.local` / `NEXT_PUBLIC_S2W_*` |
|
|
40
|
+
| `vite-react` | Vite + plain React | `bridge-harness` | `.env` / `VITE_S2W_*` |
|
|
41
|
+
|
|
42
|
+
Both mount `createClient()` → `<AICChatProvider>` → `<AICChat>` (the wiring in
|
|
43
|
+
`ecis2-next-pilot/app/chat-widget.tsx`).
|
|
44
|
+
|
|
45
|
+
## After scaffolding
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd my-app
|
|
49
|
+
npm install
|
|
50
|
+
npm run dev # http://localhost:5173
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then make sure the app's origin is allowlisted in **AI-Gateway → Headless / JWT → Allowed
|
|
54
|
+
origins** (e.g. `http://localhost:5173`), or the token mint (`/auth/jwt`) returns `403`.
|
|
55
|
+
|
|
56
|
+
## SDK dependency (SPEAK-230)
|
|
57
|
+
|
|
58
|
+
Generated apps depend on **`@speak2web/ain-aic-sdk`**. Until it's published to npm (SPEAK-230),
|
|
59
|
+
`npm install` can't resolve it from the registry — link a local build instead:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# in the SDK repo
|
|
63
|
+
npm run build && npm link
|
|
64
|
+
# in the generated app
|
|
65
|
+
npm link @speak2web/ain-aic-sdk
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Zero runtime dependencies — Node 18+ built-ins only.
|
package/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @speak2web/create-chat — scaffold a pre-wired AIN/AIC chat app (SPEAK-234).
|
|
4
|
+
*
|
|
5
|
+
* Productizes the reference apps (ecis2-next-pilot → next-app, bridge-harness →
|
|
6
|
+
* vite-react) into starters so day-one is a working app, not hand-wiring
|
|
7
|
+
* createClient + provider + widget + env vars.
|
|
8
|
+
*
|
|
9
|
+
* npm create @speak2web/chat my-app
|
|
10
|
+
* npx @speak2web/create-chat my-app --template next-app \
|
|
11
|
+
* --api-base https://site.com/wp-json/s2w/v1 --site-id pub_abc123 --yes
|
|
12
|
+
*
|
|
13
|
+
* Zero runtime deps — Node 18+ built-ins only (matches the SDK's no-deps ethos).
|
|
14
|
+
*/
|
|
15
|
+
import { parseArgs } from 'node:util';
|
|
16
|
+
import { createInterface } from 'node:readline/promises';
|
|
17
|
+
import { stdin, stdout, cwd, exit, argv } from 'node:process';
|
|
18
|
+
import { cp, rename, writeFile, readFile, readdir, stat } from 'node:fs/promises';
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { join, resolve, dirname } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const TEMPLATES_DIR = join(HERE, 'templates');
|
|
25
|
+
|
|
26
|
+
const TEMPLATES = {
|
|
27
|
+
'next-app': {
|
|
28
|
+
label: 'Next.js (App Router)',
|
|
29
|
+
envFile: '.env.local',
|
|
30
|
+
envPrefix: 'NEXT_PUBLIC_S2W_',
|
|
31
|
+
run: 'npm run dev # http://localhost:5173',
|
|
32
|
+
},
|
|
33
|
+
'vite-react': {
|
|
34
|
+
label: 'Vite + plain React',
|
|
35
|
+
envFile: '.env',
|
|
36
|
+
envPrefix: 'VITE_S2W_',
|
|
37
|
+
run: 'npm run dev',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const PLACEHOLDER_API_BASE = 'https://your-site.com/wp-json/s2w/v1';
|
|
42
|
+
const PLACEHOLDER_SITE_ID = 'pub_xxxxxxxx';
|
|
43
|
+
|
|
44
|
+
function printHelp() {
|
|
45
|
+
stdout.write(`
|
|
46
|
+
create-chat — scaffold a Speak2Web AIN/AIC chat app
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
npm create @speak2web/chat <project-name> [options]
|
|
50
|
+
npx @speak2web/create-chat <project-name> [options]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
-t, --template <name> next-app (default) | vite-react
|
|
54
|
+
--api-base <url> bridge REST base, e.g. https://site.com/wp-json/s2w/v1
|
|
55
|
+
--token-endpoint <url> JWT issuer (default: <api-base>/auth/jwt)
|
|
56
|
+
--site-id <pub_…> public site id from AI-Gateway → Headless / JWT
|
|
57
|
+
-y, --yes accept defaults / skip prompts (non-interactive)
|
|
58
|
+
-h, --help show this help
|
|
59
|
+
|
|
60
|
+
Templates are pre-wired to @speak2web/ain-aic-sdk (on npm); bridge values are
|
|
61
|
+
written to the project's env file.
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Recursively list every file under dir (absolute paths). */
|
|
66
|
+
async function walk(dir) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
69
|
+
const full = join(dir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) out.push(...(await walk(full)));
|
|
71
|
+
else out.push(full);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Replace {{TOKENS}} in every text file under dir. */
|
|
77
|
+
async function replaceTokens(dir, tokens) {
|
|
78
|
+
for (const file of await walk(dir)) {
|
|
79
|
+
let text;
|
|
80
|
+
try {
|
|
81
|
+
text = await readFile(file, 'utf8');
|
|
82
|
+
} catch {
|
|
83
|
+
continue; // unreadable/binary — skip
|
|
84
|
+
}
|
|
85
|
+
let next = text;
|
|
86
|
+
for (const [k, v] of Object.entries(tokens)) next = next.split(k).join(v);
|
|
87
|
+
if (next !== text) await writeFile(file, next);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const { values, positionals } = parseArgs({
|
|
93
|
+
args: argv.slice(2),
|
|
94
|
+
allowPositionals: true,
|
|
95
|
+
options: {
|
|
96
|
+
template: { type: 'string', short: 't' },
|
|
97
|
+
'api-base': { type: 'string' },
|
|
98
|
+
'token-endpoint': { type: 'string' },
|
|
99
|
+
'site-id': { type: 'string' },
|
|
100
|
+
yes: { type: 'boolean', short: 'y' },
|
|
101
|
+
help: { type: 'boolean', short: 'h' },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (values.help) {
|
|
106
|
+
printHelp();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Non-interactive when --yes, or when there's no TTY (CI, pipes) — fall back to
|
|
111
|
+
// flag values / defaults instead of hanging on a prompt.
|
|
112
|
+
const interactive = !values.yes && stdin.isTTY;
|
|
113
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
114
|
+
const ask = async (question, def) => {
|
|
115
|
+
if (!rl) return def;
|
|
116
|
+
const answer = (await rl.question(`${question}${def ? ` (${def})` : ''}: `)).trim();
|
|
117
|
+
return answer || def;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const projectName = positionals[0] ?? (await ask('Project name', 'my-s2w-chat'));
|
|
122
|
+
if (!projectName || /[/\\]/.test(projectName)) {
|
|
123
|
+
throw new Error(`Invalid project name: "${projectName}"`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let template = values.template ?? (await ask('Template (next-app / vite-react)', 'next-app'));
|
|
127
|
+
if (!TEMPLATES[template]) {
|
|
128
|
+
throw new Error(`Unknown template "${template}". Choose: ${Object.keys(TEMPLATES).join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
const spec = TEMPLATES[template];
|
|
131
|
+
|
|
132
|
+
const apiBase = values['api-base'] ?? (await ask('Bridge apiBase', PLACEHOLDER_API_BASE));
|
|
133
|
+
const tokenDefault = values['token-endpoint'] ?? `${apiBase.replace(/\/+$/, '')}/auth/jwt`;
|
|
134
|
+
const tokenEndpoint = values['token-endpoint'] ?? (await ask('Token endpoint', tokenDefault));
|
|
135
|
+
const siteId = values['site-id'] ?? (await ask('Public siteId (pub_…)', PLACEHOLDER_SITE_ID));
|
|
136
|
+
|
|
137
|
+
rl?.close();
|
|
138
|
+
|
|
139
|
+
const targetDir = resolve(cwd(), projectName);
|
|
140
|
+
if (existsSync(targetDir)) {
|
|
141
|
+
throw new Error(`Refusing to overwrite existing path: ${targetDir}`);
|
|
142
|
+
}
|
|
143
|
+
const templateDir = join(TEMPLATES_DIR, template);
|
|
144
|
+
if (!existsSync(templateDir)) {
|
|
145
|
+
throw new Error(`Template files missing: ${templateDir}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 1. Copy the template tree.
|
|
149
|
+
await cp(templateDir, targetDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
// 2. npm strips `.gitignore` from published tarballs, so templates ship it as
|
|
152
|
+
// `gitignore` — restore the dot in the generated project.
|
|
153
|
+
if (existsSync(join(targetDir, 'gitignore'))) {
|
|
154
|
+
await rename(join(targetDir, 'gitignore'), join(targetDir, '.gitignore'));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Fill placeholders (project name in package.json / README).
|
|
158
|
+
await replaceTokens(targetDir, { '{{PROJECT_NAME}}': projectName });
|
|
159
|
+
|
|
160
|
+
// 4. Write the bridge config to the template's env file.
|
|
161
|
+
const env =
|
|
162
|
+
`# Speak2Web bridge — written by create-chat. ${spec.envPrefix}* values are\n` +
|
|
163
|
+
`# read by the app at build/start.\n` +
|
|
164
|
+
`${spec.envPrefix}API_BASE=${apiBase}\n` +
|
|
165
|
+
`${spec.envPrefix}TOKEN_ENDPOINT=${tokenEndpoint}\n` +
|
|
166
|
+
`${spec.envPrefix}SITE_ID=${siteId}\n`;
|
|
167
|
+
await writeFile(join(targetDir, spec.envFile), env);
|
|
168
|
+
|
|
169
|
+
// 5. Report.
|
|
170
|
+
const warnUnset = apiBase === PLACEHOLDER_API_BASE || siteId === PLACEHOLDER_SITE_ID;
|
|
171
|
+
stdout.write(`\n✅ Created ${projectName} (${spec.label})\n`);
|
|
172
|
+
stdout.write(` bridge: ${apiBase} · siteId: ${siteId}\n`);
|
|
173
|
+
if (warnUnset) {
|
|
174
|
+
stdout.write(
|
|
175
|
+
`\n⚠️ Placeholder bridge values — edit ${projectName}/${spec.envFile} with your\n` +
|
|
176
|
+
` real apiBase / siteId (AI-Gateway → Headless / JWT) before the chat will connect.\n`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
stdout.write(`\nNext:\n cd ${projectName}\n npm install\n ${spec.run}\n\n`);
|
|
180
|
+
} finally {
|
|
181
|
+
rl?.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
main().catch((err) => {
|
|
186
|
+
stdout.write(`\n✖ ${err.message}\n`);
|
|
187
|
+
exit(1);
|
|
188
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@speak2web/create-chat",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a pre-wired Speak2Web AIN/AIC chat app (Next.js App Router or Vite/React) against your headless bridge. `npm create @speak2web/chat`.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-chat": "index.js",
|
|
9
|
+
"create-s2w-chat": "index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"templates",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"speak2web",
|
|
24
|
+
"ain-aic-sdk",
|
|
25
|
+
"scaffold",
|
|
26
|
+
"create",
|
|
27
|
+
"chat",
|
|
28
|
+
"headless-wordpress"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+ssh://git@bitbucket.org/speak2web/s2w-ain-aic-sdk.git",
|
|
33
|
+
"directory": "create-chat"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A Speak2Web **AI Navigator / AI Concierge** chat app on **Next.js (App Router)**, scaffolded by
|
|
4
|
+
[`@speak2web/create-chat`](https://bitbucket.org/speak2web/s2w-ain-aic-sdk).
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev # http://localhost:5173
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open the page and use the chat bubble (bottom-right).
|
|
14
|
+
|
|
15
|
+
## How it's wired
|
|
16
|
+
|
|
17
|
+
- **`app/chat-widget.tsx`** — the client island: `createClient()` → `<AICChatProvider>` → `<AICChat>`.
|
|
18
|
+
- **`lib/config.ts`** — reads `NEXT_PUBLIC_S2W_*` env vars.
|
|
19
|
+
- **`.env.local`** — your bridge values (`apiBase`, `tokenEndpoint`, `siteId`), written by the scaffolder. Edit it to retarget.
|
|
20
|
+
|
|
21
|
+
Want a styled widget? `import '@speak2web/ain-aic-sdk/react/aic-chat.css'` and pass `theme="dark"`.
|
|
22
|
+
Want lead capture? Add `enableEscalation` to `<AICChat>`.
|
|
23
|
+
|
|
24
|
+
## CORS
|
|
25
|
+
|
|
26
|
+
The bridge only accepts requests from **allowlisted origins**. Add this app's origin
|
|
27
|
+
(e.g. `http://localhost:5173`) in **AI-Gateway → Headless / JWT → Allowed origins**, or the
|
|
28
|
+
token mint (`/auth/jwt`) returns `403`.
|
|
29
|
+
|
|
30
|
+
## SDK dependency
|
|
31
|
+
|
|
32
|
+
This app depends on `@speak2web/ain-aic-sdk`. Until it's published to npm (SPEAK-230), point the
|
|
33
|
+
dependency at a local build:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# in the SDK repo
|
|
37
|
+
npm run build && npm link
|
|
38
|
+
# here
|
|
39
|
+
npm link @speak2web/ain-aic-sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
or set `"@speak2web/ain-aic-sdk": "file:../path/to/s2w-ain-aic-sdk"` in `package.json`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { createClient } from '@speak2web/ain-aic-sdk';
|
|
5
|
+
import { AICChatProvider, AICChat } from '@speak2web/ain-aic-sdk/react';
|
|
6
|
+
import { config } from '../lib/config';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The headless chat island: createClient() against your bridge, then the
|
|
10
|
+
* opinionated React provider + drop-in widget. The JWT is minted once on first
|
|
11
|
+
* call and reused (watch the Network tab for one /auth/jwt then Bearer on /chat).
|
|
12
|
+
*
|
|
13
|
+
* Auto-bridge-on-mount (adr-0004 §D4): pass a `sessionId` to AICChatProvider to
|
|
14
|
+
* resume a prior conversation across rendering layers. `enableEscalation` adds an
|
|
15
|
+
* opt-in "Talk to a human" lead-capture flow. Import the stylesheet for a styled
|
|
16
|
+
* widget: `import '@speak2web/ain-aic-sdk/react/aic-chat.css'`.
|
|
17
|
+
*/
|
|
18
|
+
export default function ChatWidget() {
|
|
19
|
+
const client = useMemo(
|
|
20
|
+
() =>
|
|
21
|
+
createClient({
|
|
22
|
+
apiBase: config.apiBase,
|
|
23
|
+
tokenEndpoint: config.tokenEndpoint,
|
|
24
|
+
siteId: config.siteId,
|
|
25
|
+
visitorFingerprint: config.visitorFingerprint,
|
|
26
|
+
}),
|
|
27
|
+
[],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
position: 'fixed',
|
|
34
|
+
right: 16,
|
|
35
|
+
bottom: 16,
|
|
36
|
+
width: 360,
|
|
37
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
38
|
+
background: '#fff',
|
|
39
|
+
border: '1px solid #e5e7eb',
|
|
40
|
+
borderRadius: 12,
|
|
41
|
+
boxShadow: '0 8px 30px rgba(0,0,0,0.12)',
|
|
42
|
+
overflow: 'hidden',
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
style={{
|
|
47
|
+
padding: '10px 14px',
|
|
48
|
+
borderBottom: '1px solid #f1f1f4',
|
|
49
|
+
fontWeight: 600,
|
|
50
|
+
fontSize: 14,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
Chat
|
|
54
|
+
</div>
|
|
55
|
+
<div style={{ padding: 12 }}>
|
|
56
|
+
<AICChatProvider client={client} onError={(e) => console.error('[s2w bridge]', e)}>
|
|
57
|
+
<AICChat greeting="Hi! How can I help?" placeholder="Type a message…" />
|
|
58
|
+
</AICChatProvider>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: 'Speak2Web chat',
|
|
5
|
+
description: 'Next.js front end with AIN/AIC chat via @speak2web/ain-aic-sdk.',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<body
|
|
12
|
+
style={{
|
|
13
|
+
margin: 0,
|
|
14
|
+
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, sans-serif',
|
|
15
|
+
color: '#1a1a1a',
|
|
16
|
+
background: '#f6f7f9',
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import ChatWidget from './chat-widget';
|
|
2
|
+
|
|
3
|
+
// Server component (App Router). The chat widget is a client island mounted below;
|
|
4
|
+
// everything it needs talks to your bridge over /wp-json/s2w/v1 — no WordPress theme.
|
|
5
|
+
export default function Home() {
|
|
6
|
+
return (
|
|
7
|
+
<main style={{ maxWidth: 820, margin: '0 auto', padding: '32px 16px 96px' }}>
|
|
8
|
+
<header style={{ marginBottom: 24 }}>
|
|
9
|
+
<p
|
|
10
|
+
style={{
|
|
11
|
+
fontSize: 12,
|
|
12
|
+
letterSpacing: 0.5,
|
|
13
|
+
textTransform: 'uppercase',
|
|
14
|
+
color: '#6b7280',
|
|
15
|
+
margin: 0,
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
Speak2Web · headless chat
|
|
19
|
+
</p>
|
|
20
|
+
<h1 style={{ fontSize: 28, margin: '4px 0 8px' }}>Your Next.js front end</h1>
|
|
21
|
+
<p style={{ color: '#4b5563', margin: 0 }}>
|
|
22
|
+
This page is rendered by Next.js. The chat bubble (bottom-right) talks to AI
|
|
23
|
+
Navigator / AI Concierge through the <code>/wp-json/s2w/v1</code> bridge via{' '}
|
|
24
|
+
<code>@speak2web/ain-aic-sdk</code>. Edit the wiring in{' '}
|
|
25
|
+
<code>app/chat-widget.tsx</code> and the bridge config in <code>.env.local</code>.
|
|
26
|
+
</p>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
<section
|
|
30
|
+
style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 12, padding: 20 }}
|
|
31
|
+
>
|
|
32
|
+
<h2 style={{ fontSize: 18, marginTop: 0 }}>It works</h2>
|
|
33
|
+
<p style={{ color: '#4b5563', margin: 0 }}>
|
|
34
|
+
Open the chat in the bottom-right corner and say hello. If it can't connect,
|
|
35
|
+
confirm this origin is allowlisted in <strong>AI-Gateway → Headless / JWT → Allowed
|
|
36
|
+
origins</strong> and that your <code>.env.local</code> values are correct.
|
|
37
|
+
</p>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<ChatWidget />
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge config. Values come from NEXT_PUBLIC_S2W_* env vars (inlined at build,
|
|
3
|
+
* so they're available in the browser). `create-chat` wrote your real values into
|
|
4
|
+
* `.env.local`; the defaults below are placeholders for reference.
|
|
5
|
+
*/
|
|
6
|
+
export const config = {
|
|
7
|
+
/** AIN/AIC public REST surface (adr-0002): /wp-json/s2w/v1 */
|
|
8
|
+
apiBase: process.env.NEXT_PUBLIC_S2W_API_BASE ?? 'https://your-site.com/wp-json/s2w/v1',
|
|
9
|
+
/** AIG JWT issuer (adr-0003) */
|
|
10
|
+
tokenEndpoint:
|
|
11
|
+
process.env.NEXT_PUBLIC_S2W_TOKEN_ENDPOINT ?? 'https://your-site.com/wp-json/s2w/v1/auth/jwt',
|
|
12
|
+
/** Public site id (AIG → Headless / JWT, `pub_…`). Tokens are bound to it. */
|
|
13
|
+
siteId: process.env.NEXT_PUBLIC_S2W_SITE_ID ?? 'pub_xxxxxxxx',
|
|
14
|
+
/**
|
|
15
|
+
* Optional fixed visitor fingerprint. Set it ONLY for local dev (localhost has no
|
|
16
|
+
* apex cookie) so cross-renderer continuity still works while testing; leave unset
|
|
17
|
+
* in production, where the apex `s2w_visitor` cookie drives a stable identity.
|
|
18
|
+
*/
|
|
19
|
+
visitorFingerprint: process.env.NEXT_PUBLIC_S2W_VISITOR_FP || undefined,
|
|
20
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Speak2Web AIN/AIC chat app (Next.js App Router) — scaffolded by @speak2web/create-chat.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev -p 5173",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@speak2web/ain-aic-sdk": "^1.0.0",
|
|
14
|
+
"next": "^14.2.5",
|
|
15
|
+
"react": "^18.3.1",
|
|
16
|
+
"react-dom": "^18.3.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.14.0",
|
|
20
|
+
"@types/react": "^18.3.0",
|
|
21
|
+
"@types/react-dom": "^18.3.0",
|
|
22
|
+
"typescript": "^5.6.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A Speak2Web **AI Navigator / AI Concierge** chat app on **Vite + React**, scaffolded by
|
|
4
|
+
[`@speak2web/create-chat`](https://bitbucket.org/speak2web/s2w-ain-aic-sdk).
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev # http://localhost:5173
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## How it's wired
|
|
14
|
+
|
|
15
|
+
- **`src/main.jsx`** — `createClient()` → `<AICChatProvider>` → `<AICChat>` (with the default stylesheet imported).
|
|
16
|
+
- **`.env`** — your bridge values (`VITE_S2W_API_BASE`, `VITE_S2W_TOKEN_ENDPOINT`, `VITE_S2W_SITE_ID`), written by the scaffolder. Edit it to retarget.
|
|
17
|
+
|
|
18
|
+
## CORS
|
|
19
|
+
|
|
20
|
+
The bridge only accepts requests from **allowlisted origins**. Add this app's origin
|
|
21
|
+
(e.g. `http://localhost:5173`) in **AI-Gateway → Headless / JWT → Allowed origins**, or the
|
|
22
|
+
token mint (`/auth/jwt`) returns `403`.
|
|
23
|
+
|
|
24
|
+
## SDK dependency
|
|
25
|
+
|
|
26
|
+
This app depends on `@speak2web/ain-aic-sdk`. Until it's published to npm (SPEAK-230), link a
|
|
27
|
+
local build: `npm link @speak2web/ain-aic-sdk` (after `npm run build && npm link` in the SDK
|
|
28
|
+
repo), or set a `file:` path in `package.json`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Speak2Web chat</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body style="margin: 0; background: #f6f7f9">
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Speak2Web AIN/AIC chat app (Vite + React) — scaffolded by @speak2web/create-chat.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@speak2web/ain-aic-sdk": "^1.0.0",
|
|
14
|
+
"react": "^18.3.1",
|
|
15
|
+
"react-dom": "^18.3.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
19
|
+
"vite": "^5.4.11"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { createClient } from '@speak2web/ain-aic-sdk';
|
|
4
|
+
import { AICChatProvider, AICChat } from '@speak2web/ain-aic-sdk/react';
|
|
5
|
+
import '@speak2web/ain-aic-sdk/react/aic-chat.css';
|
|
6
|
+
|
|
7
|
+
// Bridge config from VITE_S2W_* env vars (create-chat wrote your values into .env).
|
|
8
|
+
const config = {
|
|
9
|
+
apiBase: import.meta.env.VITE_S2W_API_BASE ?? 'https://your-site.com/wp-json/s2w/v1',
|
|
10
|
+
tokenEndpoint:
|
|
11
|
+
import.meta.env.VITE_S2W_TOKEN_ENDPOINT ?? 'https://your-site.com/wp-json/s2w/v1/auth/jwt',
|
|
12
|
+
siteId: import.meta.env.VITE_S2W_SITE_ID ?? 'pub_xxxxxxxx',
|
|
13
|
+
// Set VITE_S2W_VISITOR_FP only for local dev (localhost has no apex cookie).
|
|
14
|
+
visitorFingerprint: import.meta.env.VITE_S2W_VISITOR_FP || undefined,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function App() {
|
|
18
|
+
// createClient() mints the JWT once and reuses it (watch the Network tab for one
|
|
19
|
+
// /auth/jwt then Bearer on /chat). Then the opinionated provider + drop-in widget.
|
|
20
|
+
const client = useMemo(() => createClient(config), []);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div style={{ maxWidth: 480, margin: '40px auto', fontFamily: 'system-ui, sans-serif', padding: '0 16px' }}>
|
|
24
|
+
<h1 style={{ fontSize: 20 }}>Speak2Web chat</h1>
|
|
25
|
+
<p style={{ color: '#4b5563', fontSize: 14 }}>
|
|
26
|
+
Wired via <code>@speak2web/ain-aic-sdk</code>. Edit <code>src/main.jsx</code>; bridge
|
|
27
|
+
config lives in <code>.env</code>. Add <code>enableEscalation</code> for lead capture.
|
|
28
|
+
</p>
|
|
29
|
+
<AICChatProvider client={client} onError={(e) => console.error('[s2w bridge]', e)}>
|
|
30
|
+
<AICChat greeting="Hi! How can I help?" placeholder="Type a message…" theme="light" />
|
|
31
|
+
</AICChatProvider>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
createRoot(document.getElementById('root')).render(<App />);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
// Port 5173 is a common default to allowlist in AI-Gateway → Headless / JWT.
|
|
7
|
+
server: { port: 5173, strictPort: true },
|
|
8
|
+
// The SDK externalizes react/react-dom; dedupe so the app and the SDK share one
|
|
9
|
+
// React copy (avoids the "two Reacts" invalid-hook-call error).
|
|
10
|
+
resolve: { dedupe: ['react', 'react-dom'] },
|
|
11
|
+
});
|