@fluxy-chat/create-fluxy-chat 0.2.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 +101 -22
- package/package.json +2 -2
- package/readme.md +21 -11
- package/templates/basic/package.json +1 -1
- package/templates/discord/package.json +1 -1
- 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/.env.example +3 -0
- package/templates/minimal/README.md +14 -0
- package/templates/minimal/index.html +12 -0
- package/templates/minimal/package.json +26 -0
- package/templates/minimal/src/App.tsx +24 -0
- package/templates/minimal/src/index.css +13 -0
- package/templates/minimal/src/main.tsx +10 -0
- package/templates/minimal/src/vite-env.d.ts +7 -0
- package/templates/minimal/tsconfig.json +17 -0
- package/templates/minimal/vite.config.ts +7 -0
- package/templates/react/.env.example +9 -0
- package/templates/react/README.md +43 -0
- package/templates/react/index.html +12 -0
- package/templates/react/package.json +25 -0
- package/templates/react/src/App.tsx +188 -0
- package/templates/react/src/index.css +116 -0
- package/templates/react/src/main.tsx +10 -0
- package/templates/react/src/vite-env.d.ts +12 -0
- package/templates/react/tsconfig.json +21 -0
- package/templates/react/vite.config.ts +7 -0
- package/templates/slack/package.json +1 -1
- package/templates/telegram/package.json +1 -1
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"].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"))) {
|
|
@@ -79,6 +79,25 @@ function writeJson(projectDir, filePath, value) {
|
|
|
79
79
|
writeFile(projectDir, filePath, `${JSON.stringify(value, null, 2)}
|
|
80
80
|
`);
|
|
81
81
|
}
|
|
82
|
+
function copyDir(source, destination) {
|
|
83
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
84
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
85
|
+
const srcPath = path.join(source, entry.name);
|
|
86
|
+
const destPath = path.join(destination, entry.name);
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
copyDir(srcPath, destPath);
|
|
89
|
+
} else {
|
|
90
|
+
fs.copyFileSync(srcPath, destPath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function templatesDir() {
|
|
95
|
+
return path.resolve(
|
|
96
|
+
path.dirname(new URL(import.meta.url).pathname.replace(/^\//, "")),
|
|
97
|
+
"..",
|
|
98
|
+
"templates"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
82
101
|
|
|
83
102
|
// src/prompts.ts
|
|
84
103
|
var DEFAULT_PROJECT_NAME = "my-fluxy-bot";
|
|
@@ -101,15 +120,19 @@ async function runPrompts(inputs) {
|
|
|
101
120
|
if (nameError) {
|
|
102
121
|
throw new Error(nameError);
|
|
103
122
|
}
|
|
123
|
+
const minimal = inputs.minimal ?? false;
|
|
104
124
|
let adapter = inputs.adapter;
|
|
105
|
-
if (!adapter) {
|
|
125
|
+
if (!minimal && !adapter) {
|
|
106
126
|
if (inputs.yes) {
|
|
107
|
-
adapter = "
|
|
127
|
+
adapter = "react";
|
|
108
128
|
} else {
|
|
109
129
|
const result = await select({
|
|
110
130
|
message: "Select an adapter:",
|
|
111
131
|
options: [
|
|
112
|
-
{ label: "
|
|
132
|
+
{ label: "Minimal chat widget (ui-kit, recommended)", value: "minimal" },
|
|
133
|
+
{ label: "React chat app (Vite + useChat)", value: "react" },
|
|
134
|
+
{ label: "HR anonymous feedback (compliance starter)", value: "hr-feedback" },
|
|
135
|
+
{ label: "Basic (Cloudflare Workers bot)", value: "basic" },
|
|
113
136
|
{ label: "Slack", value: "slack" },
|
|
114
137
|
{ label: "Telegram", value: "telegram" },
|
|
115
138
|
{ label: "Discord", value: "discord" },
|
|
@@ -117,6 +140,9 @@ async function runPrompts(inputs) {
|
|
|
117
140
|
]
|
|
118
141
|
});
|
|
119
142
|
if (isCancel(result)) return null;
|
|
143
|
+
if (result === "minimal") {
|
|
144
|
+
return runPrompts({ ...inputs, minimal: true, adapter: "react" });
|
|
145
|
+
}
|
|
120
146
|
adapter = result;
|
|
121
147
|
}
|
|
122
148
|
}
|
|
@@ -167,11 +193,12 @@ async function runPrompts(inputs) {
|
|
|
167
193
|
if (isCancel(shouldInitGit)) return null;
|
|
168
194
|
return {
|
|
169
195
|
name,
|
|
170
|
-
adapter,
|
|
196
|
+
adapter: adapter ?? "react",
|
|
171
197
|
packageManager,
|
|
172
198
|
language,
|
|
173
199
|
shouldInstall,
|
|
174
|
-
shouldInitGit
|
|
200
|
+
shouldInitGit,
|
|
201
|
+
minimal: minimal || inputs.minimal === true
|
|
175
202
|
};
|
|
176
203
|
}
|
|
177
204
|
|
|
@@ -775,6 +802,7 @@ var execAsync = promisify(exec);
|
|
|
775
802
|
function parseArgs(argv) {
|
|
776
803
|
const args = {
|
|
777
804
|
yes: false,
|
|
805
|
+
minimal: false,
|
|
778
806
|
skipInstall: false,
|
|
779
807
|
noGit: false,
|
|
780
808
|
help: false
|
|
@@ -786,16 +814,30 @@ function parseArgs(argv) {
|
|
|
786
814
|
args.help = true;
|
|
787
815
|
} else if (arg === "-y" || arg === "--yes") {
|
|
788
816
|
args.yes = true;
|
|
817
|
+
} else if (arg === "--minimal") {
|
|
818
|
+
args.minimal = true;
|
|
789
819
|
} else if (arg === "--skip-install") {
|
|
790
820
|
args.skipInstall = true;
|
|
791
821
|
} else if (arg === "--no-git") {
|
|
792
822
|
args.noGit = true;
|
|
823
|
+
} else if (arg === "--template" || arg === "-t") {
|
|
824
|
+
const value = argv[++i];
|
|
825
|
+
if (value && isAdapterType(value)) {
|
|
826
|
+
args.adapter = value;
|
|
827
|
+
} else if (value === "react") {
|
|
828
|
+
args.adapter = "react";
|
|
829
|
+
} else if (value === "hr-feedback") {
|
|
830
|
+
args.adapter = "hr-feedback";
|
|
831
|
+
} else {
|
|
832
|
+
console.error(`Invalid template: ${value}. Choose: react, basic, slack, telegram, discord, web, hr-feedback`);
|
|
833
|
+
process.exit(1);
|
|
834
|
+
}
|
|
793
835
|
} else if (arg === "--adapter" || arg === "-a") {
|
|
794
836
|
const value = argv[++i];
|
|
795
837
|
if (value && isAdapterType(value)) {
|
|
796
838
|
args.adapter = value;
|
|
797
839
|
} else {
|
|
798
|
-
console.error(`Invalid adapter: ${value}. Choose: basic, slack, telegram, discord, web`);
|
|
840
|
+
console.error(`Invalid adapter: ${value}. Choose: react, basic, slack, telegram, discord, web, hr-feedback`);
|
|
799
841
|
process.exit(1);
|
|
800
842
|
}
|
|
801
843
|
} else if (arg === "--pm" || arg === "--package-manager") {
|
|
@@ -833,16 +875,21 @@ ${pc.bold("Usage:")}
|
|
|
833
875
|
npx create-fluxy-chat [project-name] [options]
|
|
834
876
|
|
|
835
877
|
${pc.bold("Options:")}
|
|
836
|
-
-a, --adapter <type> Adapter: basic, slack, telegram, discord, web
|
|
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)
|
|
837
880
|
--pm <manager> Package manager: npm, pnpm, yarn
|
|
838
881
|
-l, --language <lang> Language: typescript (default) or javascript
|
|
839
882
|
-y, --yes Skip prompts and accept defaults
|
|
883
|
+
--minimal Chat-only widget (ui-kit) \u2014 no platform modules
|
|
840
884
|
--skip-install Skip dependency installation
|
|
841
885
|
--no-git Skip git repository initialization
|
|
842
886
|
-h, --help Show this help
|
|
843
887
|
|
|
844
888
|
${pc.bold("Examples:")}
|
|
845
|
-
${pc.cyan("npx create-fluxy-chat my-
|
|
889
|
+
${pc.cyan("npx create-fluxy-chat my-chat --minimal")}
|
|
890
|
+
${pc.cyan("npx create-fluxy-chat my-hr-bot --template hr-feedback")}
|
|
891
|
+
${pc.cyan("npx create-fluxy-chat my-chat --template react")}
|
|
892
|
+
${pc.cyan("npx create-fluxy-chat my-bot --adapter basic")}
|
|
846
893
|
${pc.cyan("npx create-fluxy-chat my-bot --adapter slack")}
|
|
847
894
|
${pc.cyan("npx create-fluxy-chat my-bot --adapter telegram --pm pnpm")}
|
|
848
895
|
${pc.cyan("npx create-fluxy-chat my-bot -y --adapter discord")}
|
|
@@ -860,6 +907,7 @@ async function main() {
|
|
|
860
907
|
packageManager: args.pm,
|
|
861
908
|
language: args.language,
|
|
862
909
|
yes: args.yes,
|
|
910
|
+
minimal: args.minimal,
|
|
863
911
|
shouldInstall: args.skipInstall ? false : void 0,
|
|
864
912
|
shouldInitGit: args.noGit ? false : void 0
|
|
865
913
|
});
|
|
@@ -882,19 +930,45 @@ async function main() {
|
|
|
882
930
|
s.start("Creating project files");
|
|
883
931
|
try {
|
|
884
932
|
fs2.mkdirSync(projectDir, { recursive: true });
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
933
|
+
if (config.minimal) {
|
|
934
|
+
const templateRoot = path2.join(templatesDir(), "minimal");
|
|
935
|
+
copyDir(templateRoot, projectDir);
|
|
936
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
937
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
938
|
+
pkg.name = config.name;
|
|
939
|
+
writeJson(projectDir, "package.json", pkg);
|
|
940
|
+
s.stop("Minimal chat widget created.");
|
|
941
|
+
} else if (config.adapter === "react") {
|
|
942
|
+
const templateRoot = path2.join(templatesDir(), "react");
|
|
943
|
+
copyDir(templateRoot, projectDir);
|
|
944
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
945
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
946
|
+
pkg.name = config.name;
|
|
947
|
+
writeJson(projectDir, "package.json", pkg);
|
|
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.");
|
|
957
|
+
} else {
|
|
958
|
+
writeJson(projectDir, "package.json", generatePackageJson(config));
|
|
959
|
+
if (config.language === "typescript") {
|
|
960
|
+
writeJson(projectDir, "tsconfig.json", generateTsConfig());
|
|
961
|
+
}
|
|
962
|
+
writeFile(projectDir, "wrangler.toml", generateWranglerToml(config));
|
|
963
|
+
writeFile(projectDir, ".dev.vars", generateDevVars());
|
|
964
|
+
writeFile(projectDir, ".env.example", generateEnvExample(config));
|
|
965
|
+
writeFile(projectDir, ".gitignore", generateGitignore());
|
|
966
|
+
const ext = config.language === "typescript" ? "ts" : "js";
|
|
967
|
+
writeFile(projectDir, `src/index.${ext}`, generateWorkerIndex(config));
|
|
968
|
+
writeFile(projectDir, `src/bot.${ext}`, generateBotHandler(config));
|
|
969
|
+
writeFile(projectDir, "README.md", generateReadme(config));
|
|
970
|
+
s.stop("Project files created.");
|
|
888
971
|
}
|
|
889
|
-
writeFile(projectDir, "wrangler.toml", generateWranglerToml(config));
|
|
890
|
-
writeFile(projectDir, ".dev.vars", generateDevVars());
|
|
891
|
-
writeFile(projectDir, ".env.example", generateEnvExample(config));
|
|
892
|
-
writeFile(projectDir, ".gitignore", generateGitignore());
|
|
893
|
-
const ext = config.language === "typescript" ? "ts" : "js";
|
|
894
|
-
writeFile(projectDir, `src/index.${ext}`, generateWorkerIndex(config));
|
|
895
|
-
writeFile(projectDir, `src/bot.${ext}`, generateBotHandler(config));
|
|
896
|
-
writeFile(projectDir, "README.md", generateReadme(config));
|
|
897
|
-
s.stop("Project files created.");
|
|
898
972
|
} catch (error) {
|
|
899
973
|
s.stop("Failed to create project files.");
|
|
900
974
|
throw error;
|
|
@@ -928,7 +1002,12 @@ async function main() {
|
|
|
928
1002
|
}
|
|
929
1003
|
}
|
|
930
1004
|
note(
|
|
931
|
-
[
|
|
1005
|
+
config.adapter === "react" ? [
|
|
1006
|
+
`cd ${config.name}`,
|
|
1007
|
+
"cp .env.example .env",
|
|
1008
|
+
"# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLIC_ROOM_ID (guest) or MEMBER_JWT",
|
|
1009
|
+
`${config.packageManager === "npm" ? "npm run" : config.packageManager} dev`
|
|
1010
|
+
].join("\n") : [
|
|
932
1011
|
`cd ${config.name}`,
|
|
933
1012
|
"cp .env.example .dev.vars",
|
|
934
1013
|
`${config.packageManager === "npm" ? "npm run" : config.packageManager} dev`
|
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,14 +5,21 @@ Scaffold a new [FluxyChat](https://github.com/AlessandroFare/fluxychat) bot proj
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
# Minimal chat widget (recommended: 3 lines in App.tsx)
|
|
9
|
+
npx create-fluxy-chat my-chat --minimal
|
|
10
|
+
|
|
11
|
+
# React + useChat (guest room ~60s or member JWT)
|
|
12
|
+
npx create-fluxy-chat my-chat --template react
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
Interactive mode defaults to the **React chat app** template.
|
|
12
16
|
|
|
13
17
|
## Non-interactive usage
|
|
14
18
|
|
|
15
19
|
```bash
|
|
20
|
+
# React + Vite + useChat (recommended)
|
|
21
|
+
npx create-fluxy-chat my-chat --template react -y
|
|
22
|
+
|
|
16
23
|
# Create a Slack bot with pnpm
|
|
17
24
|
npx create-fluxy-chat my-bot --adapter slack --pm pnpm
|
|
18
25
|
|
|
@@ -30,10 +37,12 @@ npx create-fluxy-chat my-bot --adapter basic
|
|
|
30
37
|
|
|
31
38
|
| Flag | Short | Description |
|
|
32
39
|
| --- | --- | --- |
|
|
33
|
-
| `--adapter <type>` | `-a` | Adapter: `basic`, `slack`, `telegram`, `discord`, `web` |
|
|
40
|
+
| `--adapter <type>` | `-a` | Adapter: `react`, `basic`, `slack`, `telegram`, `discord`, `web` |
|
|
41
|
+
| `--template <type>` | `-t` | Alias for `--adapter (e.g. react)` |
|
|
34
42
|
| `--pm <manager>` | | Package manager: `npm`, `pnpm`, `yarn` |
|
|
35
43
|
| `--language <lang>` | `-l` | Language: `typescript` (default) or `javascript` |
|
|
36
44
|
| `--yes` | `-y` | Skip prompts and accept defaults |
|
|
45
|
+
| `--minimal` | | Chat-only widget (`@fluxy-chat/ui-kit`), no platform modules |
|
|
37
46
|
| `--skip-install` | | Skip dependency installation |
|
|
38
47
|
| `--no-git` | | Skip git repository initialization |
|
|
39
48
|
| `--help` | `-h` | Show help |
|
|
@@ -42,6 +51,7 @@ npx create-fluxy-chat my-bot --adapter basic
|
|
|
42
51
|
|
|
43
52
|
| Adapter | Description | Platform |
|
|
44
53
|
| --- | --- | --- |
|
|
54
|
+
| `react` | Vite + React chat UI with `useChat` | Browser / SPA |
|
|
45
55
|
| `basic` | Generic webhook bot | Cloudflare Workers |
|
|
46
56
|
| `slack` | Slack Events API bot | Slack |
|
|
47
57
|
| `telegram` | Telegram webhook bot | Telegram |
|
|
@@ -52,14 +62,14 @@ npx create-fluxy-chat my-bot --adapter basic
|
|
|
52
62
|
|
|
53
63
|
Each generated project includes:
|
|
54
64
|
|
|
55
|
-
- **`src/index.ts
|
|
56
|
-
- **`src/bot.ts
|
|
57
|
-
- **`fluxy.config.ts
|
|
58
|
-
- **`wrangler.toml
|
|
59
|
-
- **`.dev.vars
|
|
60
|
-
- **`.env.example
|
|
61
|
-
- **`tsconfig.json
|
|
62
|
-
- **`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
|
|
63
73
|
|
|
64
74
|
## Package manager detection
|
|
65
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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# FluxyChat minimal starter
|
|
2
|
+
|
|
3
|
+
Three-line chat widget with no platform modules in the generated project.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cp .env.example .env
|
|
7
|
+
pnpm dev
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Uses `@fluxy-chat/ui-kit` (`FluxyChatWidget`).
|
|
11
|
+
|
|
12
|
+
## Full platform
|
|
13
|
+
|
|
14
|
+
See [docs.fluxychat.com](https://docs.fluxychat.com/docs) for agents, stream, IoT, and self-host.
|
|
@@ -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</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,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fluxy-chat-minimal",
|
|
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/ui-kit": "^0.1.0",
|
|
13
|
+
"@fluxy-chat/ui": "^0.1.2",
|
|
14
|
+
"@fluxy-chat/react": "^0.1.1",
|
|
15
|
+
"@fluxy-chat/sdk": "^0.6.0",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "^19.0.0",
|
|
21
|
+
"@types/react-dom": "^19.0.0",
|
|
22
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
23
|
+
"typescript": "^5.6.0",
|
|
24
|
+
"vite": "^6.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FluxyChatWidget } from "@fluxy-chat/ui-kit";
|
|
2
|
+
|
|
3
|
+
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL;
|
|
4
|
+
const token = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT;
|
|
5
|
+
const roomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID || "general";
|
|
6
|
+
|
|
7
|
+
export function App() {
|
|
8
|
+
return (
|
|
9
|
+
<main style={{ maxWidth: 720, margin: "2rem auto", padding: "0 1rem" }}>
|
|
10
|
+
<h1 style={{ fontSize: "1.25rem", marginBottom: "1rem" }}>FluxyChat</h1>
|
|
11
|
+
<FluxyChatWidget
|
|
12
|
+
roomId={roomId}
|
|
13
|
+
workerUrl={workerUrl}
|
|
14
|
+
token={token}
|
|
15
|
+
theme="default"
|
|
16
|
+
height={520}
|
|
17
|
+
/>
|
|
18
|
+
<p style={{ marginTop: "1rem", fontSize: 13, color: "#71717a" }}>
|
|
19
|
+
Need stream, IoT, or agents? See{" "}
|
|
20
|
+
<a href="https://docs.fluxychat.com/docs">full platform docs</a>.
|
|
21
|
+
</p>
|
|
22
|
+
</main>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "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
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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 (~2 min via onboarding)
|
|
5
|
+
VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
|
|
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
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# FluxyChat React starter
|
|
2
|
+
|
|
3
|
+
Minimal Vite + React app with `useChat`. First message in about 60 seconds via the public guest room.
|
|
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)
|
|
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
|
+
```
|
|
24
|
+
|
|
25
|
+
## Get credentials
|
|
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 |
|
|
38
|
+
|
|
39
|
+
## Next steps
|
|
40
|
+
|
|
41
|
+
- Add `@fluxy-chat/ui` themes (`default`, `dark`, `minimal`, `brand`)
|
|
42
|
+
- Use `useInbox` for unified feed
|
|
43
|
+
- See [chat-only quickstart](https://docs.fluxychat.com/docs/getting-started/chat-only-quickstart)
|
|
@@ -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</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-chat-app",
|
|
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.1",
|
|
13
|
+
"@fluxy-chat/sdk": "^0.6.0",
|
|
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,188 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { FluxyChatClient } from "@fluxy-chat/sdk";
|
|
3
|
+
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
|
|
4
|
+
|
|
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
|
+
}
|
|
80
|
+
|
|
81
|
+
function ChatPanel({ roomId }: { roomId: string }) {
|
|
82
|
+
const { messages, sendMessage, connectionState } = useChat({
|
|
83
|
+
roomId,
|
|
84
|
+
markReadLatest: true,
|
|
85
|
+
});
|
|
86
|
+
const [draft, setDraft] = useState("");
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<section className="chat-panel">
|
|
90
|
+
<header className="chat-header">
|
|
91
|
+
<strong>{roomId}</strong>
|
|
92
|
+
<span className="status">{connectionState.status}</span>
|
|
93
|
+
</header>
|
|
94
|
+
<ul className="messages">
|
|
95
|
+
{messages.map((m) => (
|
|
96
|
+
<li key={m.id} className="message">
|
|
97
|
+
<span className="author">{m.userId}</span>
|
|
98
|
+
<span>{m.content}</span>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
<form
|
|
103
|
+
className="composer"
|
|
104
|
+
onSubmit={(e) => {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const text = draft.trim();
|
|
107
|
+
if (!text) return;
|
|
108
|
+
void sendMessage(text);
|
|
109
|
+
setDraft("");
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<input
|
|
113
|
+
value={draft}
|
|
114
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
115
|
+
placeholder="Type a message…"
|
|
116
|
+
aria-label="Message"
|
|
117
|
+
/>
|
|
118
|
+
<button type="submit">Send</button>
|
|
119
|
+
</form>
|
|
120
|
+
</section>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function App() {
|
|
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]);
|
|
132
|
+
|
|
133
|
+
if (!workerUrl) {
|
|
134
|
+
return (
|
|
135
|
+
<main className="shell">
|
|
136
|
+
<h1>FluxyChat — chat-only starter</h1>
|
|
137
|
+
<p>
|
|
138
|
+
Copy <code>.env.example</code> to <code>.env</code> and set{" "}
|
|
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).
|
|
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>
|
|
162
|
+
<p>
|
|
163
|
+
Get credentials from{" "}
|
|
164
|
+
<a href="https://fluxychat.com/onboarding" target="_blank" rel="noreferrer">
|
|
165
|
+
onboarding
|
|
166
|
+
</a>{" "}
|
|
167
|
+
or use a public room ID for guest mode.
|
|
168
|
+
</p>
|
|
169
|
+
</main>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<main className="shell">
|
|
175
|
+
<h1>FluxyChat</h1>
|
|
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} />
|
|
185
|
+
</FluxyRealtimeProvider>
|
|
186
|
+
</main>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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: 720px;
|
|
18
|
+
margin: 0 auto;
|
|
19
|
+
padding: 1.5rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.chat-panel {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
height: min(70vh, 560px);
|
|
26
|
+
border: 1px solid #e2e8f0;
|
|
27
|
+
border-radius: 12px;
|
|
28
|
+
background: #fff;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.chat-header {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
padding: 0.75rem 1rem;
|
|
36
|
+
border-bottom: 1px solid #e2e8f0;
|
|
37
|
+
background: #2563eb;
|
|
38
|
+
color: #fff;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.status {
|
|
42
|
+
font-size: 0.75rem;
|
|
43
|
+
opacity: 0.9;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.messages {
|
|
47
|
+
flex: 1;
|
|
48
|
+
overflow-y: auto;
|
|
49
|
+
list-style: none;
|
|
50
|
+
margin: 0;
|
|
51
|
+
padding: 1rem;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: 0.5rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.message {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 0.15rem;
|
|
61
|
+
padding: 0.5rem 0.75rem;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
background: #f1f5f9;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.author {
|
|
67
|
+
font-size: 0.7rem;
|
|
68
|
+
color: #64748b;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.composer {
|
|
72
|
+
display: flex;
|
|
73
|
+
gap: 0.5rem;
|
|
74
|
+
padding: 0.75rem;
|
|
75
|
+
border-top: 1px solid #e2e8f0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.composer input {
|
|
79
|
+
flex: 1;
|
|
80
|
+
border: 1px solid #cbd5e1;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
padding: 0.5rem 0.75rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.composer button {
|
|
86
|
+
border: none;
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
background: #2563eb;
|
|
89
|
+
color: #fff;
|
|
90
|
+
padding: 0.5rem 1rem;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.mode-badge {
|
|
95
|
+
font-size: 0.75rem;
|
|
96
|
+
color: #64748b;
|
|
97
|
+
margin: 0 0 0.75rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.error {
|
|
101
|
+
color: #b91c1c;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.room-picker {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 0.25rem;
|
|
108
|
+
margin-bottom: 1rem;
|
|
109
|
+
font-size: 0.875rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.room-picker input {
|
|
113
|
+
padding: 0.5rem 0.75rem;
|
|
114
|
+
border: 1px solid #cbd5e1;
|
|
115
|
+
border-radius: 8px;
|
|
116
|
+
}
|
|
@@ -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
|
+
}
|