@fluxy-chat/create-fluxy-chat 0.5.3 → 0.5.5
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 +16 -0
- package/package.json +1 -1
- package/templates/full/README.md +14 -33
- package/templates/full/index.html +3 -0
- package/templates/full/package.json +11 -1
- package/templates/full/postcss.config.js +6 -0
- package/templates/full/scripts/fluxy-doctor.mjs +5 -2
- package/templates/full/scripts/fluxy-setup.mjs +14 -31
- package/templates/full/src/App.tsx +153 -174
- package/templates/full/src/index.css +201 -165
- package/templates/full/src/session.ts +54 -0
- package/templates/full/tailwind.config.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.4] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Add `zustand` to `full` template dependencies so `pnpm install` gets everything without manual intervention.
|
|
8
|
+
- Fix `FluxyRealtimeProvider` usage: pass `workerUrl` + `authTokenProvider` instead of non-existent `client` prop (fixes "disconnected" state and message sending).
|
|
9
|
+
- Restyle full template UI to match FluxyChat dark theme (indigo/emerald palette, proper message bubbles, live/disconnected badge).
|
|
10
|
+
|
|
11
|
+
## [0.5.5] - 2026-08-19
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Hosted `full` template now provisions a user project + assistant room via Clerk (no more public playground session).
|
|
16
|
+
- Hosted `full` template uses the same chat UI primitives as FluxyChat (message bubbles/composer/status) to match dashboard/rooms/onboarding styling.
|
|
17
|
+
- Remove fuorvianti "dash" UI artifacts and wire message sending through the same chat path as the dashboard.
|
|
18
|
+
|
|
3
19
|
## [0.5.3] - 2026-08-19
|
|
4
20
|
|
|
5
21
|
### Fixed
|
package/package.json
CHANGED
package/templates/full/README.md
CHANGED
|
@@ -1,66 +1,47 @@
|
|
|
1
|
-
# FluxyChat
|
|
1
|
+
# FluxyChat full stack starter
|
|
2
2
|
|
|
3
|
-
Chat + AI agent
|
|
3
|
+
Chat + AI agent in a Vite app. Hosted mode signs you in with Clerk (same as the console), then creates your project and assistant room.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
|
-
**Hosted (no wrangler):**
|
|
7
|
+
**Hosted (Clerk, no wrangler):**
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx @fluxy-chat/create-fluxy-chat my-app --mode hosted -y
|
|
10
|
+
npx @fluxy-chat/create-fluxy-chat@latest my-app --mode hosted -y
|
|
11
11
|
cd my-app
|
|
12
12
|
pnpm install
|
|
13
13
|
pnpm setup:hosted
|
|
14
14
|
pnpm dev
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The app opens on localhost. Click Continue with FluxyChat, sign in, then you land in your own room (not the public playground).
|
|
18
|
+
|
|
19
|
+
**Local worker:**
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
# Terminal 1 — monorepo worker
|
|
21
22
|
pnpm --filter @fluxy-chat/worker dev
|
|
22
|
-
|
|
23
|
-
# Terminal 2 — new project
|
|
24
|
-
npx create-fluxy-chat my-app --full -y
|
|
23
|
+
npx @fluxy-chat/create-fluxy-chat@latest my-app --full -y
|
|
25
24
|
cd my-app
|
|
26
25
|
pnpm install
|
|
27
26
|
pnpm setup
|
|
28
27
|
pnpm dev
|
|
29
28
|
```
|
|
30
29
|
|
|
31
|
-
Optional: `pnpm doctor` checks `.env`, worker health, room access.
|
|
32
|
-
|
|
33
|
-
## What you get
|
|
34
|
-
|
|
35
|
-
- Realtime chat (`useChat` + WebSocket)
|
|
36
|
-
- `@assistant` invoke with tool call thread in the UI
|
|
37
|
-
- Sidebar with project/room/agent metadata
|
|
38
|
-
- Link to operator console
|
|
39
|
-
|
|
40
30
|
## Scripts
|
|
41
31
|
|
|
42
32
|
| Command | Description |
|
|
43
33
|
|---------|-------------|
|
|
44
|
-
| `pnpm setup:hosted` |
|
|
45
|
-
| `pnpm setup:local` | Local `/dev/provision`
|
|
46
|
-
| `pnpm doctor` | Health check
|
|
47
|
-
| `pnpm dev` | Start Vite
|
|
48
|
-
| `pnpm dev:app` | Vite only |
|
|
34
|
+
| `pnpm setup:hosted` | Writes worker + console URLs. Auth happens in the browser via Clerk. |
|
|
35
|
+
| `pnpm setup:local` | Local `/dev/provision` |
|
|
36
|
+
| `pnpm doctor` | Health check |
|
|
37
|
+
| `pnpm dev` | Start Vite |
|
|
49
38
|
|
|
50
39
|
## Environment
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- `VITE_FLUXYCHAT_WORKER_URL` — default `http://127.0.0.1:8787`
|
|
55
|
-
- `VITE_FLUXYCHAT_MEMBER_JWT` — member token with admin roles
|
|
56
|
-
- `VITE_FLUXYCHAT_ROOM_ID` — `{projectId}-general`
|
|
57
|
-
- `VITE_FLUXYCHAT_AGENT_ID` — built-in `@assistant`
|
|
58
|
-
|
|
59
|
-
## Hosted credentials
|
|
41
|
+
`.env` from `pnpm setup:hosted` only needs worker and console URLs. Member JWT, room, and agent come from Clerk after sign in.
|
|
60
42
|
|
|
61
|
-
|
|
43
|
+
Open console: [fluxychat.com/onboarding](https://fluxychat.com/onboarding)
|
|
62
44
|
|
|
63
45
|
## Learn more
|
|
64
46
|
|
|
65
|
-
- [One-click product roadmap](https://github.com/AlessandroFare/fluxychat/blob/main/docs/ONE-CLICK-PRODUCT-ROADMAP.md)
|
|
66
47
|
- [Docs](https://docs.fluxychat.com)
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>FluxyChat</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;650&display=swap" rel="stylesheet" />
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<div id="root"></div>
|
|
@@ -16,13 +16,23 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@fluxy-chat/react": "^0.1.1",
|
|
18
18
|
"@fluxy-chat/sdk": "^0.6.2",
|
|
19
|
+
"@fluxy-chat/ui": "^0.1.2",
|
|
20
|
+
"class-variance-authority": "^0.7.1",
|
|
21
|
+
"clsx": "^2.1.1",
|
|
22
|
+
"lucide-react": "^0.511.0",
|
|
23
|
+
"radix-ui": "^1.4.3",
|
|
19
24
|
"react": "^19.0.0",
|
|
20
|
-
"react-dom": "^19.0.0"
|
|
25
|
+
"react-dom": "^19.0.0",
|
|
26
|
+
"tailwind-merge": "^3.0.0",
|
|
27
|
+
"zustand": "^5.0.14"
|
|
21
28
|
},
|
|
22
29
|
"devDependencies": {
|
|
23
30
|
"@types/react": "^19.0.0",
|
|
24
31
|
"@types/react-dom": "^19.0.0",
|
|
25
32
|
"@vitejs/plugin-react": "^4.3.0",
|
|
33
|
+
"autoprefixer": "^10.4.20",
|
|
34
|
+
"postcss": "^8.5.0",
|
|
35
|
+
"tailwindcss": "^3.4.17",
|
|
26
36
|
"typescript": "^5.6.0",
|
|
27
37
|
"vite": "^6.0.0"
|
|
28
38
|
}
|
|
@@ -71,8 +71,11 @@ async function main() {
|
|
|
71
71
|
if (!workerUrl) fail("VITE_FLUXYCHAT_WORKER_URL unset");
|
|
72
72
|
else pass(`worker URL: ${workerUrl}`);
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const hosted = (env.VITE_FLUXYCHAT_CONSOLE_URL || "").includes("fluxychat.com");
|
|
75
|
+
if (!jwt) {
|
|
76
|
+
if (hosted) warn("VITE_FLUXYCHAT_MEMBER_JWT unset — sign in from the app (Clerk)");
|
|
77
|
+
else fail("VITE_FLUXYCHAT_MEMBER_JWT unset");
|
|
78
|
+
} else pass(`JWT present (${jwt.length} chars)`);
|
|
76
79
|
|
|
77
80
|
if (!roomId) warn("VITE_FLUXYCHAT_ROOM_ID unset");
|
|
78
81
|
else pass(`room: ${roomId}`);
|
|
@@ -189,49 +189,32 @@ async function setupHosted() {
|
|
|
189
189
|
|
|
190
190
|
console.log(dim(` mode: hosted · worker: ${workerUrl}`));
|
|
191
191
|
|
|
192
|
-
const cliHeaders = { Origin: consoleUrl };
|
|
193
|
-
|
|
194
|
-
const status = await getJson(workerUrl, "/demo/status", cliHeaders).catch(() => null);
|
|
195
|
-
if (!status?.ready) {
|
|
196
|
-
fail(
|
|
197
|
-
`Public demo not available at ${workerUrl}\n` +
|
|
198
|
-
" Try: pnpm setup -- --mode local (with local worker)\n" +
|
|
199
|
-
" Or: https://fluxychat.com/demo",
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
ok("public demo ready");
|
|
203
|
-
|
|
204
|
-
const session = await getJson(workerUrl, "/demo/session", cliHeaders);
|
|
205
|
-
if (!session?.token || !session?.roomId) {
|
|
206
|
-
fail("/demo/session did not return token + roomId");
|
|
207
|
-
}
|
|
208
|
-
ok(`guest session · room ${session.roomId}`);
|
|
209
|
-
|
|
210
192
|
writeEnv({
|
|
211
193
|
workerUrl,
|
|
212
|
-
token:
|
|
213
|
-
roomId:
|
|
214
|
-
agentId:
|
|
215
|
-
agentHandle:
|
|
216
|
-
projectId: "
|
|
217
|
-
userId:
|
|
194
|
+
token: "",
|
|
195
|
+
roomId: "",
|
|
196
|
+
agentId: "",
|
|
197
|
+
agentHandle: "@assistant",
|
|
198
|
+
projectId: "",
|
|
199
|
+
userId: "",
|
|
218
200
|
consoleUrl,
|
|
219
201
|
});
|
|
220
202
|
|
|
221
203
|
writeMeta({
|
|
222
204
|
mode: "hosted",
|
|
223
205
|
workerUrl,
|
|
224
|
-
projectId:
|
|
225
|
-
roomId:
|
|
226
|
-
agentId:
|
|
227
|
-
agentHandle:
|
|
228
|
-
expiresIn: session.expiresIn ?? null,
|
|
206
|
+
projectId: null,
|
|
207
|
+
roomId: null,
|
|
208
|
+
agentId: null,
|
|
209
|
+
agentHandle: "@assistant",
|
|
229
210
|
setupAt: new Date().toISOString(),
|
|
211
|
+
auth: "clerk",
|
|
230
212
|
});
|
|
231
213
|
|
|
214
|
+
ok("wrote hosted endpoints (sign in with Clerk on pnpm dev)");
|
|
232
215
|
console.log(dim(" Run: pnpm dev"));
|
|
233
|
-
console.log(dim(
|
|
234
|
-
console.log(dim(
|
|
216
|
+
console.log(dim(" Sign in with Clerk. We create your project and assistant room."));
|
|
217
|
+
console.log(dim(` Console: ${consoleUrl}/onboarding`));
|
|
235
218
|
}
|
|
236
219
|
|
|
237
220
|
async function setupLocal() {
|
|
@@ -1,221 +1,200 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
|
-
import { FluxyChatClient } from "@fluxy-chat/sdk";
|
|
3
2
|
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
|
|
3
|
+
import { ChatWindow } from "@fluxy-chat/ui";
|
|
4
|
+
import { loadCliSession, type CliSession } from "./session";
|
|
5
|
+
|
|
6
|
+
const envWorkerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim() ?? "";
|
|
7
|
+
const envJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim() ?? "";
|
|
8
|
+
const envRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() ?? "";
|
|
9
|
+
const envAgentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() ?? "";
|
|
10
|
+
const envAgentHandle = import.meta.env.VITE_FLUXYCHAT_AGENT_HANDLE?.trim() || "@assistant";
|
|
11
|
+
const envProjectId = import.meta.env.VITE_FLUXYCHAT_PROJECT_ID?.trim() ?? "";
|
|
12
|
+
const envUserId = import.meta.env.VITE_FLUXYCHAT_USER_ID?.trim() ?? "";
|
|
13
|
+
const consoleUrl = import.meta.env.VITE_FLUXYCHAT_CONSOLE_URL?.trim() || "https://fluxychat.com";
|
|
14
|
+
|
|
15
|
+
const SUGGESTED_PROMPTS = [
|
|
16
|
+
"Tell me about FluxyChat features",
|
|
17
|
+
"What can this assistant do?",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function sessionFromEnv(): CliSession | null {
|
|
21
|
+
if (!envWorkerUrl || !envJwt || !envRoomId) return null;
|
|
22
|
+
return {
|
|
23
|
+
workerUrl: envWorkerUrl,
|
|
24
|
+
memberJwt: envJwt,
|
|
25
|
+
roomId: envRoomId,
|
|
26
|
+
agentId: envAgentId,
|
|
27
|
+
agentHandle: envAgentHandle,
|
|
28
|
+
projectId: envProjectId,
|
|
29
|
+
userId: envUserId || "demo-user",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
4
32
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const agentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() || "";
|
|
9
|
-
const agentHandle = import.meta.env.VITE_FLUXYCHAT_AGENT_HANDLE?.trim() || "@assistant";
|
|
10
|
-
const projectId = import.meta.env.VITE_FLUXYCHAT_PROJECT_ID?.trim() || "";
|
|
11
|
-
const consoleUrl = import.meta.env.VITE_FLUXYCHAT_CONSOLE_URL?.trim() || "http://localhost:3000";
|
|
12
|
-
const memberUserId =
|
|
13
|
-
import.meta.env.VITE_FLUXYCHAT_USER_ID?.trim() || "demo-user";
|
|
33
|
+
function SignInGate() {
|
|
34
|
+
const returnTo = typeof window !== "undefined" ? window.location.origin : "http://localhost:5173";
|
|
35
|
+
const href = `${consoleUrl.replace(/\/$/, "")}/cli-auth?redirect_uri=${encodeURIComponent(returnTo)}`;
|
|
14
36
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
return (
|
|
38
|
+
<main className="app-shell">
|
|
39
|
+
<header className="topbar">
|
|
40
|
+
<div className="brand">
|
|
41
|
+
<img src={`${consoleUrl.replace(/\/$/, "")}/fluxychat-icon.svg`} alt="" width={28} height={28} />
|
|
42
|
+
<span>FluxyChat</span>
|
|
43
|
+
</div>
|
|
44
|
+
</header>
|
|
45
|
+
<div className="signin-card">
|
|
46
|
+
<p className="eyebrow">Local starter</p>
|
|
47
|
+
<h1>Sign in to open your room</h1>
|
|
48
|
+
<p>
|
|
49
|
+
Use the same Clerk account as the console. We create your project and
|
|
50
|
+
assistant room, then you chat here.
|
|
51
|
+
</p>
|
|
52
|
+
<a className="btn-primary" href={href}>
|
|
53
|
+
Continue with FluxyChat
|
|
54
|
+
</a>
|
|
55
|
+
<p className="fine-print">
|
|
56
|
+
Opens {consoleUrl.replace(/^https?:\/\//, "")} for sign in, then returns to this app.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
const
|
|
63
|
+
function ChatRoom({ session }: { session: CliSession }) {
|
|
64
|
+
const { messages, sendMessage, invokeAgent, connectionState, agentTyping, typingUsers, online } =
|
|
65
|
+
useChat({
|
|
66
|
+
roomId: session.roomId,
|
|
67
|
+
agentId: session.agentId || undefined,
|
|
68
|
+
markReadLatest: true,
|
|
69
|
+
});
|
|
32
70
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await sendMessage(text);
|
|
36
|
-
}
|
|
71
|
+
const [error, setError] = useState<string | null>(null);
|
|
72
|
+
const connected = connectionState.status === "connected";
|
|
37
73
|
|
|
38
|
-
async function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
setInvokeError(null);
|
|
74
|
+
async function onSend(content: string) {
|
|
75
|
+
setError(null);
|
|
76
|
+
const mentionsAgent = content.toLowerCase().includes(
|
|
77
|
+
(session.agentHandle || "@assistant").replace(/^@/, "").toLowerCase(),
|
|
78
|
+
);
|
|
44
79
|
try {
|
|
45
|
-
|
|
80
|
+
if (mentionsAgent && session.agentId) {
|
|
81
|
+
await invokeAgent(content, { agentId: session.agentId });
|
|
82
|
+
} else {
|
|
83
|
+
await sendMessage(content);
|
|
84
|
+
}
|
|
46
85
|
} catch (err) {
|
|
47
|
-
|
|
86
|
+
setError(err instanceof Error ? err.message : "Failed to send");
|
|
48
87
|
}
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
return (
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<li
|
|
64
|
-
key={m.id ?? `${m.createdAt}-${m.userId}`}
|
|
65
|
-
className={`message${isSelf ? " self" : ""}${isAgent ? " agent" : ""}`}
|
|
66
|
-
>
|
|
67
|
-
<span className="author">{m.userId}</span>
|
|
68
|
-
<span>{m.content}</span>
|
|
69
|
-
</li>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</ul>
|
|
73
|
-
|
|
74
|
-
{agentTyping ? <p className="typing">{agentHandle} is thinking…</p> : null}
|
|
75
|
-
|
|
76
|
-
{(toolThreadEvents.length > 0 || lastAgentRun?.toolCalls?.length) ? (
|
|
77
|
-
<div className="tools">
|
|
78
|
-
<h3>Agent tools</h3>
|
|
79
|
-
<ul>
|
|
80
|
-
{toolThreadEvents.map((ev) => (
|
|
81
|
-
<li key={ev.key}>
|
|
82
|
-
{String(ev.kind ?? "tool")}: {String(ev.title ?? ev.toolName ?? ev.key)}
|
|
83
|
-
</li>
|
|
84
|
-
))}
|
|
85
|
-
{(lastAgentRun?.toolCalls ?? []).map((tc) => (
|
|
86
|
-
<li key={tc.id}>
|
|
87
|
-
{tc.name}: {tc.status ?? "done"}
|
|
88
|
-
</li>
|
|
89
|
-
))}
|
|
90
|
-
</ul>
|
|
91
|
-
</div>
|
|
92
|
-
) : null}
|
|
91
|
+
<div className="chat-column">
|
|
92
|
+
<div className="status-row">
|
|
93
|
+
<span className="inline-flex items-center gap-2">
|
|
94
|
+
<span className={`status-dot ${connected ? "on" : "off"}`} />
|
|
95
|
+
<span className="font-medium text-foreground">
|
|
96
|
+
{connected ? "Connected" : connectionState.status}
|
|
97
|
+
</span>
|
|
98
|
+
<span className="text-muted-foreground">· {session.roomId}</span>
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
93
101
|
|
|
94
|
-
|
|
102
|
+
<div className="chat-frame">
|
|
103
|
+
<ChatWindow
|
|
104
|
+
messages={messages}
|
|
105
|
+
online={online ?? 0}
|
|
106
|
+
typingUsers={typingUsers ?? {}}
|
|
107
|
+
onSend={(content) => {
|
|
108
|
+
void onSend(content);
|
|
109
|
+
}}
|
|
110
|
+
agentTyping={Boolean(agentTyping)}
|
|
111
|
+
agentTypingLabel={session.agentHandle || "@assistant"}
|
|
112
|
+
mentionSuggestions={[
|
|
113
|
+
{
|
|
114
|
+
handle: (session.agentHandle || "@assistant").replace(/^@/, ""),
|
|
115
|
+
label: session.agentHandle || "@assistant",
|
|
116
|
+
agentId: session.agentId,
|
|
117
|
+
},
|
|
118
|
+
]}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
95
121
|
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
onSubmit={(e) => {
|
|
99
|
-
e.preventDefault();
|
|
100
|
-
const text = draft.trim();
|
|
101
|
-
if (!text) return;
|
|
102
|
-
void handleSendMessage(text);
|
|
103
|
-
setDraft("");
|
|
104
|
-
}}
|
|
105
|
-
>
|
|
106
|
-
<div className="composer-row">
|
|
107
|
-
<input
|
|
108
|
-
value={draft}
|
|
109
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
110
|
-
placeholder="Message or ask @assistant…"
|
|
111
|
-
aria-label="Message"
|
|
112
|
-
/>
|
|
113
|
-
<button type="submit">Send</button>
|
|
122
|
+
<div className="prompts">
|
|
123
|
+
{SUGGESTED_PROMPTS.map((prompt) => (
|
|
114
124
|
<button
|
|
125
|
+
key={prompt}
|
|
115
126
|
type="button"
|
|
116
|
-
className="secondary"
|
|
117
|
-
disabled={!draft.trim()}
|
|
118
127
|
onClick={() => {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const payload = text.startsWith("@") ? text : `${agentHandle} ${text}`;
|
|
122
|
-
void handleInvokeAgent(payload);
|
|
123
|
-
setDraft("");
|
|
128
|
+
const handle = session.agentHandle || "@assistant";
|
|
129
|
+
void onSend(`${handle} ${prompt}`);
|
|
124
130
|
}}
|
|
125
131
|
>
|
|
126
|
-
|
|
132
|
+
{prompt}
|
|
127
133
|
</button>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
</form>
|
|
133
|
-
</section>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
{error ? <p className="chat-error">{error}</p> : null}
|
|
137
|
+
</div>
|
|
134
138
|
);
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
export function App() {
|
|
138
|
-
const
|
|
139
|
-
if (!workerUrl || !memberJwt) return null;
|
|
140
|
-
return new FluxyChatClient({
|
|
141
|
-
baseUrl: workerUrl,
|
|
142
|
-
userId: memberUserId,
|
|
143
|
-
token: memberJwt,
|
|
144
|
-
});
|
|
145
|
-
}, []);
|
|
146
|
-
|
|
147
|
-
if (!workerUrl || !memberJwt) {
|
|
148
|
-
return (
|
|
149
|
-
<main className="shell">
|
|
150
|
-
<div className="error-box">
|
|
151
|
-
<p>
|
|
152
|
-
<strong>Setup required.</strong> Run provisioning against a local FluxyChat worker:
|
|
153
|
-
</p>
|
|
154
|
-
<pre>
|
|
155
|
-
<code>{`# Terminal 1 — from FluxyChat monorepo
|
|
156
|
-
pnpm --filter @fluxy-chat/worker dev
|
|
157
|
-
|
|
158
|
-
# Terminal 2 — in this project
|
|
159
|
-
pnpm setup
|
|
160
|
-
pnpm dev`}</code>
|
|
161
|
-
</pre>
|
|
162
|
-
<p>
|
|
163
|
-
Or copy <code>.env.example</code> → <code>.env</code> with credentials from{" "}
|
|
164
|
-
<a href="https://fluxychat.com/onboarding" target="_blank" rel="noreferrer">
|
|
165
|
-
fluxychat.com/onboarding
|
|
166
|
-
</a>
|
|
167
|
-
.
|
|
168
|
-
</p>
|
|
169
|
-
</div>
|
|
170
|
-
</main>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
142
|
+
const session = useMemo(() => loadCliSession() ?? sessionFromEnv(), []);
|
|
173
143
|
|
|
174
|
-
if (!
|
|
144
|
+
if (!session) return <SignInGate />;
|
|
175
145
|
|
|
176
146
|
return (
|
|
177
|
-
<main className="shell">
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
147
|
+
<main className="app-shell">
|
|
148
|
+
<header className="topbar">
|
|
149
|
+
<div className="brand">
|
|
150
|
+
<img src={`${consoleUrl.replace(/\/$/, "")}/fluxychat-icon.svg`} alt="" width={28} height={28} />
|
|
151
|
+
<span>FluxyChat</span>
|
|
152
|
+
</div>
|
|
153
|
+
<nav className="top-links">
|
|
154
|
+
<span className="meta">{session.projectName || session.projectId || "project"}</span>
|
|
182
155
|
<a href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`} target="_blank" rel="noreferrer">
|
|
183
|
-
|
|
156
|
+
Open console
|
|
184
157
|
</a>
|
|
185
|
-
</
|
|
186
|
-
</
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
158
|
+
</nav>
|
|
159
|
+
</header>
|
|
160
|
+
<div className="page">
|
|
161
|
+
<FluxyRealtimeProvider
|
|
162
|
+
workerUrl={session.workerUrl}
|
|
163
|
+
authTokenProvider={session.memberJwt}
|
|
164
|
+
userId={session.userId}
|
|
165
|
+
>
|
|
166
|
+
<ChatRoom session={session} />
|
|
191
167
|
</FluxyRealtimeProvider>
|
|
192
|
-
|
|
193
168
|
<aside className="side">
|
|
194
|
-
<
|
|
169
|
+
<section>
|
|
195
170
|
<h2>Project</h2>
|
|
196
171
|
<dl>
|
|
197
172
|
<div>
|
|
198
|
-
<dt>
|
|
199
|
-
<dd>{projectId || "—"}</dd>
|
|
173
|
+
<dt>ID</dt>
|
|
174
|
+
<dd>{session.projectId || "—"}</dd>
|
|
200
175
|
</div>
|
|
201
176
|
<div>
|
|
202
177
|
<dt>Room</dt>
|
|
203
|
-
<dd>{roomId}</dd>
|
|
178
|
+
<dd>{session.roomId}</dd>
|
|
204
179
|
</div>
|
|
205
180
|
<div>
|
|
206
181
|
<dt>Agent</dt>
|
|
207
|
-
<dd>{agentHandle}
|
|
182
|
+
<dd>{session.agentHandle}</dd>
|
|
208
183
|
</div>
|
|
209
184
|
</dl>
|
|
210
|
-
</
|
|
211
|
-
<
|
|
212
|
-
<h2>
|
|
213
|
-
<ul
|
|
214
|
-
<li>
|
|
215
|
-
<li>
|
|
216
|
-
<li>
|
|
185
|
+
</section>
|
|
186
|
+
<section>
|
|
187
|
+
<h2>Next</h2>
|
|
188
|
+
<ul>
|
|
189
|
+
<li>Send a message, or mention the agent to invoke it</li>
|
|
190
|
+
<li>Open a second tab on this URL for realtime</li>
|
|
191
|
+
<li>
|
|
192
|
+
<a href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`} target="_blank" rel="noreferrer">
|
|
193
|
+
Continue onboarding in the console
|
|
194
|
+
</a>
|
|
195
|
+
</li>
|
|
217
196
|
</ul>
|
|
218
|
-
</
|
|
197
|
+
</section>
|
|
219
198
|
</aside>
|
|
220
199
|
</div>
|
|
221
200
|
</main>
|
|
@@ -1,242 +1,278 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
color: #0f172a;
|
|
5
|
-
background: #fdfbf9;
|
|
6
|
-
}
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
:root {
|
|
6
|
+
--background: #fdfbf9;
|
|
7
|
+
--foreground: #0e1316;
|
|
8
|
+
--surface: #fdfbf9;
|
|
9
|
+
--surface-muted: #f3f0ec;
|
|
10
|
+
--surface-card: #ffffff;
|
|
11
|
+
--text-primary: #1a1a1a;
|
|
12
|
+
--text-muted: #6b7280;
|
|
13
|
+
--border: #e5e0d8;
|
|
14
|
+
--brand: #ff725e;
|
|
15
|
+
--destructive: #dc2626;
|
|
16
|
+
--ring: #ff725e;
|
|
17
|
+
--radius: 0.5rem;
|
|
18
|
+
|
|
19
|
+
--fluxy-logo-color: #ff6a1a;
|
|
20
|
+
--fluxy-cta-color: #c2410c;
|
|
21
|
+
--fluxy-bubble-sent-bg: #ff6a1a;
|
|
22
|
+
--fluxy-bubble-sent-text: #ffffff;
|
|
23
|
+
--fluxy-bubble-received-bg: #f4f4f5;
|
|
24
|
+
--fluxy-bubble-received-border: #e4e4e7;
|
|
25
|
+
--fluxy-bubble-received-text: #1a1a1a;
|
|
26
|
+
--primary: var(--fluxy-cta-color);
|
|
27
|
+
--primary-foreground: #ffffff;
|
|
28
|
+
--muted: var(--surface-muted);
|
|
29
|
+
--muted-foreground: var(--text-muted);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
html,
|
|
33
|
+
body,
|
|
34
|
+
#root {
|
|
35
|
+
height: 100%;
|
|
10
36
|
}
|
|
11
37
|
|
|
12
38
|
body {
|
|
13
39
|
margin: 0;
|
|
40
|
+
background: var(--background);
|
|
41
|
+
color: var(--foreground);
|
|
42
|
+
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
43
|
+
-webkit-font-smoothing: antialiased;
|
|
14
44
|
}
|
|
15
45
|
|
|
16
|
-
.shell {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.hero {
|
|
23
|
-
margin-bottom: 1rem;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.hero h1 {
|
|
27
|
-
font-size: 1.35rem;
|
|
28
|
-
margin: 0 0 0.35rem;
|
|
46
|
+
.app-shell {
|
|
47
|
+
min-height: 100%;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
background: var(--background);
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
.topbar {
|
|
54
|
+
height: 56px;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
padding: 0 20px;
|
|
59
|
+
border-bottom: 1px solid var(--border);
|
|
60
|
+
background: var(--surface-card);
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
.
|
|
38
|
-
|
|
63
|
+
.brand {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
font-weight: 650;
|
|
68
|
+
font-size: 15px;
|
|
39
69
|
}
|
|
40
70
|
|
|
41
|
-
.
|
|
42
|
-
display:
|
|
43
|
-
|
|
71
|
+
.top-links {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 14px;
|
|
75
|
+
font-size: 13px;
|
|
44
76
|
}
|
|
45
77
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
.top-links a,
|
|
79
|
+
.side a {
|
|
80
|
+
color: var(--fluxy-cta-color);
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
text-decoration: none;
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
min-height: 520px;
|
|
57
|
-
border: 1px solid #e7e5e4;
|
|
58
|
-
border-radius: 16px;
|
|
59
|
-
background: #fff;
|
|
60
|
-
overflow: hidden;
|
|
61
|
-
box-shadow: 0 8px 30px rgb(15 23 42 / 6%);
|
|
85
|
+
.top-links a:hover,
|
|
86
|
+
.side a:hover {
|
|
87
|
+
text-decoration: underline;
|
|
62
88
|
}
|
|
63
89
|
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
justify-content: space-between;
|
|
67
|
-
align-items: center;
|
|
68
|
-
padding: 0.75rem 1rem;
|
|
69
|
-
border-bottom: 1px solid #e7e5e4;
|
|
70
|
-
background: #0f172a;
|
|
71
|
-
color: #fff;
|
|
90
|
+
.meta {
|
|
91
|
+
color: var(--text-muted);
|
|
72
92
|
}
|
|
73
93
|
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
.page {
|
|
95
|
+
flex: 1;
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
98
|
+
min-height: 0;
|
|
77
99
|
}
|
|
78
100
|
|
|
79
|
-
.
|
|
80
|
-
flex: 1;
|
|
81
|
-
overflow-y: auto;
|
|
82
|
-
list-style: none;
|
|
83
|
-
margin: 0;
|
|
84
|
-
padding: 1rem;
|
|
101
|
+
.chat-column {
|
|
85
102
|
display: flex;
|
|
86
103
|
flex-direction: column;
|
|
87
|
-
|
|
104
|
+
min-width: 0;
|
|
105
|
+
padding: 16px 20px 20px;
|
|
106
|
+
gap: 12px;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
gap: 0.15rem;
|
|
94
|
-
padding: 0.6rem 0.85rem;
|
|
95
|
-
border-radius: 10px;
|
|
96
|
-
background: #f8fafc;
|
|
97
|
-
max-width: 85%;
|
|
109
|
+
.status-row {
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
color: var(--text-muted);
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
border:
|
|
114
|
+
.status-dot {
|
|
115
|
+
width: 8px;
|
|
116
|
+
height: 8px;
|
|
117
|
+
border-radius: 99px;
|
|
118
|
+
display: inline-block;
|
|
104
119
|
}
|
|
120
|
+
.status-dot.on { background: #16a34a; }
|
|
121
|
+
.status-dot.off { background: #d97706; }
|
|
105
122
|
|
|
106
|
-
.
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
.chat-frame {
|
|
124
|
+
flex: 1;
|
|
125
|
+
min-height: 420px;
|
|
126
|
+
background: var(--surface-card);
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
border-radius: 16px;
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
109
131
|
}
|
|
110
132
|
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
.chat-frame > div {
|
|
134
|
+
height: 100% !important;
|
|
135
|
+
max-width: none !important;
|
|
136
|
+
border: none !important;
|
|
137
|
+
border-radius: 0 !important;
|
|
115
138
|
}
|
|
116
139
|
|
|
117
|
-
.
|
|
140
|
+
.prompts {
|
|
118
141
|
display: flex;
|
|
119
|
-
flex-
|
|
120
|
-
gap:
|
|
121
|
-
padding: 0.75rem;
|
|
122
|
-
border-top: 1px solid #e7e5e4;
|
|
142
|
+
flex-wrap: wrap;
|
|
143
|
+
gap: 8px;
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
.
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
.prompts button {
|
|
147
|
+
border: 1px solid var(--border);
|
|
148
|
+
background: var(--surface-card);
|
|
149
|
+
color: var(--foreground);
|
|
150
|
+
border-radius: 999px;
|
|
151
|
+
padding: 6px 12px;
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
cursor: pointer;
|
|
128
154
|
}
|
|
129
155
|
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
border-radius: 10px;
|
|
134
|
-
padding: 0.55rem 0.75rem;
|
|
156
|
+
.prompts button:hover {
|
|
157
|
+
border-color: var(--fluxy-cta-color);
|
|
158
|
+
color: var(--fluxy-cta-color);
|
|
135
159
|
}
|
|
136
160
|
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
background: #c2410c;
|
|
141
|
-
color: #fff;
|
|
142
|
-
padding: 0.55rem 1rem;
|
|
143
|
-
cursor: pointer;
|
|
144
|
-
font-weight: 600;
|
|
161
|
+
.chat-error {
|
|
162
|
+
color: var(--destructive);
|
|
163
|
+
font-size: 13px;
|
|
145
164
|
}
|
|
146
165
|
|
|
147
|
-
.
|
|
148
|
-
|
|
166
|
+
.side {
|
|
167
|
+
border-left: 1px solid var(--border);
|
|
168
|
+
background: var(--surface-muted);
|
|
169
|
+
padding: 20px 16px;
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
gap: 16px;
|
|
149
173
|
}
|
|
150
174
|
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
.side section {
|
|
176
|
+
background: var(--surface-card);
|
|
177
|
+
border: 1px solid var(--border);
|
|
178
|
+
border-radius: 12px;
|
|
179
|
+
padding: 14px;
|
|
154
180
|
}
|
|
155
181
|
|
|
156
|
-
.
|
|
157
|
-
font-size:
|
|
158
|
-
|
|
182
|
+
.side h2 {
|
|
183
|
+
font-size: 11px;
|
|
184
|
+
text-transform: uppercase;
|
|
185
|
+
letter-spacing: 0.08em;
|
|
186
|
+
color: var(--text-muted);
|
|
187
|
+
margin: 0 0 10px;
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
background: #f8fafc;
|
|
190
|
+
.side dl {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
gap: 8px;
|
|
194
|
+
margin: 0;
|
|
167
195
|
}
|
|
168
196
|
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
letter-spacing: 0.06em;
|
|
173
|
-
text-transform: uppercase;
|
|
174
|
-
color: #64748b;
|
|
197
|
+
.side dt {
|
|
198
|
+
font-size: 11px;
|
|
199
|
+
color: var(--text-muted);
|
|
175
200
|
}
|
|
176
201
|
|
|
177
|
-
.
|
|
202
|
+
.side dd {
|
|
178
203
|
margin: 0;
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
font-size: 13px;
|
|
205
|
+
word-break: break-all;
|
|
181
206
|
}
|
|
182
207
|
|
|
183
|
-
.side {
|
|
208
|
+
.side ul {
|
|
209
|
+
margin: 0;
|
|
210
|
+
padding-left: 1.1rem;
|
|
211
|
+
color: var(--text-muted);
|
|
212
|
+
font-size: 13px;
|
|
184
213
|
display: flex;
|
|
185
214
|
flex-direction: column;
|
|
186
|
-
gap:
|
|
215
|
+
gap: 8px;
|
|
187
216
|
}
|
|
188
217
|
|
|
189
|
-
.card {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
background:
|
|
194
|
-
|
|
218
|
+
.signin-card {
|
|
219
|
+
margin: auto;
|
|
220
|
+
max-width: 440px;
|
|
221
|
+
width: calc(100% - 32px);
|
|
222
|
+
background: var(--surface-card);
|
|
223
|
+
border: 1px solid var(--border);
|
|
224
|
+
border-radius: 16px;
|
|
225
|
+
padding: 32px;
|
|
226
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
195
227
|
}
|
|
196
228
|
|
|
197
|
-
.
|
|
198
|
-
|
|
199
|
-
font-
|
|
229
|
+
.eyebrow {
|
|
230
|
+
font-size: 11px;
|
|
231
|
+
font-weight: 650;
|
|
232
|
+
letter-spacing: 0.08em;
|
|
200
233
|
text-transform: uppercase;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.card dl {
|
|
206
|
-
margin: 0;
|
|
207
|
-
display: grid;
|
|
208
|
-
gap: 0.35rem;
|
|
234
|
+
color: var(--fluxy-cta-color);
|
|
235
|
+
margin: 0 0 8px;
|
|
209
236
|
}
|
|
210
237
|
|
|
211
|
-
.card
|
|
212
|
-
|
|
213
|
-
font-size:
|
|
238
|
+
.signin-card h1 {
|
|
239
|
+
margin: 0 0 8px;
|
|
240
|
+
font-size: 24px;
|
|
214
241
|
}
|
|
215
242
|
|
|
216
|
-
.card
|
|
217
|
-
|
|
218
|
-
|
|
243
|
+
.signin-card p {
|
|
244
|
+
color: var(--text-muted);
|
|
245
|
+
font-size: 14px;
|
|
246
|
+
line-height: 1.5;
|
|
219
247
|
}
|
|
220
248
|
|
|
221
|
-
.
|
|
222
|
-
|
|
249
|
+
.btn-primary {
|
|
250
|
+
display: inline-flex;
|
|
251
|
+
margin-top: 16px;
|
|
252
|
+
background: var(--fluxy-cta-color);
|
|
253
|
+
color: #fff;
|
|
254
|
+
border-radius: 10px;
|
|
255
|
+
padding: 10px 16px;
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
font-size: 14px;
|
|
258
|
+
text-decoration: none;
|
|
223
259
|
}
|
|
224
260
|
|
|
225
|
-
.
|
|
226
|
-
|
|
227
|
-
background: #fef2f2;
|
|
228
|
-
color: #991b1b;
|
|
229
|
-
border-radius: 12px;
|
|
230
|
-
padding: 1rem;
|
|
231
|
-
font-size: 0.875rem;
|
|
261
|
+
.btn-primary:hover {
|
|
262
|
+
background: #9a3412;
|
|
232
263
|
}
|
|
233
264
|
|
|
234
|
-
.
|
|
235
|
-
|
|
265
|
+
.fine-print {
|
|
266
|
+
margin-top: 12px;
|
|
267
|
+
font-size: 12px !important;
|
|
236
268
|
}
|
|
237
269
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
270
|
+
@media (max-width: 860px) {
|
|
271
|
+
.page {
|
|
272
|
+
grid-template-columns: 1fr;
|
|
273
|
+
}
|
|
274
|
+
.side {
|
|
275
|
+
border-left: none;
|
|
276
|
+
border-top: 1px solid var(--border);
|
|
277
|
+
}
|
|
242
278
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const STORAGE_KEY = "fluxy-cli-session";
|
|
2
|
+
|
|
3
|
+
export interface CliSession {
|
|
4
|
+
workerUrl: string;
|
|
5
|
+
memberJwt: string;
|
|
6
|
+
roomId: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
agentHandle: string;
|
|
9
|
+
projectId: string;
|
|
10
|
+
userId: string;
|
|
11
|
+
projectName?: string;
|
|
12
|
+
consoleUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isCliSession(value: unknown): value is CliSession {
|
|
16
|
+
if (!value || typeof value !== "object") return false;
|
|
17
|
+
const v = value as Record<string, unknown>;
|
|
18
|
+
return typeof v.memberJwt === "string" && v.memberJwt.length > 0 && typeof v.workerUrl === "string";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readCliSessionFromHash(): CliSession | null {
|
|
22
|
+
if (typeof window === "undefined") return null;
|
|
23
|
+
const hash = window.location.hash.replace(/^#/, "");
|
|
24
|
+
const raw = new URLSearchParams(hash).get("fluxy_cli") || (hash.startsWith("fluxy_cli=") ? hash.slice("fluxy_cli=".length) : "");
|
|
25
|
+
if (!raw) return null;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(atob(decodeURIComponent(raw))) as unknown;
|
|
28
|
+
if (!isCliSession(parsed)) return null;
|
|
29
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadCliSession(): CliSession | null {
|
|
37
|
+
const fromHash = readCliSessionFromHash();
|
|
38
|
+
if (fromHash) {
|
|
39
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(fromHash));
|
|
40
|
+
return fromHash;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
44
|
+
if (!stored) return null;
|
|
45
|
+
const parsed = JSON.parse(stored) as unknown;
|
|
46
|
+
return isCliSession(parsed) ? parsed : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clearCliSession() {
|
|
53
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
54
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
content: [
|
|
3
|
+
"./index.html",
|
|
4
|
+
"./src/**/*.{ts,tsx}",
|
|
5
|
+
"./node_modules/@fluxy-chat/ui/dist/**/*.{js,mjs}",
|
|
6
|
+
],
|
|
7
|
+
theme: {
|
|
8
|
+
extend: {
|
|
9
|
+
colors: {
|
|
10
|
+
background: "var(--background)",
|
|
11
|
+
foreground: "var(--foreground)",
|
|
12
|
+
border: "var(--border)",
|
|
13
|
+
muted: {
|
|
14
|
+
DEFAULT: "var(--surface-muted)",
|
|
15
|
+
foreground: "var(--text-muted)",
|
|
16
|
+
},
|
|
17
|
+
primary: {
|
|
18
|
+
DEFAULT: "var(--fluxy-cta-color)",
|
|
19
|
+
foreground: "#ffffff",
|
|
20
|
+
},
|
|
21
|
+
secondary: {
|
|
22
|
+
DEFAULT: "var(--surface-muted)",
|
|
23
|
+
foreground: "var(--foreground)",
|
|
24
|
+
},
|
|
25
|
+
card: {
|
|
26
|
+
DEFAULT: "var(--surface-card)",
|
|
27
|
+
foreground: "var(--foreground)",
|
|
28
|
+
},
|
|
29
|
+
destructive: {
|
|
30
|
+
DEFAULT: "var(--destructive)",
|
|
31
|
+
foreground: "#ffffff",
|
|
32
|
+
},
|
|
33
|
+
ring: "var(--ring)",
|
|
34
|
+
},
|
|
35
|
+
borderRadius: {
|
|
36
|
+
lg: "var(--radius)",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
plugins: [],
|
|
41
|
+
};
|