@fluxy-chat/create-fluxy-chat 0.5.14 → 0.5.19
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/CHANGELOG.md +33 -0
- package/dist/index.js +9 -4
- package/package.json +1 -1
- package/readme.md +2 -1
- package/templates/deal-room/src/App.tsx +17 -1
- package/templates/full/README.md +14 -4
- package/templates/full/fluxy.hosted.json +15 -0
- package/templates/full/package.json +4 -3
- package/templates/full/scripts/fluxy-deploy.mjs +53 -0
- package/templates/full/scripts/fluxy-setup.mjs +31 -7
- package/templates/full/src/App.tsx +14 -0
- package/templates/full/src/session.ts +7 -2
- package/templates/full/src/vite-env.d.ts +2 -1
- package/templates/live-cursors/.env.example +8 -5
- package/templates/live-cursors/package.json +2 -2
- package/templates/live-cursors/src/App.tsx +19 -2
- package/templates/live-cursors/src/vite-env.d.ts +1 -0
- package/templates/minimal/package.json +1 -1
- package/templates/polls/.env.example +13 -0
- package/templates/polls/README.md +13 -0
- package/templates/polls/index.html +12 -0
- package/templates/polls/package.json +24 -0
- package/templates/polls/src/App.tsx +135 -0
- package/templates/polls/src/index.css +85 -0
- package/templates/polls/src/main.tsx +10 -0
- package/templates/polls/src/session.ts +73 -0
- package/templates/polls/src/vite-env.d.ts +13 -0
- package/templates/polls/tsconfig.json +21 -0
- package/templates/polls/vite.config.ts +7 -0
- package/templates/react/.env.example +9 -5
- package/templates/react/README.md +7 -32
- package/templates/react/package.json +2 -2
- package/templates/react/src/App.tsx +31 -23
- package/templates/react/src/vite-env.d.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.19] - 2026-09-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Full template `fluxy.hosted.json` deploys `rooms` templates and extension slots with `pnpm fluxy:deploy`.
|
|
8
|
+
- Pin `@fluxy-chat/sdk@^0.6.8` (`connectInbox`, room extensions).
|
|
9
|
+
|
|
10
|
+
## [0.5.18] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Hosted `setup:hosted` reads `GET /public/demo-credentials` and writes `VITE_FLUXYCHAT_PUBLISHABLE_KEY`.
|
|
15
|
+
- `pnpm fluxy:deploy` on the full template: `fluxy.hosted.json` → `PUT /admin/projects/:id/publish-config`.
|
|
16
|
+
- React and live-cursors templates prefer `publishableKey` (pin sdk `^0.6.7` / react `^0.1.6`).
|
|
17
|
+
|
|
18
|
+
## [0.5.17] - 2026-09-01
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Gallery `--example polls`: `createPoll` / `votePoll`. Prefers `VITE_FLUXYCHAT_PUBLISHABLE_KEY`. Documents `POST /polls` `isAnonymous`.
|
|
23
|
+
|
|
24
|
+
## [0.5.16] - 2026-08-31
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `deal-room` template: Ask agent when `VITE_FLUXYCHAT_AGENT_ID` is set (`invokeAgent`).
|
|
29
|
+
|
|
30
|
+
## [0.5.15] - 2026-08-31
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Help text states the scoped package: `npx @fluxy-chat/create-fluxy-chat@latest`. Unscoped `create-fluxy-chat` is not ours.
|
|
35
|
+
|
|
3
36
|
## [0.5.14] - 2026-08-27
|
|
4
37
|
|
|
5
38
|
### Changed
|
package/dist/index.js
CHANGED
|
@@ -888,6 +888,7 @@ var GALLERY_EXAMPLES = [
|
|
|
888
888
|
"game-tick",
|
|
889
889
|
"voice-stage",
|
|
890
890
|
"comments-board",
|
|
891
|
+
"polls",
|
|
891
892
|
"whiteboard"
|
|
892
893
|
];
|
|
893
894
|
function parseArgs(argv) {
|
|
@@ -983,6 +984,9 @@ function parseArgs(argv) {
|
|
|
983
984
|
var HELP_TEXT = `
|
|
984
985
|
${pc.bold("create-fluxy-chat")} \u2014 Scaffold a FluxyChat app or bot worker
|
|
985
986
|
|
|
987
|
+
${pc.yellow("Use the scoped package:")} npx @fluxy-chat/create-fluxy-chat@latest
|
|
988
|
+
Unscoped ${pc.bold("npx create-fluxy-chat")} is not ours and will 404.
|
|
989
|
+
|
|
986
990
|
${pc.bold("Usage:")}
|
|
987
991
|
npx @fluxy-chat/create-fluxy-chat [project-name] [options]
|
|
988
992
|
|
|
@@ -1008,7 +1012,7 @@ ${pc.bold("Examples:")}
|
|
|
1008
1012
|
${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-cursors --example live-cursors")}
|
|
1009
1013
|
${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-doc --example tiptap-room")}
|
|
1010
1014
|
${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-war --example war-room")}
|
|
1011
|
-
${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-
|
|
1015
|
+
${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-polls --example polls")}
|
|
1012
1016
|
${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-chat --minimal")}
|
|
1013
1017
|
${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-bot --adapter slack")}
|
|
1014
1018
|
`;
|
|
@@ -1084,6 +1088,7 @@ async function main() {
|
|
|
1084
1088
|
"game-tick": "# Matchmake + start, then Submit input (not netcode)",
|
|
1085
1089
|
"voice-stage": "# Open two tabs \u2014 join speaker/listener (signaling, not WebRTC)",
|
|
1086
1090
|
"comments-board": "# Click the canvas to pin a thread",
|
|
1091
|
+
polls: "# Create a poll, open a second tab, vote",
|
|
1087
1092
|
"live-cursors-chat": "# Open two tabs \u2014 move and chat",
|
|
1088
1093
|
whiteboard: "# Open two tabs \u2014 draw strokes (Yjs, not a second CRDT)"
|
|
1089
1094
|
};
|
|
@@ -1092,7 +1097,7 @@ async function main() {
|
|
|
1092
1097
|
[
|
|
1093
1098
|
`cd ${projectName}`,
|
|
1094
1099
|
"cp .env.example .env",
|
|
1095
|
-
"# Set VITE_FLUXYCHAT_WORKER_URL + public room ID
|
|
1100
|
+
"# Set VITE_FLUXYCHAT_WORKER_URL + pk_ (or public room ID / member JWT)",
|
|
1096
1101
|
`${devCmd2} dev`,
|
|
1097
1102
|
tryHint
|
|
1098
1103
|
].join("\n"),
|
|
@@ -1253,7 +1258,7 @@ async function main() {
|
|
|
1253
1258
|
const devCmd = config.packageManager === "npm" ? "npm run" : config.packageManager;
|
|
1254
1259
|
const nextSteps = config.full || config.adapter === "full" ? config.mode === "hosted" ? [
|
|
1255
1260
|
`cd ${config.name}`,
|
|
1256
|
-
`${devCmd} setup:hosted #
|
|
1261
|
+
`${devCmd} setup:hosted # writes pk_ when /public/demo-credentials is on`,
|
|
1257
1262
|
`${devCmd} dev # http://localhost:5173`,
|
|
1258
1263
|
`# Keep this project: https://fluxychat.com/onboarding?from=cli`
|
|
1259
1264
|
].join("\n") : [
|
|
@@ -1266,7 +1271,7 @@ async function main() {
|
|
|
1266
1271
|
].join("\n") : config.adapter === "react" || config.minimal ? [
|
|
1267
1272
|
`cd ${config.name}`,
|
|
1268
1273
|
"cp .env.example .env",
|
|
1269
|
-
"# Set VITE_FLUXYCHAT_WORKER_URL +
|
|
1274
|
+
"# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLISHABLE_KEY",
|
|
1270
1275
|
`${devCmd} dev`,
|
|
1271
1276
|
"# In the room, send: @assistant hello"
|
|
1272
1277
|
].join("\n") : [
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -36,6 +36,7 @@ npx @fluxy-chat/create-fluxy-chat@latest my-fleet --example fleet-panel
|
|
|
36
36
|
npx @fluxy-chat/create-fluxy-chat@latest my-game --example game-tick
|
|
37
37
|
npx @fluxy-chat/create-fluxy-chat@latest my-stage --example voice-stage
|
|
38
38
|
npx @fluxy-chat/create-fluxy-chat@latest my-comments --example comments-board
|
|
39
|
+
npx @fluxy-chat/create-fluxy-chat@latest my-polls --example polls
|
|
39
40
|
npx @fluxy-chat/create-fluxy-chat@latest my-chat --template react -y
|
|
40
41
|
npx @fluxy-chat/create-fluxy-chat@latest my-bot --adapter slack --pm pnpm
|
|
41
42
|
```
|
|
@@ -51,7 +52,7 @@ npx @fluxy-chat/create-fluxy-chat@latest my-bot --adapter slack --pm pnpm
|
|
|
51
52
|
| `--yes` | `-y` | Skip prompts and accept defaults |
|
|
52
53
|
| `--full` | | Full stack template: chat + `@assistant` + `pnpm setup` / `pnpm dev` |
|
|
53
54
|
| `--minimal` | | Chat-only widget (`@fluxy-chat/ui-kit`), no platform modules |
|
|
54
|
-
| `--example <name>` | | Gallery: `live-cursors`, `live-cursors-chat`, `javascript-live-cursors`, `tiptap-room`, `war-room`, `iot-panel`, `draw`, `deal-room`, `fleet-panel`, `game-tick`, `voice-stage`, `comments-board`, `whiteboard` |
|
|
55
|
+
| `--example <name>` | | Gallery: `live-cursors`, `live-cursors-chat`, `javascript-live-cursors`, `tiptap-room`, `war-room`, `iot-panel`, `draw`, `deal-room`, `fleet-panel`, `game-tick`, `voice-stage`, `comments-board`, `polls`, `whiteboard` |
|
|
55
56
|
| `--skip-install` | | Skip dependency installation |
|
|
56
57
|
| `--no-git` | | Skip git repository initialization |
|
|
57
58
|
| `--help` | `-h` | Show help |
|
|
@@ -4,9 +4,10 @@ import { useFluxySession, workerUrl } from "./session";
|
|
|
4
4
|
|
|
5
5
|
function DealBoard({ roomId }: { roomId: string }) {
|
|
6
6
|
const { client } = useFluxyChat();
|
|
7
|
-
const { messages, sendMessage, connected } = useChat({ roomId, replay: "request" });
|
|
7
|
+
const { messages, sendMessage, connected, invokeAgent } = useChat({ roomId, replay: "request" });
|
|
8
8
|
const [draft, setDraft] = useState("Approve term sheet?");
|
|
9
9
|
const [busy, setBusy] = useState(false);
|
|
10
|
+
const agentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim();
|
|
10
11
|
|
|
11
12
|
async function propose() {
|
|
12
13
|
const content = draft.trim();
|
|
@@ -45,6 +46,16 @@ function DealBoard({ roomId }: { roomId: string }) {
|
|
|
45
46
|
URL.revokeObjectURL(url);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
async function askAgent() {
|
|
50
|
+
if (!agentId) return;
|
|
51
|
+
setBusy(true);
|
|
52
|
+
try {
|
|
53
|
+
await invokeAgent(agentId, "Summarize this deal thread and list open decisions.");
|
|
54
|
+
} finally {
|
|
55
|
+
setBusy(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
return (
|
|
49
60
|
<section className="panel">
|
|
50
61
|
<header className="chat-header">
|
|
@@ -87,6 +98,11 @@ function DealBoard({ roomId }: { roomId: string }) {
|
|
|
87
98
|
<button type="button" className="primary" onClick={() => void exportMarkdown()}>
|
|
88
99
|
Export .md
|
|
89
100
|
</button>
|
|
101
|
+
{agentId ? (
|
|
102
|
+
<button type="button" disabled={busy} onClick={() => void askAgent()}>
|
|
103
|
+
Ask agent
|
|
104
|
+
</button>
|
|
105
|
+
) : null}
|
|
90
106
|
</form>
|
|
91
107
|
</section>
|
|
92
108
|
);
|
package/templates/full/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# FluxyChat full stack starter
|
|
2
2
|
|
|
3
|
-
Chat + AI agent in a Vite app. Hosted
|
|
3
|
+
Chat + AI agent in a Vite app. Hosted: `pnpm setup:hosted` writes a public `pk_` when the Worker exposes `GET /public/demo-credentials`. Otherwise copy a `pk_` from the console, or sign in for a private assistant room.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -14,7 +14,16 @@ pnpm setup:hosted
|
|
|
14
14
|
pnpm dev
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
If demo credentials are configured on the Worker, chat starts with `publishableKey` (no Clerk). Sign in later for a private assistant room.
|
|
18
|
+
|
|
19
|
+
**Hosted overlay (one file deploy for deny / rooms / extension slots):**
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# fluxy.hosted.json → PUT /admin/projects/:id/publish-config
|
|
23
|
+
FLUXY_WORKER_URL=https://api.fluxychat.com FLUXY_ADMIN_JWT=eyJ... FLUXY_PROJECT_ID=prj_... pnpm fluxy:deploy
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This is not Worker-bundled `onPublish` callbacks. Those run in `fluxy.config.ts` on self-host. Hosted extension slots are declared `kv` / `counter` on the room Durable Object (`GET /rooms/:id/extensions`).
|
|
18
27
|
|
|
19
28
|
**Self-host (your Worker):**
|
|
20
29
|
|
|
@@ -37,14 +46,15 @@ If the Worker is down, `setup:local` asks for the URL. Merge `.fluxy/worker.dev.
|
|
|
37
46
|
|
|
38
47
|
| Command | Description |
|
|
39
48
|
|---------|-------------|
|
|
40
|
-
| `pnpm setup:hosted` |
|
|
49
|
+
| `pnpm setup:hosted` | Worker URL + public `pk_` when `/public/demo-credentials` is configured |
|
|
50
|
+
| `pnpm fluxy:deploy` | PUT hosted publish-config from `fluxy.hosted.json` |
|
|
41
51
|
| `pnpm setup:local` / `pnpm setup:self-host` | `POST /dev/provision` on your Worker |
|
|
42
52
|
| `pnpm doctor` | Health check |
|
|
43
53
|
| `pnpm dev` | Start Vite |
|
|
44
54
|
|
|
45
55
|
## Environment
|
|
46
56
|
|
|
47
|
-
`.env` from `pnpm setup:hosted`
|
|
57
|
+
`.env` from `pnpm setup:hosted` includes worker URL and, when configured, `VITE_FLUXYCHAT_PUBLISHABLE_KEY`. Clerk still creates a private assistant room.
|
|
48
58
|
|
|
49
59
|
Dashboard: [fluxychat.com/dashboard](https://fluxychat.com/dashboard)
|
|
50
60
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"denySubstrings": [],
|
|
3
|
+
"guestCanPublish": true,
|
|
4
|
+
"iotAutoAgentId": null,
|
|
5
|
+
"rooms": {
|
|
6
|
+
"support-*": {
|
|
7
|
+
"anonymous": false,
|
|
8
|
+
"guestCanPublish": false
|
|
9
|
+
},
|
|
10
|
+
"room-*": {
|
|
11
|
+
"anonymous": true,
|
|
12
|
+
"extensions": [{ "id": "state", "kind": "kv" }]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"setup:hosted": "node scripts/fluxy-setup.mjs --mode hosted",
|
|
9
9
|
"setup:local": "node scripts/fluxy-setup.mjs --mode local",
|
|
10
10
|
"setup:self-host": "node scripts/fluxy-setup.mjs --mode self-host",
|
|
11
|
+
"fluxy:deploy": "node scripts/fluxy-deploy.mjs",
|
|
11
12
|
"doctor": "node scripts/fluxy-doctor.mjs",
|
|
12
13
|
"dev": "node scripts/fluxy-dev.mjs",
|
|
13
14
|
"dev:app": "vite",
|
|
@@ -16,8 +17,8 @@
|
|
|
16
17
|
"postinstall": "node scripts/hoist-fluxy-sdk.mjs"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@fluxy-chat/react": "^0.1.
|
|
20
|
-
"@fluxy-chat/sdk": "^0.6.
|
|
20
|
+
"@fluxy-chat/react": "^0.1.6",
|
|
21
|
+
"@fluxy-chat/sdk": "^0.6.8",
|
|
21
22
|
"@fluxy-chat/ui": "^0.1.4",
|
|
22
23
|
"class-variance-authority": "^0.7.1",
|
|
23
24
|
"clsx": "^2.1.1",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
},
|
|
41
42
|
"pnpm": {
|
|
42
43
|
"overrides": {
|
|
43
|
-
"@fluxy-chat/sdk": "^0.6.
|
|
44
|
+
"@fluxy-chat/sdk": "^0.6.8"
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hosted overlay: PUT /admin/projects/:id/publish-config
|
|
4
|
+
* Serializes one fluxy.hosted.json (deny, guest publish, iotAutoAgentId, rooms
|
|
5
|
+
* templates + extension slots). Does not upload Worker callbacks.
|
|
6
|
+
*
|
|
7
|
+
* FLUXY_WORKER_URL=https://api.fluxychat.com \
|
|
8
|
+
* FLUXY_ADMIN_JWT=eyJ... \
|
|
9
|
+
* FLUXY_PROJECT_ID=prj_... \
|
|
10
|
+
* pnpm fluxy:deploy
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const configPath = join(root, "fluxy.hosted.json");
|
|
18
|
+
|
|
19
|
+
const workerUrl = (process.env.FLUXY_WORKER_URL || process.env.VITE_FLUXYCHAT_WORKER_URL || "").replace(/\/$/, "");
|
|
20
|
+
const adminJwt = process.env.FLUXY_ADMIN_JWT || "";
|
|
21
|
+
const projectId = process.env.FLUXY_PROJECT_ID || process.env.VITE_FLUXYCHAT_PROJECT_ID || "";
|
|
22
|
+
|
|
23
|
+
function fail(msg) {
|
|
24
|
+
console.error(`fluxy deploy: ${msg}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!workerUrl) fail("set FLUXY_WORKER_URL");
|
|
29
|
+
if (!adminJwt) fail("set FLUXY_ADMIN_JWT (owner/admin JWT, not pk_)");
|
|
30
|
+
if (!projectId) fail("set FLUXY_PROJECT_ID");
|
|
31
|
+
if (!existsSync(configPath)) fail(`missing ${configPath}`);
|
|
32
|
+
|
|
33
|
+
const hosted = JSON.parse(readFileSync(configPath, "utf8"));
|
|
34
|
+
const body = {
|
|
35
|
+
denySubstrings: Array.isArray(hosted.denySubstrings) ? hosted.denySubstrings : [],
|
|
36
|
+
guestCanPublish: hosted.guestCanPublish !== false,
|
|
37
|
+
iotAutoAgentId: hosted.iotAutoAgentId ?? null,
|
|
38
|
+
rooms: hosted.rooms && typeof hosted.rooms === "object" ? hosted.rooms : {},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const url = `${workerUrl}/admin/projects/${encodeURIComponent(projectId)}/publish-config`;
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method: "PUT",
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${adminJwt}`,
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
const text = await res.text();
|
|
51
|
+
if (!res.ok) fail(`${res.status} ${text}`);
|
|
52
|
+
console.log("publish-config updated");
|
|
53
|
+
console.log(text);
|
|
@@ -194,6 +194,7 @@ function writeEnv(vars) {
|
|
|
194
194
|
const lines = [
|
|
195
195
|
"# Auto-generated by pnpm setup — do not commit secrets",
|
|
196
196
|
`VITE_FLUXYCHAT_WORKER_URL=${vars.workerUrl}`,
|
|
197
|
+
`VITE_FLUXYCHAT_PUBLISHABLE_KEY=${vars.publishableKey || ""}`,
|
|
197
198
|
`VITE_FLUXYCHAT_MEMBER_JWT=${vars.token}`,
|
|
198
199
|
`VITE_FLUXYCHAT_ROOM_ID=${vars.roomId}`,
|
|
199
200
|
`VITE_FLUXYCHAT_AGENT_ID=${vars.agentId}`,
|
|
@@ -218,13 +219,30 @@ async function setupHosted() {
|
|
|
218
219
|
process.env.FLUXY_WORKER_URL ||
|
|
219
220
|
HOSTED_WORKER_DEFAULT;
|
|
220
221
|
const consoleUrl = process.env.FLUXY_CONSOLE_URL || HOSTED_CONSOLE_DEFAULT;
|
|
222
|
+
const fromEnvPk = (process.env.FLUXY_PUBLISHABLE_KEY || process.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY || "").trim();
|
|
221
223
|
|
|
222
224
|
console.log(dim(` mode: hosted · worker: ${workerUrl}`));
|
|
223
225
|
|
|
226
|
+
let publishableKey = fromEnvPk.startsWith("pk_") ? fromEnvPk : "";
|
|
227
|
+
let roomId = process.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "";
|
|
228
|
+
if (!publishableKey) {
|
|
229
|
+
try {
|
|
230
|
+
const demo = await getJson(workerUrl, "/public/demo-credentials");
|
|
231
|
+
if (demo?.publishableKey?.startsWith("pk_")) {
|
|
232
|
+
publishableKey = demo.publishableKey;
|
|
233
|
+
roomId = demo.roomId || roomId || "general";
|
|
234
|
+
ok("wrote public demo pk_ (GET /public/demo-credentials)");
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
/* demo not configured on this Worker */
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
224
241
|
writeEnv({
|
|
225
242
|
workerUrl,
|
|
243
|
+
publishableKey,
|
|
226
244
|
token: "",
|
|
227
|
-
roomId: "",
|
|
245
|
+
roomId: roomId || (publishableKey ? "general" : ""),
|
|
228
246
|
agentId: "",
|
|
229
247
|
agentHandle: "@assistant",
|
|
230
248
|
projectId: "",
|
|
@@ -236,17 +254,22 @@ async function setupHosted() {
|
|
|
236
254
|
mode: "hosted",
|
|
237
255
|
workerUrl,
|
|
238
256
|
projectId: null,
|
|
239
|
-
roomId: null,
|
|
257
|
+
roomId: roomId || null,
|
|
240
258
|
agentId: null,
|
|
241
259
|
agentHandle: "@assistant",
|
|
242
260
|
setupAt: new Date().toISOString(),
|
|
243
|
-
auth: "clerk",
|
|
261
|
+
auth: publishableKey ? "publishableKey" : "clerk",
|
|
244
262
|
});
|
|
245
263
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
if (publishableKey) {
|
|
265
|
+
ok("public room is ready. pnpm dev — no Clerk required");
|
|
266
|
+
console.log(dim(" Sign in later for a private assistant room: Open dashboard"));
|
|
267
|
+
} else {
|
|
268
|
+
ok("wrote hosted endpoints (copy a pk_ from the console, or sign in on pnpm dev)");
|
|
269
|
+
console.log(dim(" Run: pnpm dev"));
|
|
270
|
+
console.log(dim(` Console: ${consoleUrl}/onboarding`));
|
|
271
|
+
console.log(dim(" Hosted ops: set PUBLIC_DEMO_PUBLISHABLE_KEY on the Worker for a no-account path"));
|
|
272
|
+
}
|
|
250
273
|
}
|
|
251
274
|
|
|
252
275
|
async function setupLocal() {
|
|
@@ -323,6 +346,7 @@ async function setupLocal() {
|
|
|
323
346
|
|
|
324
347
|
writeEnv({
|
|
325
348
|
workerUrl,
|
|
349
|
+
publishableKey: provision.publishableKey || "",
|
|
326
350
|
token: auth.token,
|
|
327
351
|
roomId,
|
|
328
352
|
agentId: agent.id,
|
|
@@ -4,6 +4,7 @@ import { ChatWindow } from "@fluxy-chat/ui";
|
|
|
4
4
|
import { loadCliSession, type CliSession } from "./session";
|
|
5
5
|
|
|
6
6
|
const envWorkerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim() ?? "";
|
|
7
|
+
const envPublishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim() ?? "";
|
|
7
8
|
const envJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim() ?? "";
|
|
8
9
|
const envRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() ?? "";
|
|
9
10
|
const envAgentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() ?? "";
|
|
@@ -31,6 +32,18 @@ function dashboardHref(): string {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function sessionFromEnv(): CliSession | null {
|
|
35
|
+
if (envWorkerUrl && envPublishableKey.startsWith("pk_") && envRoomId) {
|
|
36
|
+
return {
|
|
37
|
+
workerUrl: envWorkerUrl,
|
|
38
|
+
publishableKey: envPublishableKey,
|
|
39
|
+
memberJwt: envJwt || undefined,
|
|
40
|
+
roomId: envRoomId,
|
|
41
|
+
agentId: envAgentId,
|
|
42
|
+
agentHandle: envAgentHandle,
|
|
43
|
+
projectId: envProjectId,
|
|
44
|
+
userId: envUserId || "guest",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
34
47
|
if (!envWorkerUrl || !envJwt || !envRoomId) return null;
|
|
35
48
|
return {
|
|
36
49
|
workerUrl: envWorkerUrl,
|
|
@@ -226,6 +239,7 @@ export function App() {
|
|
|
226
239
|
<div className="page">
|
|
227
240
|
<FluxyRealtimeProvider
|
|
228
241
|
workerUrl={session.workerUrl}
|
|
242
|
+
publishableKey={session.memberJwt ? undefined : session.publishableKey}
|
|
229
243
|
authTokenProvider={session.memberJwt}
|
|
230
244
|
userId={session.userId}
|
|
231
245
|
>
|
|
@@ -2,7 +2,8 @@ const STORAGE_KEY = "fluxy-cli-session";
|
|
|
2
2
|
|
|
3
3
|
export interface CliSession {
|
|
4
4
|
workerUrl: string;
|
|
5
|
-
memberJwt
|
|
5
|
+
memberJwt?: string;
|
|
6
|
+
publishableKey?: string;
|
|
6
7
|
roomId: string;
|
|
7
8
|
agentId: string;
|
|
8
9
|
agentHandle: string;
|
|
@@ -15,7 +16,11 @@ export interface CliSession {
|
|
|
15
16
|
function isCliSession(value: unknown): value is CliSession {
|
|
16
17
|
if (!value || typeof value !== "object") return false;
|
|
17
18
|
const v = value as Record<string, unknown>;
|
|
18
|
-
|
|
19
|
+
if (typeof v.workerUrl !== "string" || v.workerUrl.length === 0) return false;
|
|
20
|
+
if (typeof v.roomId !== "string" || v.roomId.length === 0) return false;
|
|
21
|
+
const hasMember = typeof v.memberJwt === "string" && v.memberJwt.length > 0;
|
|
22
|
+
const hasPk = typeof v.publishableKey === "string" && v.publishableKey.startsWith("pk_");
|
|
23
|
+
return hasMember || hasPk;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export function readCliSessionFromHash(): CliSession | null {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
interface ImportMetaEnv {
|
|
4
4
|
readonly VITE_FLUXYCHAT_WORKER_URL: string;
|
|
5
|
-
readonly
|
|
5
|
+
readonly VITE_FLUXYCHAT_PUBLISHABLE_KEY?: string;
|
|
6
|
+
readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
|
|
6
7
|
readonly VITE_FLUXYCHAT_ROOM_ID: string;
|
|
7
8
|
readonly VITE_FLUXYCHAT_AGENT_ID: string;
|
|
8
9
|
readonly VITE_FLUXYCHAT_AGENT_HANDLE: string;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# FluxyChat worker URL (hosted or self-hosted)
|
|
2
|
-
VITE_FLUXYCHAT_WORKER_URL=https://
|
|
2
|
+
VITE_FLUXYCHAT_WORKER_URL=https://api.fluxychat.com
|
|
3
3
|
|
|
4
|
-
# Option A —
|
|
5
|
-
|
|
6
|
-
VITE_FLUXYCHAT_ROOM_ID=
|
|
4
|
+
# Option A — public room (pin sdk 0.6.7+)
|
|
5
|
+
VITE_FLUXYCHAT_PUBLISHABLE_KEY=pk_...
|
|
6
|
+
VITE_FLUXYCHAT_ROOM_ID=general
|
|
7
7
|
|
|
8
|
-
# Option B —
|
|
8
|
+
# Option B — member JWT
|
|
9
|
+
# VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
|
|
10
|
+
|
|
11
|
+
# Option C — guest session without pk_
|
|
9
12
|
# VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@fluxy-chat/react": "^0.1.
|
|
13
|
-
"@fluxy-chat/sdk": "^0.6.
|
|
12
|
+
"@fluxy-chat/react": "^0.1.6",
|
|
13
|
+
"@fluxy-chat/sdk": "^0.6.7",
|
|
14
14
|
"react": "^19.0.0",
|
|
15
15
|
"react-dom": "^19.0.0",
|
|
16
16
|
"zustand": "^5.0.0"
|
|
@@ -3,6 +3,7 @@ import { FluxyChatClient } from "@fluxy-chat/sdk";
|
|
|
3
3
|
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
|
|
4
4
|
|
|
5
5
|
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
|
|
6
|
+
const publishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim();
|
|
6
7
|
const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
|
|
7
8
|
const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
|
|
8
9
|
const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
|
|
@@ -149,6 +150,23 @@ function hashCode(value: string): number {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
export function App() {
|
|
153
|
+
const pkRoomId = publicRoomId || configuredRoomId;
|
|
154
|
+
if (workerUrl && publishableKey) {
|
|
155
|
+
return (
|
|
156
|
+
<main className="shell">
|
|
157
|
+
<h1>Live cursors</h1>
|
|
158
|
+
<p className="mode-badge">publishableKey · two tabs, same room</p>
|
|
159
|
+
<FluxyRealtimeProvider workerUrl={workerUrl} publishableKey={publishableKey}>
|
|
160
|
+
<CursorCanvas roomId={pkRoomId} selfUserId="guest" />
|
|
161
|
+
</FluxyRealtimeProvider>
|
|
162
|
+
</main>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return <MemberOrGuestCursors />;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function MemberOrGuestCursors() {
|
|
152
170
|
const { session, loading, error } = useFluxySession();
|
|
153
171
|
const [roomId, setRoomId] = useState(configuredRoomId);
|
|
154
172
|
|
|
@@ -166,8 +184,7 @@ export function App() {
|
|
|
166
184
|
<code>VITE_FLUXYCHAT_WORKER_URL</code>.
|
|
167
185
|
</p>
|
|
168
186
|
<p>
|
|
169
|
-
Then
|
|
170
|
-
<code>VITE_FLUXYCHAT_PUBLIC_ROOM_ID</code>.
|
|
187
|
+
Then <code>VITE_FLUXYCHAT_PUBLISHABLE_KEY</code>, a member JWT, or a public room id.
|
|
171
188
|
</p>
|
|
172
189
|
</main>
|
|
173
190
|
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# FluxyChat worker URL (hosted or self-hosted)
|
|
2
|
+
VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
|
|
3
|
+
|
|
4
|
+
# Option A — public room, no member JWT (pin sdk 0.6.7+)
|
|
5
|
+
VITE_FLUXYCHAT_PUBLISHABLE_KEY=pk_...
|
|
6
|
+
VITE_FLUXYCHAT_ROOM_ID=your-public-room-id
|
|
7
|
+
|
|
8
|
+
# Option B — member JWT
|
|
9
|
+
# VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
|
|
10
|
+
# VITE_FLUXYCHAT_ROOM_ID=demo
|
|
11
|
+
|
|
12
|
+
# Option C — public guest session without pk_
|
|
13
|
+
# VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# FluxyChat polls
|
|
2
|
+
|
|
3
|
+
First-class room polls: `createPoll` / `votePoll` on the chat timeline.
|
|
4
|
+
|
|
5
|
+
Anonymous ballots (voter ids omitted from results) are a separate HTTP path: `POST /polls` with `isAnonymous: true`, then `POST /polls/:id/close`. That is not `createPoll`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @fluxy-chat/create-fluxy-chat@latest my-polls --example polls
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
npm run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Fastest path: `VITE_FLUXYCHAT_WORKER_URL` + `VITE_FLUXYCHAT_PUBLISHABLE_KEY` (`pk_`) + a public room id. Pin `@fluxy-chat/sdk@0.6.7` and `@fluxy-chat/react@0.1.6` once those versions are on npm.
|
|
@@ -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.0" />
|
|
6
|
+
<title>FluxyChat polls</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fluxy-polls",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@fluxy-chat/react": "^0.1.6",
|
|
13
|
+
"@fluxy-chat/sdk": "^0.6.7",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^19.0.0",
|
|
19
|
+
"@types/react-dom": "^19.0.0",
|
|
20
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
21
|
+
"typescript": "^5.6.0",
|
|
22
|
+
"vite": "^6.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
FluxyRealtimeProvider,
|
|
4
|
+
useChat,
|
|
5
|
+
useFluxyChat,
|
|
6
|
+
} from "@fluxy-chat/react";
|
|
7
|
+
import { useFluxySession, workerUrl } from "./session";
|
|
8
|
+
|
|
9
|
+
const publishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim();
|
|
10
|
+
const pkRoomId =
|
|
11
|
+
import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() ||
|
|
12
|
+
import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim() ||
|
|
13
|
+
"demo";
|
|
14
|
+
|
|
15
|
+
function PollRoom({ roomId }: { roomId: string }) {
|
|
16
|
+
const { client } = useFluxyChat();
|
|
17
|
+
const { messages } = useChat({ roomId });
|
|
18
|
+
const [question, setQuestion] = useState("Ship Friday?");
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
async function createTimelinePoll() {
|
|
22
|
+
setError(null);
|
|
23
|
+
try {
|
|
24
|
+
await client.createPoll(roomId, {
|
|
25
|
+
question,
|
|
26
|
+
options: ["Yes", "No", "Abstain"],
|
|
27
|
+
});
|
|
28
|
+
} catch (err) {
|
|
29
|
+
setError(err instanceof Error ? err.message : "createPoll failed");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<section className="panel">
|
|
35
|
+
<header className="chat-header">
|
|
36
|
+
<strong>Polls · {roomId}</strong>
|
|
37
|
+
<span className="status">createPoll / votePoll on the timeline</span>
|
|
38
|
+
</header>
|
|
39
|
+
<div className="composer">
|
|
40
|
+
<input
|
|
41
|
+
value={question}
|
|
42
|
+
onChange={(event) => setQuestion(event.target.value)}
|
|
43
|
+
placeholder="Question"
|
|
44
|
+
/>
|
|
45
|
+
<button type="button" className="primary" onClick={() => void createTimelinePoll()}>
|
|
46
|
+
Create poll
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
{error ? <p className="error" style={{ padding: "0 1rem" }}>{error}</p> : null}
|
|
50
|
+
<ul className="log">
|
|
51
|
+
{messages.map((message) => (
|
|
52
|
+
<li key={message.id} className="poll-card">
|
|
53
|
+
{message.poll ? (
|
|
54
|
+
<>
|
|
55
|
+
<p>
|
|
56
|
+
<strong>{message.poll.question}</strong>
|
|
57
|
+
{message.poll.closed ? " · closed" : ""}
|
|
58
|
+
</p>
|
|
59
|
+
<div className="poll-options">
|
|
60
|
+
{message.poll.options.map((option) => (
|
|
61
|
+
<button
|
|
62
|
+
key={option.index}
|
|
63
|
+
type="button"
|
|
64
|
+
disabled={message.poll?.closed}
|
|
65
|
+
onClick={() => void client.votePoll(message.id, option.index)}
|
|
66
|
+
>
|
|
67
|
+
{option.text} · {option.votes}
|
|
68
|
+
</button>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
<p className="hint" style={{ marginTop: 8 }}>
|
|
72
|
+
{message.poll.totalVoters} voters
|
|
73
|
+
{message.poll.userVote != null ? ` · you voted ${message.poll.userVote}` : ""}
|
|
74
|
+
</p>
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
<p>
|
|
78
|
+
<strong>{message.userId}</strong> {message.content}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
</section>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function App() {
|
|
89
|
+
if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
|
|
90
|
+
|
|
91
|
+
if (publishableKey) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="shell">
|
|
94
|
+
<p className="mode-badge">publishableKey · public room {pkRoomId}</p>
|
|
95
|
+
<p className="hint">
|
|
96
|
+
Timeline polls use createPoll. Anonymous ballots (no voter ids in results) are POST /polls with isAnonymous: true. See README.
|
|
97
|
+
</p>
|
|
98
|
+
<FluxyRealtimeProvider workerUrl={workerUrl} publishableKey={publishableKey}>
|
|
99
|
+
<PollRoom roomId={pkRoomId} />
|
|
100
|
+
</FluxyRealtimeProvider>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return <MemberOrGuestApp />;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function MemberOrGuestApp() {
|
|
109
|
+
const { session, loading, error } = useFluxySession();
|
|
110
|
+
if (loading) return <div className="shell">Starting…</div>;
|
|
111
|
+
if (error) return <div className="shell error">{error}</div>;
|
|
112
|
+
if (!session) {
|
|
113
|
+
return (
|
|
114
|
+
<div className="shell">
|
|
115
|
+
Set <code>VITE_FLUXYCHAT_PUBLISHABLE_KEY</code>, a member JWT, or a public room id.
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="shell">
|
|
122
|
+
<p className="mode-badge">{session.mode} · {session.roomId}</p>
|
|
123
|
+
<p className="hint">
|
|
124
|
+
Timeline polls use createPoll. Anonymous ballots: POST /polls with isAnonymous: true. See README.
|
|
125
|
+
</p>
|
|
126
|
+
<FluxyRealtimeProvider
|
|
127
|
+
workerUrl={session.workerUrl}
|
|
128
|
+
authTokenProvider={session.token}
|
|
129
|
+
userId={session.userId}
|
|
130
|
+
>
|
|
131
|
+
<PollRoom roomId={session.roomId} />
|
|
132
|
+
</FluxyRealtimeProvider>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
color: #0f172a;
|
|
5
|
+
background: #f8fafc;
|
|
6
|
+
}
|
|
7
|
+
* { box-sizing: border-box; }
|
|
8
|
+
body { margin: 0; }
|
|
9
|
+
.shell { max-width: 720px; margin: 0 auto; padding: 1.5rem; }
|
|
10
|
+
.panel {
|
|
11
|
+
border: 1px solid #e2e8f0;
|
|
12
|
+
border-radius: 12px;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
background: #fff;
|
|
15
|
+
}
|
|
16
|
+
.chat-header {
|
|
17
|
+
display: flex;
|
|
18
|
+
justify-content: space-between;
|
|
19
|
+
padding: 0.75rem 1rem;
|
|
20
|
+
background: #0f172a;
|
|
21
|
+
color: #fff;
|
|
22
|
+
}
|
|
23
|
+
.status { font-size: 0.75rem; opacity: 0.9; }
|
|
24
|
+
.roster {
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0.5rem 1rem;
|
|
27
|
+
list-style: none;
|
|
28
|
+
font-size: 0.8rem;
|
|
29
|
+
color: #64748b;
|
|
30
|
+
border-bottom: 1px solid #e2e8f0;
|
|
31
|
+
}
|
|
32
|
+
.log { min-height: 240px; max-height: 50vh; overflow: auto; padding: 1rem; }
|
|
33
|
+
.composer { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid #e2e8f0; }
|
|
34
|
+
.composer input { flex: 1; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 8px; }
|
|
35
|
+
.composer button, button.primary {
|
|
36
|
+
padding: 0.5rem 0.75rem;
|
|
37
|
+
border: 0;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
background: #2563eb;
|
|
40
|
+
color: #fff;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
.mode-badge { font-size: 0.75rem; color: #64748b; margin-bottom: 0.75rem; }
|
|
44
|
+
.error { color: #b91c1c; }
|
|
45
|
+
code { font-size: 0.85em; }
|
|
46
|
+
.hint { font-size: 0.8rem; color: #475569; white-space: pre-wrap; background: #f1f5f9; padding: 0.75rem; border-radius: 8px; }
|
|
47
|
+
.readings { margin: 0; padding: 0.75rem 1rem; list-style: none; font-size: 0.875rem; }
|
|
48
|
+
.canvas {
|
|
49
|
+
position: relative;
|
|
50
|
+
height: min(70vh, 480px);
|
|
51
|
+
background: #f8fafc;
|
|
52
|
+
touch-action: none;
|
|
53
|
+
}
|
|
54
|
+
.dot {
|
|
55
|
+
position: absolute;
|
|
56
|
+
width: 10px;
|
|
57
|
+
height: 10px;
|
|
58
|
+
border-radius: 999px;
|
|
59
|
+
transform: translate(-50%, -50%);
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
.peer-cursor {
|
|
63
|
+
position: absolute;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
z-index: 2;
|
|
66
|
+
}
|
|
67
|
+
.peer-cursor span {
|
|
68
|
+
display: block;
|
|
69
|
+
margin-top: 2px;
|
|
70
|
+
padding: 1px 6px;
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
background: currentColor;
|
|
73
|
+
color: #fff;
|
|
74
|
+
font-size: 11px;
|
|
75
|
+
}
|
|
76
|
+
.poll-card { margin: 0 0 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid #e2e8f0; }
|
|
77
|
+
.poll-options { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
78
|
+
.poll-options button {
|
|
79
|
+
padding: 0.4rem 0.7rem;
|
|
80
|
+
border: 1px solid #cbd5e1;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
background: #f8fafc;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
.poll-options button:disabled { opacity: 0.6; cursor: default; }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { FluxyChatClient } from "@fluxy-chat/sdk";
|
|
3
|
+
|
|
4
|
+
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
|
|
5
|
+
const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
|
|
6
|
+
const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
|
|
7
|
+
const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
|
|
8
|
+
|
|
9
|
+
export interface FluxySession {
|
|
10
|
+
workerUrl: string;
|
|
11
|
+
token: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
roomId: string;
|
|
14
|
+
mode: "member" | "guest";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useFluxySession(): {
|
|
18
|
+
session: FluxySession | null;
|
|
19
|
+
loading: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
} {
|
|
22
|
+
const [session, setSession] = useState<FluxySession | null>(null);
|
|
23
|
+
const [loading, setLoading] = useState(Boolean(workerUrl));
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!workerUrl) {
|
|
28
|
+
setLoading(false);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (memberJwt) {
|
|
32
|
+
setSession({
|
|
33
|
+
workerUrl,
|
|
34
|
+
token: memberJwt,
|
|
35
|
+
userId: "demo-user",
|
|
36
|
+
roomId: configuredRoomId,
|
|
37
|
+
mode: "member",
|
|
38
|
+
});
|
|
39
|
+
setLoading(false);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (publicRoomId) {
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
|
|
45
|
+
displayName: "Guest",
|
|
46
|
+
})
|
|
47
|
+
.then((guest) => {
|
|
48
|
+
if (cancelled) return;
|
|
49
|
+
setSession({
|
|
50
|
+
workerUrl,
|
|
51
|
+
token: guest.token,
|
|
52
|
+
userId: guest.userId,
|
|
53
|
+
roomId: guest.roomId,
|
|
54
|
+
mode: "guest",
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "Guest session failed");
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
if (!cancelled) setLoading(false);
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return { session, loading, error };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { workerUrl };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly VITE_FLUXYCHAT_WORKER_URL: string;
|
|
5
|
+
readonly VITE_FLUXYCHAT_PUBLISHABLE_KEY?: string;
|
|
6
|
+
readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
|
|
7
|
+
readonly VITE_FLUXYCHAT_ROOM_ID?: string;
|
|
8
|
+
readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ImportMeta {
|
|
12
|
+
readonly env: ImportMetaEnv;
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUncheckedSideEffectImports": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"]
|
|
21
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# FluxyChat worker URL (hosted or self-hosted)
|
|
2
|
-
VITE_FLUXYCHAT_WORKER_URL=https://
|
|
2
|
+
VITE_FLUXYCHAT_WORKER_URL=https://api.fluxychat.com
|
|
3
3
|
|
|
4
|
-
# Option A —
|
|
5
|
-
|
|
6
|
-
VITE_FLUXYCHAT_ROOM_ID=
|
|
4
|
+
# Option A — public room, no JWT (pin @fluxy-chat/sdk@0.6.7+)
|
|
5
|
+
VITE_FLUXYCHAT_PUBLISHABLE_KEY=pk_...
|
|
6
|
+
VITE_FLUXYCHAT_ROOM_ID=general
|
|
7
7
|
|
|
8
|
-
# Option B —
|
|
8
|
+
# Option B — member JWT
|
|
9
|
+
# VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
|
|
10
|
+
# VITE_FLUXYCHAT_ROOM_ID=demo
|
|
11
|
+
|
|
12
|
+
# Option C — guest session without pk_
|
|
9
13
|
# VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
|
|
@@ -1,43 +1,18 @@
|
|
|
1
1
|
# FluxyChat React starter
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Quick start (guest, fastest)
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
cp .env.example .env
|
|
9
|
-
# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLIC_ROOM_ID (public room from console)
|
|
10
|
-
npm install
|
|
11
|
-
npm run dev
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Open http://localhost:5173. A guest JWT is minted automatically via `joinPublicRoomAsGuest`.
|
|
15
|
-
|
|
16
|
-
## Quick start (member JWT)
|
|
3
|
+
`FluxyRealtimeProvider` + `publishableKey` + `useChat`. No member JWT mint.
|
|
17
4
|
|
|
18
5
|
```bash
|
|
19
6
|
cp .env.example .env
|
|
20
|
-
# Set VITE_FLUXYCHAT_WORKER_URL +
|
|
7
|
+
# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLISHABLE_KEY + room id
|
|
21
8
|
npm install
|
|
22
9
|
npm run dev
|
|
23
10
|
```
|
|
24
11
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
1. **Guest:** create a **public** room in console, copy room ID, set `VITE_FLUXYCHAT_PUBLIC_ROOM_ID`
|
|
28
|
-
2. **Member:** [fluxychat.com/onboarding](https://fluxychat.com/onboarding) for Worker URL and JWT
|
|
29
|
-
3. **Local monorepo:** `pnpm run first-message` from the FluxyChat repo
|
|
30
|
-
|
|
31
|
-
## Scripts
|
|
32
|
-
|
|
33
|
-
| Command | Description |
|
|
34
|
-
|---------|-------------|
|
|
35
|
-
| `npm run dev` | Vite dev server |
|
|
36
|
-
| `npm run build` | Production build |
|
|
37
|
-
| `npm run preview` | Preview production build |
|
|
12
|
+
Pin `@fluxy-chat/sdk@0.6.7` and `@fluxy-chat/react@0.1.6` once those versions are on npm.
|
|
38
13
|
|
|
39
|
-
##
|
|
14
|
+
## Other paths
|
|
40
15
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
16
|
+
- Member JWT: `VITE_FLUXYCHAT_MEMBER_JWT`
|
|
17
|
+
- Guest session: `VITE_FLUXYCHAT_PUBLIC_ROOM_ID` (`joinPublicRoomAsGuest`)
|
|
18
|
+
- Credentials: [fluxychat.com/onboarding](https://fluxychat.com/onboarding) or `GET /public/demo-credentials` on a Worker that sets `PUBLIC_DEMO_PUBLISHABLE_KEY`
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@fluxy-chat/react": "^0.1.
|
|
13
|
-
"@fluxy-chat/sdk": "^0.6.
|
|
12
|
+
"@fluxy-chat/react": "^0.1.6",
|
|
13
|
+
"@fluxy-chat/sdk": "^0.6.7",
|
|
14
14
|
"react": "^19.0.0",
|
|
15
15
|
"react-dom": "^19.0.0",
|
|
16
16
|
"zustand": "^5.0.0"
|
|
@@ -3,6 +3,7 @@ import { FluxyChatClient } from "@fluxy-chat/sdk";
|
|
|
3
3
|
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
|
|
4
4
|
|
|
5
5
|
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
|
|
6
|
+
const publishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim();
|
|
6
7
|
const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
|
|
7
8
|
const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
|
|
8
9
|
const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
|
|
@@ -21,11 +22,11 @@ function useFluxySession(): {
|
|
|
21
22
|
error: string | null;
|
|
22
23
|
} {
|
|
23
24
|
const [session, setSession] = useState<FluxySession | null>(null);
|
|
24
|
-
const [loading, setLoading] = useState(Boolean(workerUrl));
|
|
25
|
+
const [loading, setLoading] = useState(Boolean(workerUrl) && !publishableKey);
|
|
25
26
|
const [error, setError] = useState<string | null>(null);
|
|
26
27
|
|
|
27
28
|
useEffect(() => {
|
|
28
|
-
if (!workerUrl) {
|
|
29
|
+
if (!workerUrl || publishableKey) {
|
|
29
30
|
setLoading(false);
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
@@ -126,30 +127,44 @@ function ChatPanel({ roomId }: { roomId: string }) {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
export function App() {
|
|
129
|
-
const
|
|
130
|
-
const [roomId, setRoomId] = useState(configuredRoomId);
|
|
131
|
-
|
|
132
|
-
const activeRoomId = useMemo(() => {
|
|
133
|
-
if (session?.mode === "guest") return session.roomId;
|
|
134
|
-
return roomId.trim() || configuredRoomId;
|
|
135
|
-
}, [session, roomId]);
|
|
130
|
+
const pkRoomId = publicRoomId || configuredRoomId;
|
|
136
131
|
|
|
137
132
|
if (!workerUrl) {
|
|
138
133
|
return (
|
|
139
134
|
<main className="shell">
|
|
140
|
-
<h1>FluxyChat
|
|
135
|
+
<h1>FluxyChat</h1>
|
|
141
136
|
<p>
|
|
142
137
|
Copy <code>.env.example</code> to <code>.env</code> and set{" "}
|
|
143
|
-
<code>VITE_FLUXYCHAT_WORKER_URL</code>.
|
|
144
|
-
</p>
|
|
145
|
-
<p>
|
|
146
|
-
Then either <code>VITE_FLUXYCHAT_MEMBER_JWT</code> (member) or{" "}
|
|
147
|
-
<code>VITE_FLUXYCHAT_PUBLIC_ROOM_ID</code> (guest, ~60s to first message).
|
|
138
|
+
<code>VITE_FLUXYCHAT_WORKER_URL</code> plus <code>VITE_FLUXYCHAT_PUBLISHABLE_KEY</code>.
|
|
148
139
|
</p>
|
|
149
140
|
</main>
|
|
150
141
|
);
|
|
151
142
|
}
|
|
152
143
|
|
|
144
|
+
if (publishableKey) {
|
|
145
|
+
return (
|
|
146
|
+
<main className="shell">
|
|
147
|
+
<h1>FluxyChat</h1>
|
|
148
|
+
<p className="mode-badge">publishableKey · public room {pkRoomId}</p>
|
|
149
|
+
<FluxyRealtimeProvider workerUrl={workerUrl} publishableKey={publishableKey}>
|
|
150
|
+
<ChatPanel roomId={pkRoomId} />
|
|
151
|
+
</FluxyRealtimeProvider>
|
|
152
|
+
</main>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return <MemberOrGuestApp />;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function MemberOrGuestApp() {
|
|
160
|
+
const { session, loading, error } = useFluxySession();
|
|
161
|
+
const [roomId, setRoomId] = useState(configuredRoomId);
|
|
162
|
+
|
|
163
|
+
const activeRoomId = useMemo(() => {
|
|
164
|
+
if (session?.mode === "guest") return session.roomId;
|
|
165
|
+
return roomId.trim() || configuredRoomId;
|
|
166
|
+
}, [session, roomId]);
|
|
167
|
+
|
|
153
168
|
if (loading) {
|
|
154
169
|
return (
|
|
155
170
|
<main className="shell">
|
|
@@ -162,14 +177,7 @@ export function App() {
|
|
|
162
177
|
return (
|
|
163
178
|
<main className="shell">
|
|
164
179
|
<h1>FluxyChat</h1>
|
|
165
|
-
<p className="error">{error ?? "Set JWT or public room ID in .env"}</p>
|
|
166
|
-
<p>
|
|
167
|
-
Get credentials from{" "}
|
|
168
|
-
<a href="https://fluxychat.com/onboarding" target="_blank" rel="noreferrer">
|
|
169
|
-
onboarding
|
|
170
|
-
</a>{" "}
|
|
171
|
-
or use a public room ID for guest mode.
|
|
172
|
-
</p>
|
|
180
|
+
<p className="error">{error ?? "Set pk_, JWT, or public room ID in .env"}</p>
|
|
173
181
|
</main>
|
|
174
182
|
);
|
|
175
183
|
}
|