@fluxy-chat/create-fluxy-chat 0.5.9 → 0.5.13

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/index.js +111 -1
  3. package/package.json +1 -1
  4. package/readme.md +11 -0
  5. package/templates/basic/package.json +1 -1
  6. package/templates/comments-board/.env.example +11 -0
  7. package/templates/comments-board/README.md +9 -0
  8. package/templates/comments-board/index.html +12 -0
  9. package/templates/comments-board/package.json +26 -0
  10. package/templates/comments-board/src/App.tsx +86 -0
  11. package/templates/comments-board/src/index.css +75 -0
  12. package/templates/comments-board/src/main.tsx +10 -0
  13. package/templates/comments-board/src/session.ts +73 -0
  14. package/templates/comments-board/src/vite-env.d.ts +13 -0
  15. package/templates/comments-board/tsconfig.json +21 -0
  16. package/templates/comments-board/vite.config.ts +7 -0
  17. package/templates/deal-room/.env.example +11 -0
  18. package/templates/deal-room/README.md +11 -0
  19. package/templates/deal-room/index.html +12 -0
  20. package/templates/deal-room/package.json +25 -0
  21. package/templates/deal-room/src/App.tsx +110 -0
  22. package/templates/deal-room/src/index.css +75 -0
  23. package/templates/deal-room/src/main.tsx +10 -0
  24. package/templates/deal-room/src/session.ts +73 -0
  25. package/templates/deal-room/src/vite-env.d.ts +13 -0
  26. package/templates/deal-room/tsconfig.json +21 -0
  27. package/templates/deal-room/vite.config.ts +7 -0
  28. package/templates/discord/package.json +1 -1
  29. package/templates/draw/.env.example +7 -0
  30. package/templates/draw/README.md +7 -0
  31. package/templates/draw/index.html +12 -0
  32. package/templates/draw/package.json +25 -0
  33. package/templates/draw/src/App.tsx +91 -0
  34. package/templates/draw/src/index.css +75 -0
  35. package/templates/draw/src/main.tsx +10 -0
  36. package/templates/draw/src/session.ts +73 -0
  37. package/templates/draw/src/vite-env.d.ts +13 -0
  38. package/templates/draw/tsconfig.json +21 -0
  39. package/templates/draw/vite.config.ts +7 -0
  40. package/templates/fleet-panel/.env.example +11 -0
  41. package/templates/fleet-panel/README.md +9 -0
  42. package/templates/fleet-panel/index.html +12 -0
  43. package/templates/fleet-panel/package.json +25 -0
  44. package/templates/fleet-panel/src/App.tsx +83 -0
  45. package/templates/fleet-panel/src/index.css +75 -0
  46. package/templates/fleet-panel/src/main.tsx +10 -0
  47. package/templates/fleet-panel/src/session.ts +73 -0
  48. package/templates/fleet-panel/src/vite-env.d.ts +13 -0
  49. package/templates/fleet-panel/tsconfig.json +21 -0
  50. package/templates/fleet-panel/vite.config.ts +7 -0
  51. package/templates/full/package.json +4 -4
  52. package/templates/full/scripts/hoist-fluxy-sdk.mjs +1 -1
  53. package/templates/full/src/App.tsx +20 -6
  54. package/templates/game-tick/.env.example +11 -0
  55. package/templates/game-tick/README.md +9 -0
  56. package/templates/game-tick/index.html +12 -0
  57. package/templates/game-tick/package.json +25 -0
  58. package/templates/game-tick/src/App.tsx +87 -0
  59. package/templates/game-tick/src/index.css +75 -0
  60. package/templates/game-tick/src/main.tsx +10 -0
  61. package/templates/game-tick/src/session.ts +73 -0
  62. package/templates/game-tick/src/vite-env.d.ts +13 -0
  63. package/templates/game-tick/tsconfig.json +21 -0
  64. package/templates/game-tick/vite.config.ts +7 -0
  65. package/templates/iot-panel/.env.example +7 -0
  66. package/templates/iot-panel/README.md +16 -0
  67. package/templates/iot-panel/index.html +12 -0
  68. package/templates/iot-panel/package.json +25 -0
  69. package/templates/iot-panel/src/App.tsx +97 -0
  70. package/templates/iot-panel/src/index.css +75 -0
  71. package/templates/iot-panel/src/main.tsx +10 -0
  72. package/templates/iot-panel/src/session.ts +73 -0
  73. package/templates/iot-panel/src/vite-env.d.ts +13 -0
  74. package/templates/iot-panel/tsconfig.json +21 -0
  75. package/templates/iot-panel/vite.config.ts +7 -0
  76. package/templates/javascript-live-cursors/.env.example +9 -0
  77. package/templates/javascript-live-cursors/README.md +9 -0
  78. package/templates/javascript-live-cursors/index.html +17 -0
  79. package/templates/javascript-live-cursors/package.json +18 -0
  80. package/templates/javascript-live-cursors/src/index.css +31 -0
  81. package/templates/javascript-live-cursors/src/main.ts +89 -0
  82. package/templates/javascript-live-cursors/src/vite-env.d.ts +12 -0
  83. package/templates/javascript-live-cursors/tsconfig.json +13 -0
  84. package/templates/javascript-live-cursors/vite.config.ts +5 -0
  85. package/templates/live-cursors/.env.example +9 -0
  86. package/templates/live-cursors/README.md +20 -0
  87. package/templates/live-cursors/index.html +12 -0
  88. package/templates/live-cursors/package.json +25 -0
  89. package/templates/live-cursors/src/App.tsx +214 -0
  90. package/templates/live-cursors/src/index.css +94 -0
  91. package/templates/live-cursors/src/main.tsx +10 -0
  92. package/templates/live-cursors/src/vite-env.d.ts +12 -0
  93. package/templates/live-cursors/tsconfig.json +21 -0
  94. package/templates/live-cursors/vite.config.ts +7 -0
  95. package/templates/live-cursors-chat/.env.example +9 -0
  96. package/templates/live-cursors-chat/README.md +11 -0
  97. package/templates/live-cursors-chat/index.html +12 -0
  98. package/templates/live-cursors-chat/package.json +25 -0
  99. package/templates/live-cursors-chat/src/App.tsx +241 -0
  100. package/templates/live-cursors-chat/src/index.css +94 -0
  101. package/templates/live-cursors-chat/src/main.tsx +10 -0
  102. package/templates/live-cursors-chat/src/vite-env.d.ts +12 -0
  103. package/templates/live-cursors-chat/tsconfig.json +21 -0
  104. package/templates/live-cursors-chat/vite.config.ts +7 -0
  105. package/templates/minimal/package.json +4 -4
  106. package/templates/react/package.json +2 -2
  107. package/templates/react/src/App.tsx +8 -2
  108. package/templates/slack/package.json +1 -1
  109. package/templates/telegram/package.json +1 -1
  110. package/templates/tiptap-room/.env.example +9 -0
  111. package/templates/tiptap-room/README.md +14 -0
  112. package/templates/tiptap-room/index.html +12 -0
  113. package/templates/tiptap-room/package.json +29 -0
  114. package/templates/tiptap-room/src/App.tsx +217 -0
  115. package/templates/tiptap-room/src/index.css +107 -0
  116. package/templates/tiptap-room/src/main.tsx +10 -0
  117. package/templates/tiptap-room/src/vite-env.d.ts +12 -0
  118. package/templates/tiptap-room/tsconfig.json +21 -0
  119. package/templates/tiptap-room/vite.config.ts +7 -0
  120. package/templates/voice-stage/.env.example +11 -0
  121. package/templates/voice-stage/README.md +11 -0
  122. package/templates/voice-stage/index.html +12 -0
  123. package/templates/voice-stage/package.json +25 -0
  124. package/templates/voice-stage/src/App.tsx +61 -0
  125. package/templates/voice-stage/src/index.css +75 -0
  126. package/templates/voice-stage/src/main.tsx +10 -0
  127. package/templates/voice-stage/src/session.ts +73 -0
  128. package/templates/voice-stage/src/vite-env.d.ts +13 -0
  129. package/templates/voice-stage/tsconfig.json +21 -0
  130. package/templates/voice-stage/vite.config.ts +7 -0
  131. package/templates/war-room/.env.example +11 -0
  132. package/templates/war-room/README.md +11 -0
  133. package/templates/war-room/index.html +12 -0
  134. package/templates/war-room/package.json +26 -0
  135. package/templates/war-room/src/App.tsx +120 -0
  136. package/templates/war-room/src/index.css +75 -0
  137. package/templates/war-room/src/main.tsx +10 -0
  138. package/templates/war-room/src/session.ts +73 -0
  139. package/templates/war-room/src/vite-env.d.ts +13 -0
  140. package/templates/war-room/tsconfig.json +21 -0
  141. package/templates/war-room/vite.config.ts +7 -0
  142. package/templates/whiteboard/.env.example +11 -0
  143. package/templates/whiteboard/README.md +9 -0
  144. package/templates/whiteboard/index.html +12 -0
  145. package/templates/whiteboard/package.json +26 -0
  146. package/templates/whiteboard/src/App.tsx +122 -0
  147. package/templates/whiteboard/src/index.css +75 -0
  148. package/templates/whiteboard/src/main.tsx +10 -0
  149. package/templates/whiteboard/src/session.ts +73 -0
  150. package/templates/whiteboard/src/vite-env.d.ts +13 -0
  151. package/templates/whiteboard/tsconfig.json +21 -0
  152. package/templates/whiteboard/vite.config.ts +7 -0
