@fluxy-chat/create-fluxy-chat 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +18 -5
- package/package.json +2 -2
- package/readme.md +11 -11
- package/templates/hr-feedback/.env.example +9 -0
- package/templates/hr-feedback/README.md +45 -0
- package/templates/hr-feedback/package.json +19 -0
- package/templates/hr-feedback/src/feedback.ts +71 -0
- package/templates/hr-feedback/src/index.ts +22 -0
- package/templates/hr-feedback/tsconfig.json +18 -0
- package/templates/hr-feedback/wrangler.toml +7 -0
- package/templates/minimal/README.md +1 -1
- package/templates/react/.env.example +4 -3
- package/templates/react/README.md +17 -7
- package/templates/react/src/App.tsx +116 -23
- package/templates/react/src/index.css +10 -0
- package/templates/react/src/vite-env.d.ts +3 -2
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ function isPackageManager(value) {
|
|
|
30
30
|
return PACKAGE_MANAGERS.has(value);
|
|
31
31
|
}
|
|
32
32
|
function isAdapterType(value) {
|
|
33
|
-
return ["basic", "slack", "telegram", "discord", "web", "react"].includes(value);
|
|
33
|
+
return ["basic", "slack", "telegram", "discord", "web", "react", "hr-feedback"].includes(value);
|
|
34
34
|
}
|
|
35
35
|
function detectPackageManagerFromLockfiles(cwd) {
|
|
36
36
|
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
|
|
@@ -131,6 +131,7 @@ async function runPrompts(inputs) {
|
|
|
131
131
|
options: [
|
|
132
132
|
{ label: "Minimal chat widget (ui-kit, recommended)", value: "minimal" },
|
|
133
133
|
{ label: "React chat app (Vite + useChat)", value: "react" },
|
|
134
|
+
{ label: "HR anonymous feedback (compliance starter)", value: "hr-feedback" },
|
|
134
135
|
{ label: "Basic (Cloudflare Workers bot)", value: "basic" },
|
|
135
136
|
{ label: "Slack", value: "slack" },
|
|
136
137
|
{ label: "Telegram", value: "telegram" },
|
|
@@ -825,8 +826,10 @@ function parseArgs(argv) {
|
|
|
825
826
|
args.adapter = value;
|
|
826
827
|
} else if (value === "react") {
|
|
827
828
|
args.adapter = "react";
|
|
829
|
+
} else if (value === "hr-feedback") {
|
|
830
|
+
args.adapter = "hr-feedback";
|
|
828
831
|
} else {
|
|
829
|
-
console.error(`Invalid template: ${value}. Choose: react, basic, slack, telegram, discord, web`);
|
|
832
|
+
console.error(`Invalid template: ${value}. Choose: react, basic, slack, telegram, discord, web, hr-feedback`);
|
|
830
833
|
process.exit(1);
|
|
831
834
|
}
|
|
832
835
|
} else if (arg === "--adapter" || arg === "-a") {
|
|
@@ -834,7 +837,7 @@ function parseArgs(argv) {
|
|
|
834
837
|
if (value && isAdapterType(value)) {
|
|
835
838
|
args.adapter = value;
|
|
836
839
|
} else {
|
|
837
|
-
console.error(`Invalid adapter: ${value}. Choose: react, basic, slack, telegram, discord, web`);
|
|
840
|
+
console.error(`Invalid adapter: ${value}. Choose: react, basic, slack, telegram, discord, web, hr-feedback`);
|
|
838
841
|
process.exit(1);
|
|
839
842
|
}
|
|
840
843
|
} else if (arg === "--pm" || arg === "--package-manager") {
|
|
@@ -872,8 +875,8 @@ ${pc.bold("Usage:")}
|
|
|
872
875
|
npx create-fluxy-chat [project-name] [options]
|
|
873
876
|
|
|
874
877
|
${pc.bold("Options:")}
|
|
875
|
-
-a, --adapter <type> Adapter: react, basic, slack, telegram, discord, web
|
|
876
|
-
-t, --template <type> Alias for --adapter (e.g. react)
|
|
878
|
+
-a, --adapter <type> Adapter: react, basic, slack, telegram, discord, web, hr-feedback
|
|
879
|
+
-t, --template <type> Alias for --adapter (e.g. react, hr-feedback)
|
|
877
880
|
--pm <manager> Package manager: npm, pnpm, yarn
|
|
878
881
|
-l, --language <lang> Language: typescript (default) or javascript
|
|
879
882
|
-y, --yes Skip prompts and accept defaults
|
|
@@ -884,6 +887,7 @@ ${pc.bold("Options:")}
|
|
|
884
887
|
|
|
885
888
|
${pc.bold("Examples:")}
|
|
886
889
|
${pc.cyan("npx create-fluxy-chat my-chat --minimal")}
|
|
890
|
+
${pc.cyan("npx create-fluxy-chat my-hr-bot --template hr-feedback")}
|
|
887
891
|
${pc.cyan("npx create-fluxy-chat my-chat --template react")}
|
|
888
892
|
${pc.cyan("npx create-fluxy-chat my-bot --adapter basic")}
|
|
889
893
|
${pc.cyan("npx create-fluxy-chat my-bot --adapter slack")}
|
|
@@ -942,6 +946,14 @@ async function main() {
|
|
|
942
946
|
pkg.name = config.name;
|
|
943
947
|
writeJson(projectDir, "package.json", pkg);
|
|
944
948
|
s.stop("React chat app created.");
|
|
949
|
+
} else if (config.adapter === "hr-feedback") {
|
|
950
|
+
const templateRoot = path2.join(templatesDir(), "hr-feedback");
|
|
951
|
+
copyDir(templateRoot, projectDir);
|
|
952
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
953
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
954
|
+
pkg.name = config.name;
|
|
955
|
+
writeJson(projectDir, "package.json", pkg);
|
|
956
|
+
s.stop("HR feedback starter created.");
|
|
945
957
|
} else {
|
|
946
958
|
writeJson(projectDir, "package.json", generatePackageJson(config));
|
|
947
959
|
if (config.language === "typescript") {
|
|
@@ -993,6 +1005,7 @@ async function main() {
|
|
|
993
1005
|
config.adapter === "react" ? [
|
|
994
1006
|
`cd ${config.name}`,
|
|
995
1007
|
"cp .env.example .env",
|
|
1008
|
+
"# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLIC_ROOM_ID (guest) or MEMBER_JWT",
|
|
996
1009
|
`${config.packageManager === "npm" ? "npm run" : config.packageManager} dev`
|
|
997
1010
|
].join("\n") : [
|
|
998
1011
|
`cd ${config.name}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluxy-chat/create-fluxy-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Scaffold a new FluxyChat bot project with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|
|
41
|
-
"build": "tsup src/index.ts --format esm
|
|
41
|
+
"build": "tsup src/index.ts --format esm",
|
|
42
42
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
43
43
|
"typecheck": "tsc --noEmit"
|
|
44
44
|
}
|
package/readme.md
CHANGED
|
@@ -5,10 +5,10 @@ Scaffold a new [FluxyChat](https://github.com/AlessandroFare/fluxychat) bot proj
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Minimal chat widget (recommended
|
|
8
|
+
# Minimal chat widget (recommended: 3 lines in App.tsx)
|
|
9
9
|
npx create-fluxy-chat my-chat --minimal
|
|
10
10
|
|
|
11
|
-
# React + useChat (
|
|
11
|
+
# React + useChat (guest room ~60s or member JWT)
|
|
12
12
|
npx create-fluxy-chat my-chat --template react
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -42,7 +42,7 @@ npx create-fluxy-chat my-bot --adapter basic
|
|
|
42
42
|
| `--pm <manager>` | | Package manager: `npm`, `pnpm`, `yarn` |
|
|
43
43
|
| `--language <lang>` | `-l` | Language: `typescript` (default) or `javascript` |
|
|
44
44
|
| `--yes` | `-y` | Skip prompts and accept defaults |
|
|
45
|
-
| `--minimal` | | Chat-only widget (`@fluxy-chat/ui-kit`)
|
|
45
|
+
| `--minimal` | | Chat-only widget (`@fluxy-chat/ui-kit`), no platform modules |
|
|
46
46
|
| `--skip-install` | | Skip dependency installation |
|
|
47
47
|
| `--no-git` | | Skip git repository initialization |
|
|
48
48
|
| `--help` | `-h` | Show help |
|
|
@@ -62,14 +62,14 @@ npx create-fluxy-chat my-bot --adapter basic
|
|
|
62
62
|
|
|
63
63
|
Each generated project includes:
|
|
64
64
|
|
|
65
|
-
- **`src/index.ts
|
|
66
|
-
- **`src/bot.ts
|
|
67
|
-
- **`fluxy.config.ts
|
|
68
|
-
- **`wrangler.toml
|
|
69
|
-
- **`.dev.vars
|
|
70
|
-
- **`.env.example
|
|
71
|
-
- **`tsconfig.json
|
|
72
|
-
- **`README.md
|
|
65
|
+
- **`src/index.ts`**: Cloudflare Workers entry point with route handling
|
|
66
|
+
- **`src/bot.ts`**: Bot handler using `@fluxy-chat/sdk`
|
|
67
|
+
- **`fluxy.config.ts`**: Room authz and publish middleware (basic template)
|
|
68
|
+
- **`wrangler.toml`**: Cloudflare Workers deployment config
|
|
69
|
+
- **`.dev.vars`**: Local development environment variables
|
|
70
|
+
- **`.env.example`**: Example environment variables for your adapter
|
|
71
|
+
- **`tsconfig.json`**: TypeScript configuration (for TS projects)
|
|
72
|
+
- **`README.md`**: Project-specific setup instructions
|
|
73
73
|
|
|
74
74
|
## Package manager detection
|
|
75
75
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# FluxyChat worker URL + service API key (server-side only)
|
|
2
|
+
FLUXY_BASE_URL=https://your-fluxychat-worker.example.com
|
|
3
|
+
FLUXY_API_KEY=your-api-key-here
|
|
4
|
+
|
|
5
|
+
# Optional dedicated room for aggregated summaries
|
|
6
|
+
HR_FEEDBACK_ROOM_ID=hr-anonymous-feedback
|
|
7
|
+
|
|
8
|
+
# Optional webhook for Path B HR escalation (de-identified payload only)
|
|
9
|
+
HR_ESCALATION_WEBHOOK_URL=
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# HR Anonymous Feedback Starter
|
|
2
|
+
|
|
3
|
+
Production starter for anonymous employee feedback with sensitive classification and privacy-safe audit.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Path A** — routine feedback → aggregated anonymous summary room
|
|
8
|
+
- **Path B** — sensitive categories → HR escalation hook (category + confidence only, no identity)
|
|
9
|
+
- Reuses FluxyChat `approvalChain` + room timeline audit when wired to agent tools
|
|
10
|
+
- Classification runs on the worker via `POST /anonymous-feedback`
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cp .env.example .dev.vars
|
|
16
|
+
# fill FLUXY_BASE_URL + FLUXY_API_KEY
|
|
17
|
+
npm install
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Submit feedback:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
curl -X POST http://localhost:8787/feedback \
|
|
25
|
+
-H "Content-Type: application/json" \
|
|
26
|
+
-d '{"content":"My manager makes hostile comments in meetings"}'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Endpoints
|
|
30
|
+
|
|
31
|
+
| Route | Description |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `GET /` | Health check |
|
|
34
|
+
| `POST /feedback` | Anonymous submission (no user id stored) |
|
|
35
|
+
|
|
36
|
+
## Privacy
|
|
37
|
+
|
|
38
|
+
- Raw message content is **not** persisted by this template
|
|
39
|
+
- Worker audit stores **category + timestamp + path** only
|
|
40
|
+
- Configure `HR_ESCALATION_WEBHOOK_URL` for your HRIS / ticketing integration
|
|
41
|
+
|
|
42
|
+
## Learn more
|
|
43
|
+
|
|
44
|
+
- [FluxyChat docs](https://github.com/AlessandroFare/fluxychat)
|
|
45
|
+
- Worker route: `POST /anonymous-feedback` (JWT) for in-app widgets
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hr-feedback-bot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"type-check": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@fluxy-chat/sdk": "latest"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@cloudflare/workers-types": "^4.0.0",
|
|
16
|
+
"typescript": "^5.6.0",
|
|
17
|
+
"wrangler": "^3.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
interface FeedbackEnv {
|
|
2
|
+
FLUXY_BASE_URL: string;
|
|
3
|
+
FLUXY_API_KEY: string;
|
|
4
|
+
HR_FEEDBACK_ROOM_ID?: string;
|
|
5
|
+
HR_ESCALATION_WEBHOOK_URL?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface FeedbackBody {
|
|
9
|
+
content?: string;
|
|
10
|
+
roomId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FeedbackResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
path?: string;
|
|
16
|
+
category?: string;
|
|
17
|
+
confidence?: number;
|
|
18
|
+
message?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Submit anonymous feedback through the FluxyChat worker classifier.
|
|
24
|
+
* Content is classified server-side; only metadata is returned.
|
|
25
|
+
*/
|
|
26
|
+
export async function submitHrFeedback(
|
|
27
|
+
env: FeedbackEnv,
|
|
28
|
+
body: FeedbackBody,
|
|
29
|
+
): Promise<FeedbackResult> {
|
|
30
|
+
const content = String(body.content ?? "").trim();
|
|
31
|
+
if (!content) return { ok: false, error: "content required" };
|
|
32
|
+
if (content.length > 8000) return { ok: false, error: "content_too_long" };
|
|
33
|
+
|
|
34
|
+
const baseUrl = env.FLUXY_BASE_URL.replace(/\/$/, "");
|
|
35
|
+
const res = await fetch(`${baseUrl}/anonymous-feedback`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
Authorization: `Bearer ${env.FLUXY_API_KEY}`,
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
content,
|
|
43
|
+
roomId: body.roomId ?? env.HR_FEEDBACK_ROOM_ID ?? null,
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const data = (await res.json().catch(() => ({}))) as FeedbackResult;
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
return { ok: false, error: data.error ?? `worker_${res.status}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (data.path === "hr_escalation" && env.HR_ESCALATION_WEBHOOK_URL) {
|
|
53
|
+
await fetch(env.HR_ESCALATION_WEBHOOK_URL, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
category: data.category,
|
|
58
|
+
confidence: data.confidence,
|
|
59
|
+
at: new Date().toISOString(),
|
|
60
|
+
}),
|
|
61
|
+
}).catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
path: data.path,
|
|
67
|
+
category: data.category,
|
|
68
|
+
confidence: data.confidence,
|
|
69
|
+
message: data.message,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { submitHrFeedback } from "./feedback.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
async fetch(request: Request, env: Record<string, string>): Promise<Response> {
|
|
5
|
+
const url = new URL(request.url);
|
|
6
|
+
|
|
7
|
+
if (url.pathname === "/" && request.method === "GET") {
|
|
8
|
+
return new Response("HR anonymous feedback bot is running.", { status: 200 });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (url.pathname === "/feedback" && request.method === "POST") {
|
|
12
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
13
|
+
content?: string;
|
|
14
|
+
roomId?: string;
|
|
15
|
+
};
|
|
16
|
+
const result = await submitHrFeedback(env, body);
|
|
17
|
+
return Response.json(result, { status: result.ok ? 200 : 400 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return new Response("Not Found", { status: 404 });
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["@cloudflare/workers-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"noEmit": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# FluxyChat worker URL (hosted or self-hosted)
|
|
2
2
|
VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# Option A — member JWT (~2 min via onboarding)
|
|
5
5
|
VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
|
|
6
|
-
|
|
7
|
-
# Demo room — create in console or onboarding
|
|
8
6
|
VITE_FLUXYCHAT_ROOM_ID=demo
|
|
7
|
+
|
|
8
|
+
# Option B — public guest room (~60s, no signup)
|
|
9
|
+
# VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
# FluxyChat React starter
|
|
2
2
|
|
|
3
|
-
Minimal Vite + React app with `useChat
|
|
3
|
+
Minimal Vite + React app with `useChat`. First message in about 60 seconds via the public guest room.
|
|
4
4
|
|
|
5
|
-
## Quick start
|
|
5
|
+
## Quick start (guest, fastest)
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
cp .env.example .env
|
|
9
|
-
#
|
|
9
|
+
# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLIC_ROOM_ID (public room from console)
|
|
10
10
|
npm install
|
|
11
11
|
npm run dev
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Open http://localhost:5173
|
|
14
|
+
Open http://localhost:5173. A guest JWT is minted automatically via `joinPublicRoomAsGuest`.
|
|
15
|
+
|
|
16
|
+
## Quick start (member JWT)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cp .env.example .env
|
|
20
|
+
# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_MEMBER_JWT + VITE_FLUXYCHAT_ROOM_ID
|
|
21
|
+
npm install
|
|
22
|
+
npm run dev
|
|
23
|
+
```
|
|
15
24
|
|
|
16
25
|
## Get credentials
|
|
17
26
|
|
|
18
|
-
1. **
|
|
19
|
-
2. **
|
|
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
|
|
20
30
|
|
|
21
31
|
## Scripts
|
|
22
32
|
|
|
@@ -28,6 +38,6 @@ Open http://localhost:5173 and send a message.
|
|
|
28
38
|
|
|
29
39
|
## Next steps
|
|
30
40
|
|
|
31
|
-
- Add `@fluxy-chat/ui`
|
|
41
|
+
- Add `@fluxy-chat/ui` themes (`default`, `dark`, `minimal`, `brand`)
|
|
32
42
|
- Use `useInbox` for unified feed
|
|
33
43
|
- See [chat-only quickstart](https://docs.fluxychat.com/docs/getting-started/chat-only-quickstart)
|
|
@@ -1,10 +1,82 @@
|
|
|
1
|
-
import { useMemo, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { FluxyChatClient } from "@fluxy-chat/sdk";
|
|
3
3
|
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
|
|
4
4
|
|
|
5
|
-
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL;
|
|
6
|
-
const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT;
|
|
7
|
-
const
|
|
5
|
+
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
|
|
6
|
+
const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
|
|
7
|
+
const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
|
|
8
|
+
const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
|
|
9
|
+
|
|
10
|
+
interface FluxySession {
|
|
11
|
+
client: FluxyChatClient;
|
|
12
|
+
roomId: string;
|
|
13
|
+
mode: "member" | "guest";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function useFluxySession(): {
|
|
17
|
+
session: FluxySession | null;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
error: string | null;
|
|
20
|
+
} {
|
|
21
|
+
const [session, setSession] = useState<FluxySession | null>(null);
|
|
22
|
+
const [loading, setLoading] = useState(Boolean(workerUrl));
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!workerUrl) {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (memberJwt) {
|
|
32
|
+
setSession({
|
|
33
|
+
client: new FluxyChatClient({
|
|
34
|
+
baseUrl: workerUrl,
|
|
35
|
+
userId: "demo-user",
|
|
36
|
+
token: memberJwt,
|
|
37
|
+
}),
|
|
38
|
+
roomId: configuredRoomId,
|
|
39
|
+
mode: "member",
|
|
40
|
+
});
|
|
41
|
+
setLoading(false);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (publicRoomId) {
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
|
|
48
|
+
displayName: "Guest",
|
|
49
|
+
})
|
|
50
|
+
.then((guest) => {
|
|
51
|
+
if (cancelled) return;
|
|
52
|
+
setSession({
|
|
53
|
+
client: new FluxyChatClient({
|
|
54
|
+
baseUrl: workerUrl,
|
|
55
|
+
userId: guest.userId,
|
|
56
|
+
token: guest.token,
|
|
57
|
+
}),
|
|
58
|
+
roomId: guest.roomId,
|
|
59
|
+
mode: "guest",
|
|
60
|
+
});
|
|
61
|
+
})
|
|
62
|
+
.catch((err) => {
|
|
63
|
+
if (!cancelled) {
|
|
64
|
+
setError(err instanceof Error ? err.message : "Guest session failed");
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.finally(() => {
|
|
68
|
+
if (!cancelled) setLoading(false);
|
|
69
|
+
});
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return { session, loading, error };
|
|
79
|
+
}
|
|
8
80
|
|
|
9
81
|
function ChatPanel({ roomId }: { roomId: string }) {
|
|
10
82
|
const { messages, sendMessage, connectionState } = useChat({
|
|
@@ -50,31 +122,49 @@ function ChatPanel({ roomId }: { roomId: string }) {
|
|
|
50
122
|
}
|
|
51
123
|
|
|
52
124
|
export function App() {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
token: memberJwt.trim(),
|
|
61
|
-
});
|
|
62
|
-
}, []);
|
|
125
|
+
const { session, loading, error } = useFluxySession();
|
|
126
|
+
const [roomId, setRoomId] = useState(configuredRoomId);
|
|
127
|
+
|
|
128
|
+
const activeRoomId = useMemo(() => {
|
|
129
|
+
if (session?.mode === "guest") return session.roomId;
|
|
130
|
+
return roomId.trim() || configuredRoomId;
|
|
131
|
+
}, [session, roomId]);
|
|
63
132
|
|
|
64
|
-
if (!
|
|
133
|
+
if (!workerUrl) {
|
|
65
134
|
return (
|
|
66
135
|
<main className="shell">
|
|
67
136
|
<h1>FluxyChat — chat-only starter</h1>
|
|
68
137
|
<p>
|
|
69
138
|
Copy <code>.env.example</code> to <code>.env</code> and set{" "}
|
|
70
|
-
<code>VITE_FLUXYCHAT_WORKER_URL</code
|
|
139
|
+
<code>VITE_FLUXYCHAT_WORKER_URL</code>.
|
|
140
|
+
</p>
|
|
141
|
+
<p>
|
|
142
|
+
Then either <code>VITE_FLUXYCHAT_MEMBER_JWT</code> (member) or{" "}
|
|
143
|
+
<code>VITE_FLUXYCHAT_PUBLIC_ROOM_ID</code> (guest, ~60s to first message).
|
|
71
144
|
</p>
|
|
145
|
+
</main>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (loading) {
|
|
150
|
+
return (
|
|
151
|
+
<main className="shell">
|
|
152
|
+
<p>Connecting…</p>
|
|
153
|
+
</main>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (error || !session) {
|
|
158
|
+
return (
|
|
159
|
+
<main className="shell">
|
|
160
|
+
<h1>FluxyChat</h1>
|
|
161
|
+
<p className="error">{error ?? "Set JWT or public room ID in .env"}</p>
|
|
72
162
|
<p>
|
|
73
163
|
Get credentials from{" "}
|
|
74
164
|
<a href="https://fluxychat.com/onboarding" target="_blank" rel="noreferrer">
|
|
75
165
|
onboarding
|
|
76
166
|
</a>{" "}
|
|
77
|
-
or
|
|
167
|
+
or use a public room ID for guest mode.
|
|
78
168
|
</p>
|
|
79
169
|
</main>
|
|
80
170
|
);
|
|
@@ -83,12 +173,15 @@ export function App() {
|
|
|
83
173
|
return (
|
|
84
174
|
<main className="shell">
|
|
85
175
|
<h1>FluxyChat</h1>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
176
|
+
<p className="mode-badge">{session.mode === "guest" ? "Guest session" : "Member JWT"}</p>
|
|
177
|
+
{session.mode === "member" ? (
|
|
178
|
+
<label className="room-picker">
|
|
179
|
+
Room
|
|
180
|
+
<input value={roomId} onChange={(e) => setRoomId(e.target.value)} />
|
|
181
|
+
</label>
|
|
182
|
+
) : null}
|
|
183
|
+
<FluxyRealtimeProvider client={session.client}>
|
|
184
|
+
<ChatPanel roomId={activeRoomId} />
|
|
92
185
|
</FluxyRealtimeProvider>
|
|
93
186
|
</main>
|
|
94
187
|
);
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
interface ImportMetaEnv {
|
|
4
4
|
readonly VITE_FLUXYCHAT_WORKER_URL: string;
|
|
5
|
-
readonly VITE_FLUXYCHAT_MEMBER_JWT
|
|
6
|
-
readonly VITE_FLUXYCHAT_ROOM_ID
|
|
5
|
+
readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
|
|
6
|
+
readonly VITE_FLUXYCHAT_ROOM_ID?: string;
|
|
7
|
+
readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
interface ImportMeta {
|