@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.
- package/CHANGELOG.md +36 -0
- package/dist/index.js +111 -1
- package/package.json +1 -1
- package/readme.md +11 -0
- package/templates/basic/package.json +1 -1
- package/templates/comments-board/.env.example +11 -0
- package/templates/comments-board/README.md +9 -0
- package/templates/comments-board/index.html +12 -0
- package/templates/comments-board/package.json +26 -0
- package/templates/comments-board/src/App.tsx +86 -0
- package/templates/comments-board/src/index.css +75 -0
- package/templates/comments-board/src/main.tsx +10 -0
- package/templates/comments-board/src/session.ts +73 -0
- package/templates/comments-board/src/vite-env.d.ts +13 -0
- package/templates/comments-board/tsconfig.json +21 -0
- package/templates/comments-board/vite.config.ts +7 -0
- package/templates/deal-room/.env.example +11 -0
- package/templates/deal-room/README.md +11 -0
- package/templates/deal-room/index.html +12 -0
- package/templates/deal-room/package.json +25 -0
- package/templates/deal-room/src/App.tsx +110 -0
- package/templates/deal-room/src/index.css +75 -0
- package/templates/deal-room/src/main.tsx +10 -0
- package/templates/deal-room/src/session.ts +73 -0
- package/templates/deal-room/src/vite-env.d.ts +13 -0
- package/templates/deal-room/tsconfig.json +21 -0
- package/templates/deal-room/vite.config.ts +7 -0
- package/templates/discord/package.json +1 -1
- package/templates/draw/.env.example +7 -0
- package/templates/draw/README.md +7 -0
- package/templates/draw/index.html +12 -0
- package/templates/draw/package.json +25 -0
- package/templates/draw/src/App.tsx +91 -0
- package/templates/draw/src/index.css +75 -0
- package/templates/draw/src/main.tsx +10 -0
- package/templates/draw/src/session.ts +73 -0
- package/templates/draw/src/vite-env.d.ts +13 -0
- package/templates/draw/tsconfig.json +21 -0
- package/templates/draw/vite.config.ts +7 -0
- package/templates/fleet-panel/.env.example +11 -0
- package/templates/fleet-panel/README.md +9 -0
- package/templates/fleet-panel/index.html +12 -0
- package/templates/fleet-panel/package.json +25 -0
- package/templates/fleet-panel/src/App.tsx +83 -0
- package/templates/fleet-panel/src/index.css +75 -0
- package/templates/fleet-panel/src/main.tsx +10 -0
- package/templates/fleet-panel/src/session.ts +73 -0
- package/templates/fleet-panel/src/vite-env.d.ts +13 -0
- package/templates/fleet-panel/tsconfig.json +21 -0
- package/templates/fleet-panel/vite.config.ts +7 -0
- package/templates/full/package.json +4 -4
- package/templates/full/scripts/hoist-fluxy-sdk.mjs +1 -1
- package/templates/full/src/App.tsx +20 -6
- package/templates/game-tick/.env.example +11 -0
- package/templates/game-tick/README.md +9 -0
- package/templates/game-tick/index.html +12 -0
- package/templates/game-tick/package.json +25 -0
- package/templates/game-tick/src/App.tsx +87 -0
- package/templates/game-tick/src/index.css +75 -0
- package/templates/game-tick/src/main.tsx +10 -0
- package/templates/game-tick/src/session.ts +73 -0
- package/templates/game-tick/src/vite-env.d.ts +13 -0
- package/templates/game-tick/tsconfig.json +21 -0
- package/templates/game-tick/vite.config.ts +7 -0
- package/templates/iot-panel/.env.example +7 -0
- package/templates/iot-panel/README.md +16 -0
- package/templates/iot-panel/index.html +12 -0
- package/templates/iot-panel/package.json +25 -0
- package/templates/iot-panel/src/App.tsx +97 -0
- package/templates/iot-panel/src/index.css +75 -0
- package/templates/iot-panel/src/main.tsx +10 -0
- package/templates/iot-panel/src/session.ts +73 -0
- package/templates/iot-panel/src/vite-env.d.ts +13 -0
- package/templates/iot-panel/tsconfig.json +21 -0
- package/templates/iot-panel/vite.config.ts +7 -0
- package/templates/javascript-live-cursors/.env.example +9 -0
- package/templates/javascript-live-cursors/README.md +9 -0
- package/templates/javascript-live-cursors/index.html +17 -0
- package/templates/javascript-live-cursors/package.json +18 -0
- package/templates/javascript-live-cursors/src/index.css +31 -0
- package/templates/javascript-live-cursors/src/main.ts +89 -0
- package/templates/javascript-live-cursors/src/vite-env.d.ts +12 -0
- package/templates/javascript-live-cursors/tsconfig.json +13 -0
- package/templates/javascript-live-cursors/vite.config.ts +5 -0
- package/templates/live-cursors/.env.example +9 -0
- package/templates/live-cursors/README.md +20 -0
- package/templates/live-cursors/index.html +12 -0
- package/templates/live-cursors/package.json +25 -0
- package/templates/live-cursors/src/App.tsx +214 -0
- package/templates/live-cursors/src/index.css +94 -0
- package/templates/live-cursors/src/main.tsx +10 -0
- package/templates/live-cursors/src/vite-env.d.ts +12 -0
- package/templates/live-cursors/tsconfig.json +21 -0
- package/templates/live-cursors/vite.config.ts +7 -0
- package/templates/live-cursors-chat/.env.example +9 -0
- package/templates/live-cursors-chat/README.md +11 -0
- package/templates/live-cursors-chat/index.html +12 -0
- package/templates/live-cursors-chat/package.json +25 -0
- package/templates/live-cursors-chat/src/App.tsx +241 -0
- package/templates/live-cursors-chat/src/index.css +94 -0
- package/templates/live-cursors-chat/src/main.tsx +10 -0
- package/templates/live-cursors-chat/src/vite-env.d.ts +12 -0
- package/templates/live-cursors-chat/tsconfig.json +21 -0
- package/templates/live-cursors-chat/vite.config.ts +7 -0
- package/templates/minimal/package.json +4 -4
- package/templates/react/package.json +2 -2
- package/templates/react/src/App.tsx +8 -2
- package/templates/slack/package.json +1 -1
- package/templates/telegram/package.json +1 -1
- package/templates/tiptap-room/.env.example +9 -0
- package/templates/tiptap-room/README.md +14 -0
- package/templates/tiptap-room/index.html +12 -0
- package/templates/tiptap-room/package.json +29 -0
- package/templates/tiptap-room/src/App.tsx +217 -0
- package/templates/tiptap-room/src/index.css +107 -0
- package/templates/tiptap-room/src/main.tsx +10 -0
- package/templates/tiptap-room/src/vite-env.d.ts +12 -0
- package/templates/tiptap-room/tsconfig.json +21 -0
- package/templates/tiptap-room/vite.config.ts +7 -0
- package/templates/voice-stage/.env.example +11 -0
- package/templates/voice-stage/README.md +11 -0
- package/templates/voice-stage/index.html +12 -0
- package/templates/voice-stage/package.json +25 -0
- package/templates/voice-stage/src/App.tsx +61 -0
- package/templates/voice-stage/src/index.css +75 -0
- package/templates/voice-stage/src/main.tsx +10 -0
- package/templates/voice-stage/src/session.ts +73 -0
- package/templates/voice-stage/src/vite-env.d.ts +13 -0
- package/templates/voice-stage/tsconfig.json +21 -0
- package/templates/voice-stage/vite.config.ts +7 -0
- package/templates/war-room/.env.example +11 -0
- package/templates/war-room/README.md +11 -0
- package/templates/war-room/index.html +12 -0
- package/templates/war-room/package.json +26 -0
- package/templates/war-room/src/App.tsx +120 -0
- package/templates/war-room/src/index.css +75 -0
- package/templates/war-room/src/main.tsx +10 -0
- package/templates/war-room/src/session.ts +73 -0
- package/templates/war-room/src/vite-env.d.ts +13 -0
- package/templates/war-room/tsconfig.json +21 -0
- package/templates/war-room/vite.config.ts +7 -0
- package/templates/whiteboard/.env.example +11 -0
- package/templates/whiteboard/README.md +9 -0
- package/templates/whiteboard/index.html +12 -0
- package/templates/whiteboard/package.json +26 -0
- package/templates/whiteboard/src/App.tsx +122 -0
- package/templates/whiteboard/src/index.css +75 -0
- package/templates/whiteboard/src/main.tsx +10 -0
- package/templates/whiteboard/src/session.ts +73 -0
- package/templates/whiteboard/src/vite-env.d.ts +13 -0
- package/templates/whiteboard/tsconfig.json +21 -0
- package/templates/whiteboard/vite.config.ts +7 -0
|
@@ -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,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 fleet panel</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-fleet-panel",
|
|
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,83 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { FluxyRealtimeProvider, useChat, useFluxyChat } from "@fluxy-chat/react";
|
|
3
|
+
import { useFluxySession, workerUrl } from "./session";
|
|
4
|
+
|
|
5
|
+
function FleetBoard({ roomId }: { roomId: string }) {
|
|
6
|
+
const { client } = useFluxyChat();
|
|
7
|
+
const [rows, setRows] = useState<string[]>([]);
|
|
8
|
+
const [vehicleId, setVehicleId] = useState<string | null>(null);
|
|
9
|
+
const { connected } = useChat({
|
|
10
|
+
roomId,
|
|
11
|
+
replay: "request",
|
|
12
|
+
onServerEvent: (ev) => {
|
|
13
|
+
if (ev.name !== "fleet.gps_update") return;
|
|
14
|
+
const d = ev.data;
|
|
15
|
+
setRows((prev) =>
|
|
16
|
+
[`${String(d.vehicleId)} ${String(d.lat)},${String(d.lng)}`, ...prev].slice(0, 40),
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function ensureVehicle() {
|
|
22
|
+
if (!client) return null;
|
|
23
|
+
if (vehicleId) return vehicleId;
|
|
24
|
+
const created = await client.createFleetVehicle({ name: "demo-van" });
|
|
25
|
+
setVehicleId(created.vehicle.id);
|
|
26
|
+
return created.vehicle.id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function pingGps() {
|
|
30
|
+
if (!client) return;
|
|
31
|
+
const id = await ensureVehicle();
|
|
32
|
+
if (!id) return;
|
|
33
|
+
await client.ingestGps({
|
|
34
|
+
vehicleId: id,
|
|
35
|
+
lat: 45.46 + Math.random() * 0.01,
|
|
36
|
+
lng: 9.18 + Math.random() * 0.01,
|
|
37
|
+
roomId,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const curl = `curl -X POST "$VITE_FLUXYCHAT_WORKER_URL/fleet/gps" \\
|
|
42
|
+
-H "Authorization: Bearer $JWT" \\
|
|
43
|
+
-H "Content-Type: application/json" \\
|
|
44
|
+
-d '{"vehicleId":"<id>","lat":45.46,"lng":9.18,"roomId":"${roomId}"}'`;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<section className="panel">
|
|
48
|
+
<header className="chat-header">
|
|
49
|
+
<strong>Fleet · {roomId}</strong>
|
|
50
|
+
<span className="status">{connected ? "listening for fleet.gps_update" : "connecting"}</span>
|
|
51
|
+
</header>
|
|
52
|
+
<p className="hint">{curl}</p>
|
|
53
|
+
<ul className="readings">
|
|
54
|
+
{rows.length === 0 ? <li>No GPS yet.</li> : null}
|
|
55
|
+
{rows.map((row, i) => (
|
|
56
|
+
<li key={`${row}-${i}`}>{row}</li>
|
|
57
|
+
))}
|
|
58
|
+
</ul>
|
|
59
|
+
<div className="composer">
|
|
60
|
+
<button type="button" className="primary" onClick={() => void pingGps()}>
|
|
61
|
+
Post sample GPS
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function App() {
|
|
69
|
+
const { session, loading, error } = useFluxySession();
|
|
70
|
+
if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
|
|
71
|
+
if (loading) return <div className="shell">Starting…</div>;
|
|
72
|
+
if (error) return <div className="shell error">{error}</div>;
|
|
73
|
+
if (!session) return <div className="shell">Set a member JWT or public room id.</div>;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="shell">
|
|
77
|
+
<p className="mode-badge">{session.mode} · HTTP GPS ingest. Not MQTT.</p>
|
|
78
|
+
<FluxyRealtimeProvider workerUrl={session.workerUrl} authTokenProvider={session.token} userId={session.userId}>
|
|
79
|
+
<FleetBoard roomId={session.roomId} />
|
|
80
|
+
</FluxyRealtimeProvider>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"postinstall": "node scripts/hoist-fluxy-sdk.mjs"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@fluxy-chat/react": "^0.1.
|
|
20
|
-
"@fluxy-chat/sdk": "^0.6.
|
|
21
|
-
"@fluxy-chat/ui": "^0.1.
|
|
19
|
+
"@fluxy-chat/react": "^0.1.4",
|
|
20
|
+
"@fluxy-chat/sdk": "^0.6.4",
|
|
21
|
+
"@fluxy-chat/ui": "^0.1.4",
|
|
22
22
|
"class-variance-authority": "^0.7.1",
|
|
23
23
|
"clsx": "^2.1.1",
|
|
24
24
|
"lucide-react": "^0.511.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"pnpm": {
|
|
42
42
|
"overrides": {
|
|
43
|
-
"@fluxy-chat/sdk": "0.6.
|
|
43
|
+
"@fluxy-chat/sdk": "^0.6.4"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -13,5 +13,5 @@ const nestedSdk = join(
|
|
|
13
13
|
|
|
14
14
|
if (existsSync(nestedSdk)) {
|
|
15
15
|
rmSync(nestedSdk, { recursive: true, force: true });
|
|
16
|
-
console.log("[fluxy] removed nested @fluxy-chat/sdk so Vite uses the complete
|
|
16
|
+
console.log("[fluxy] removed nested @fluxy-chat/sdk so Vite uses the hoisted complete package");
|
|
17
17
|
}
|
|
@@ -116,15 +116,24 @@ function LocalOnboarding() {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function ChatRoom({ session }: { session: CliSession }) {
|
|
119
|
-
const {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
const {
|
|
120
|
+
messages,
|
|
121
|
+
sendMessage,
|
|
122
|
+
invokeAgent,
|
|
123
|
+
connectionState,
|
|
124
|
+
agentTyping,
|
|
125
|
+
typingUsers,
|
|
126
|
+
online,
|
|
127
|
+
stopAgentStream,
|
|
128
|
+
} = useChat({
|
|
129
|
+
roomId: session.roomId,
|
|
130
|
+
agentId: session.agentId || undefined,
|
|
131
|
+
markReadLatest: true,
|
|
132
|
+
});
|
|
125
133
|
|
|
126
134
|
const [error, setError] = useState<string | null>(null);
|
|
127
135
|
const connected = connectionState.status === "connected";
|
|
136
|
+
const isStreaming = messages.some((m) => m.streaming);
|
|
128
137
|
|
|
129
138
|
async function onSend(content: string) {
|
|
130
139
|
setError(null);
|
|
@@ -152,6 +161,11 @@ function ChatRoom({ session }: { session: CliSession }) {
|
|
|
152
161
|
</span>
|
|
153
162
|
<span className="text-muted-foreground"> · {session.roomId}</span>
|
|
154
163
|
</span>
|
|
164
|
+
{isStreaming ? (
|
|
165
|
+
<button type="button" className="btn-ghost" onClick={() => stopAgentStream()}>
|
|
166
|
+
Stop generation
|
|
167
|
+
</button>
|
|
168
|
+
) : null}
|
|
155
169
|
</div>
|
|
156
170
|
|
|
157
171
|
<div className="chat-frame">
|
|
@@ -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,9 @@
|
|
|
1
|
+
# FluxyChat game tick
|
|
2
|
+
|
|
3
|
+
Lobby → match → `POST /games/matches/:id/input` fans out `server_event` `game.tick` on the same room WebSocket. **Not a netcode engine.**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @fluxy-chat/create-fluxy-chat@latest my-game --example game-tick
|
|
7
|
+
cp .env.example .env
|
|
8
|
+
npm run dev
|
|
9
|
+
```
|
|
@@ -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 game tick</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-game-tick",
|
|
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,87 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { createWorkerFluxyGameClient } from "@fluxy-chat/sdk";
|
|
3
|
+
import { FluxyRealtimeProvider, useChat, useFluxyChat } from "@fluxy-chat/react";
|
|
4
|
+
import { useFluxySession, workerUrl } from "./session";
|
|
5
|
+
|
|
6
|
+
function GameBoard({ roomId, userId }: { roomId: string; userId: string }) {
|
|
7
|
+
const { client } = useFluxyChat();
|
|
8
|
+
const game = useMemo(() => (client ? createWorkerFluxyGameClient(client) : null), [client]);
|
|
9
|
+
const [ticks, setTicks] = useState<string[]>([]);
|
|
10
|
+
const [matchId, setMatchId] = useState<string | null>(null);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const { connected } = useChat({
|
|
13
|
+
roomId,
|
|
14
|
+
replay: "request",
|
|
15
|
+
onServerEvent: (ev) => {
|
|
16
|
+
if (ev.name === "game.tick" || ev.name === "game.match_started") {
|
|
17
|
+
setTicks((prev) => [`${ev.name} ${JSON.stringify(ev.data)}`, ...prev].slice(0, 40));
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function start() {
|
|
23
|
+
if (!game) return;
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
const gameMode = `demo-${Date.now()}`;
|
|
27
|
+
await game.matchmake({ playerId: userId, roomId, gameMode, maxPlayers: 2 });
|
|
28
|
+
const lobby = await game.matchmake({
|
|
29
|
+
playerId: `${userId}-cpu`,
|
|
30
|
+
roomId,
|
|
31
|
+
gameMode,
|
|
32
|
+
maxPlayers: 2,
|
|
33
|
+
});
|
|
34
|
+
const match = await game.startMatch(lobby.lobbyId);
|
|
35
|
+
setMatchId(match.matchId);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
setError(err instanceof Error ? err.message : "match_failed");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function inputTick() {
|
|
42
|
+
if (!game || !matchId) return;
|
|
43
|
+
await game.submitInput(matchId, { playerId: userId, actions: { move: "nudge" } });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<section className="panel">
|
|
48
|
+
<header className="chat-header">
|
|
49
|
+
<strong>Game tick · {roomId}</strong>
|
|
50
|
+
<span className="status">{connected ? "listening for game.tick" : "connecting"}</span>
|
|
51
|
+
</header>
|
|
52
|
+
<p className="hint">Not netcode. Matchmake binds this room, then input posts a server_event tick.</p>
|
|
53
|
+
{error ? <p className="error" style={{ padding: "0 1rem" }}>{error}</p> : null}
|
|
54
|
+
<ul className="readings">
|
|
55
|
+
{ticks.length === 0 ? <li>No ticks yet.</li> : null}
|
|
56
|
+
{ticks.map((row, i) => (
|
|
57
|
+
<li key={`${i}-${row.slice(0, 24)}`}>{row}</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
<div className="composer">
|
|
61
|
+
<button type="button" className="primary" onClick={() => void start()}>
|
|
62
|
+
Matchmake + start
|
|
63
|
+
</button>
|
|
64
|
+
<button type="button" className="primary" disabled={!matchId} onClick={() => void inputTick()}>
|
|
65
|
+
Submit input
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function App() {
|
|
73
|
+
const { session, loading, error } = useFluxySession();
|
|
74
|
+
if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
|
|
75
|
+
if (loading) return <div className="shell">Starting…</div>;
|
|
76
|
+
if (error) return <div className="shell error">{error}</div>;
|
|
77
|
+
if (!session) return <div className="shell">Set a member JWT or public room id.</div>;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="shell">
|
|
81
|
+
<p className="mode-badge">{session.mode} · server_event ticks, not rollback netcode</p>
|
|
82
|
+
<FluxyRealtimeProvider workerUrl={session.workerUrl} authTokenProvider={session.token} userId={session.userId}>
|
|
83
|
+
<GameBoard roomId={session.roomId} userId={session.userId} />
|
|
84
|
+
</FluxyRealtimeProvider>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -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
|
+
}
|