@@ -0,0 +1,217 @@
1
+ import { useEffect, useState } from "react";
2
+ import { EditorContent, useEditor } from "@tiptap/react";
3
+ import StarterKit from "@tiptap/starter-kit";
4
+ import Collaboration from "@tiptap/extension-collaboration";
5
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
6
+ import {
7
+ FLUXY_YJS_EDITOR_FRAGMENT,
8
+ FluxyYjsProvider,
9
+ isLiveFile,
10
+ uploadLiveFile,
11
+ useMutation,
12
+ useRedo,
13
+ useStorage,
14
+ useUndo,
15
+ useYjsContext,
16
+ type FluxyLiveFile,
17
+ } from "@fluxy-chat/sdk/yjs";
18
+
19
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
20
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
21
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
22
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
23
+
24
+ interface FluxySession {
25
+ workerUrl: string;
26
+ token: string;
27
+ userId: string;
28
+ roomId: string;
29
+ mode: "member" | "guest";
30
+ }
31
+
32
+ function useFluxySession(): {
33
+ session: FluxySession | null;
34
+ loading: boolean;
35
+ error: string | null;
36
+ } {
37
+ const [session, setSession] = useState<FluxySession | null>(null);
38
+ const [loading, setLoading] = useState(Boolean(workerUrl));
39
+ const [error, setError] = useState<string | null>(null);
40
+
41
+ useEffect(() => {
42
+ if (!workerUrl) {
43
+ setLoading(false);
44
+ return;
45
+ }
46
+
47
+ if (memberJwt) {
48
+ setSession({
49
+ workerUrl,
50
+ token: memberJwt,
51
+ userId: "demo-user",
52
+ roomId: configuredRoomId,
53
+ mode: "member",
54
+ });
55
+ setLoading(false);
56
+ return;
57
+ }
58
+
59
+ if (publicRoomId) {
60
+ let cancelled = false;
61
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
62
+ displayName: "Guest",
63
+ })
64
+ .then((guest) => {
65
+ if (cancelled) return;
66
+ setSession({
67
+ workerUrl,
68
+ token: guest.token,
69
+ userId: guest.userId,
70
+ roomId: guest.roomId,
71
+ mode: "guest",
72
+ });
73
+ })
74
+ .catch((err) => {
75
+ if (!cancelled) {
76
+ setError(err instanceof Error ? err.message : "Guest session failed");
77
+ }
78
+ })
79
+ .finally(() => {
80
+ if (!cancelled) setLoading(false);
81
+ });
82
+ return () => {
83
+ cancelled = true;
84
+ };
85
+ }
86
+
87
+ setLoading(false);
88
+ }, []);
89
+
90
+ return { session, loading, error };
91
+ }
92
+
93
+ function DocBoard() {
94
+ const { doc, connected, client, roomId } = useYjsContext();
95
+ const title = useStorage((root) => (typeof root.title === "string" ? root.title : "Untitled"));
96
+ const hero = useStorage((root) => (isLiveFile(root.hero) ? (root.hero as FluxyLiveFile) : null));
97
+ const setTitle = useMutation((storage, next: string) => {
98
+ storage.set("title", next);
99
+ }, []);
100
+ const setHero = useMutation((storage, file: FluxyLiveFile) => {
101
+ storage.set("hero", file);
102
+ }, []);
103
+ const undo = useUndo();
104
+ const redo = useRedo();
105
+
106
+ const editor = useEditor({
107
+ extensions: [
108
+ StarterKit.configure({ history: false }),
109
+ Collaboration.configure({
110
+ document: doc,
111
+ field: FLUXY_YJS_EDITOR_FRAGMENT,
112
+ }),
113
+ ],
114
+ });
115
+
116
+ async function onPickFile(file: File | undefined) {
117
+ if (!file) return;
118
+ const live = await uploadLiveFile(client, roomId, file);
119
+ setHero(live);
120
+ }
121
+
122
+ return (
123
+ <>
124
+ <div className="chat-header">
125
+ <strong>Tiptap room</strong>
126
+ <span className="status">{connected ? "Yjs connected" : "connecting…"}</span>
127
+ </div>
128
+ <div className="toolbar">
129
+ <input
130
+ type="text"
131
+ value={title}
132
+ onChange={(event) => setTitle(event.target.value)}
133
+ aria-label="Document title"
134
+ />
135
+ <button type="button" onClick={() => undo()}>
136
+ Undo
137
+ </button>
138
+ <button type="button" onClick={() => redo()}>
139
+ Redo
140
+ </button>
141
+ <input
142
+ type="file"
143
+ accept="image/*"
144
+ onChange={(event) => void onPickFile(event.target.files?.[0])}
145
+ />
146
+ </div>
147
+ <div className="editor">
148
+ {editor ? <EditorContent editor={editor} /> : <p>Loading editor…</p>}
149
+ {hero ? <img className="hero" src={hero.url} alt={hero.name} /> : null}
150
+ </div>
151
+ </>
152
+ );
153
+ }
154
+
155
+ export function App() {
156
+ const { session, loading, error } = useFluxySession();
157
+ const [roomOverride, setRoomOverride] = useState("");
158
+
159
+ if (!workerUrl) {
160
+ return (
161
+ <div className="shell">
162
+ <p>Set <code>VITE_FLUXYCHAT_WORKER_URL</code> in <code>.env</code>.</p>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ if (loading) {
168
+ return (
169
+ <div className="shell">
170
+ <p>Starting session…</p>
171
+ </div>
172
+ );
173
+ }
174
+
175
+ if (error) {
176
+ return (
177
+ <div className="shell">
178
+ <p className="error">{error}</p>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ if (!session) {
184
+ return (
185
+ <div className="shell">
186
+ <p>Set a member JWT or a public room id in <code>.env</code>.</p>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ const roomId = roomOverride.trim() || session.roomId;
192
+
193
+ return (
194
+ <div className="shell">
195
+ <p className="mode-badge">
196
+ {session.mode} · room <code>{roomId}</code> · open two tabs
197
+ </p>
198
+ <label className="room-picker">
199
+ Room id
200
+ <input
201
+ value={roomOverride}
202
+ placeholder={session.roomId}
203
+ onChange={(event) => setRoomOverride(event.target.value)}
204
+ />
205
+ </label>
206
+ <FluxyYjsProvider
207
+ key={roomId}
208
+ workerUrl={session.workerUrl}
209
+ token={session.token}
210
+ userId={session.userId}
211
+ roomId={roomId}
212
+ >
213
+ <DocBoard />
214
+ </FluxyYjsProvider>
215
+ </div>
216
+ );
217
+ }
@@ -0,0 +1,107 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #0f172a;
5
+ background: #f8fafc;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ .shell {
17
+ max-width: 800px;
18
+ margin: 0 auto;
19
+ padding: 1.5rem;
20
+ }
21
+
22
+ .chat-header {
23
+ display: flex;
24
+ justify-content: space-between;
25
+ padding: 0.75rem 1rem;
26
+ border: 1px solid #e2e8f0;
27
+ border-bottom: 0;
28
+ border-radius: 12px 12px 0 0;
29
+ background: #0f172a;
30
+ color: #fff;
31
+ }
32
+
33
+ .status {
34
+ font-size: 0.75rem;
35
+ opacity: 0.9;
36
+ }
37
+
38
+ .toolbar {
39
+ display: flex;
40
+ flex-wrap: wrap;
41
+ gap: 0.5rem;
42
+ align-items: center;
43
+ padding: 0.75rem 1rem;
44
+ border: 1px solid #e2e8f0;
45
+ border-bottom: 0;
46
+ background: #fff;
47
+ }
48
+
49
+ .toolbar input[type="text"] {
50
+ flex: 1;
51
+ min-width: 12rem;
52
+ padding: 0.4rem 0.6rem;
53
+ border: 1px solid #cbd5e1;
54
+ border-radius: 8px;
55
+ }
56
+
57
+ .toolbar button {
58
+ padding: 0.4rem 0.75rem;
59
+ border: 0;
60
+ border-radius: 8px;
61
+ background: #2563eb;
62
+ color: #fff;
63
+ cursor: pointer;
64
+ }
65
+
66
+ .editor {
67
+ min-height: 280px;
68
+ padding: 1rem 1.25rem;
69
+ border: 1px solid #e2e8f0;
70
+ border-radius: 0 0 12px 12px;
71
+ background: #fff;
72
+ }
73
+
74
+ .editor .tiptap {
75
+ outline: none;
76
+ min-height: 240px;
77
+ }
78
+
79
+ .hero {
80
+ margin: 0.75rem 0 0;
81
+ max-width: 100%;
82
+ border-radius: 8px;
83
+ }
84
+
85
+ .mode-badge {
86
+ font-size: 0.75rem;
87
+ color: #64748b;
88
+ margin: 0 0 0.75rem;
89
+ }
90
+
91
+ .error {
92
+ color: #b91c1c;
93
+ }
94
+
95
+ .room-picker {
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 0.25rem;
99
+ margin-bottom: 1rem;
100
+ font-size: 0.875rem;
101
+ }
102
+
103
+ .room-picker input {
104
+ padding: 0.5rem 0.75rem;
105
+ border: 1px solid #cbd5e1;
106
+ border-radius: 8px;
107
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
+ readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
6
+ readonly VITE_FLUXYCHAT_ROOM_ID?: string;
7
+ readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv;
12
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173 },
7
+ });
@@ -0,0 +1,11 @@
1
+ # FluxyChat worker URL (hosted or self-hosted)
2
+ VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
3
+
4
+ # Option A — member JWT
5
+ VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
6
+ VITE_FLUXYCHAT_ROOM_ID=demo
7
+ # Optional — room-peer agent on the timeline
8
+ # VITE_FLUXYCHAT_AGENT_ID=agt_...
9
+
10
+ # Option B — public guest room
11
+ # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -0,0 +1,11 @@
1
+ # FluxyChat voice stage
2
+
3
+ `joinVoiceStage` is **signaling on the room WebSocket** (who is speaker/listener). It is not live WebRTC audio and does not publish latency numbers.
4
+
5
+ Async clips: `POST /messages/voice`.
6
+
7
+ ```bash
8
+ npx @fluxy-chat/create-fluxy-chat@latest my-stage --example voice-stage
9
+ cp .env.example .env
10
+ npm run dev
11
+ ```
@@ -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 voice stage</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,25 @@
1
+ {
2
+ "name": "fluxy-voice-stage",
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.4",
13
+ "@fluxy-chat/sdk": "^0.6.4",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "zustand": "^5.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "@vitejs/plugin-react": "^4.3.0",
22
+ "typescript": "^5.6.0",
23
+ "vite": "^6.0.0"
24
+ }
25
+ }
@@ -0,0 +1,61 @@
1
+ import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
2
+ import { useFluxySession, workerUrl } from "./session";
3
+
4
+ function StageBoard({ roomId }: { roomId: string }) {
5
+ const { connected, voiceStage, joinVoiceStage, leaveVoiceStage, sendVoiceStageVad } = useChat({
6
+ roomId,
7
+ replay: "request",
8
+ });
9
+
10
+ return (
11
+ <section className="panel">
12
+ <header className="chat-header">
13
+ <strong>Voice stage · {roomId}</strong>
14
+ <span className="status">{connected ? "live" : "connecting"} · signaling only</span>
15
+ </header>
16
+ <p className="hint">
17
+ stage_join / stage_leave on the same room WS. No huddle audio, no unpublished P95. Clips: POST /messages/voice.
18
+ </p>
19
+ <ul className="readings">
20
+ {(voiceStage?.participants ?? []).length === 0 ? <li>Empty stage.</li> : null}
21
+ {(voiceStage?.participants ?? []).map((p) => (
22
+ <li key={p.userId}>
23
+ {p.displayName || p.userId} · {p.role}
24
+ {p.isActiveSpeaker ? " · speaking" : ""}
25
+ </li>
26
+ ))}
27
+ </ul>
28
+ <div className="composer">
29
+ <button type="button" className="primary" onClick={() => joinVoiceStage("speaker", "Demo")}>
30
+ Join as speaker
31
+ </button>
32
+ <button type="button" className="primary" onClick={() => joinVoiceStage("listener", "Demo")}>
33
+ Join as listener
34
+ </button>
35
+ <button type="button" className="primary" onClick={() => sendVoiceStageVad(0.8)}>
36
+ Pulse VAD
37
+ </button>
38
+ <button type="button" className="primary" onClick={() => leaveVoiceStage()}>
39
+ Leave
40
+ </button>
41
+ </div>
42
+ </section>
43
+ );
44
+ }
45
+
46
+ export function App() {
47
+ const { session, loading, error } = useFluxySession();
48
+ if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
49
+ if (loading) return <div className="shell">Starting…</div>;
50
+ if (error) return <div className="shell error">{error}</div>;
51
+ if (!session) return <div className="shell">Set a member JWT or public room id.</div>;
52
+
53
+ return (
54
+ <div className="shell">
55
+ <p className="mode-badge">{session.mode} · presence stage, not WebRTC</p>
56
+ <FluxyRealtimeProvider workerUrl={session.workerUrl} authTokenProvider={session.token} userId={session.userId}>
57
+ <StageBoard roomId={session.roomId} />
58
+ </FluxyRealtimeProvider>
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,75 @@
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
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -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_MEMBER_JWT?: string;
6
+ readonly VITE_FLUXYCHAT_ROOM_ID?: string;
7
+ readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
8
+ readonly VITE_FLUXYCHAT_AGENT_ID?: string;
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }