@fluxy-chat/create-fluxy-chat 0.5.4 → 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 +8 -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 +9 -0
- 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 +155 -175
- package/templates/full/src/index.css +180 -256
- package/templates/full/src/session.ts +54 -0
- package/templates/full/tailwind.config.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
- Fix `FluxyRealtimeProvider` usage: pass `workerUrl` + `authTokenProvider` instead of non-existent `client` prop (fixes "disconnected" state and message sending).
|
|
9
9
|
- Restyle full template UI to match FluxyChat dark theme (indigo/emerald palette, proper message bubbles, live/disconnected badge).
|
|
10
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
|
+
|
|
11
19
|
## [0.5.3] - 2026-08-19
|
|
12
20
|
|
|
13
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,14 +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
25
|
"react-dom": "^19.0.0",
|
|
26
|
+
"tailwind-merge": "^3.0.0",
|
|
21
27
|
"zustand": "^5.0.14"
|
|
22
28
|
},
|
|
23
29
|
"devDependencies": {
|
|
24
30
|
"@types/react": "^19.0.0",
|
|
25
31
|
"@types/react-dom": "^19.0.0",
|
|
26
32
|
"@vitejs/plugin-react": "^4.3.0",
|
|
33
|
+
"autoprefixer": "^10.4.20",
|
|
34
|
+
"postcss": "^8.5.0",
|
|
35
|
+
"tailwindcss": "^3.4.17",
|
|
27
36
|
"typescript": "^5.6.0",
|
|
28
37
|
"vite": "^6.0.0"
|
|
29
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,220 +1,200 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
2
|
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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() ?? "";
|
|
10
13
|
const consoleUrl = import.meta.env.VITE_FLUXYCHAT_CONSOLE_URL?.trim() || "https://fluxychat.com";
|
|
11
|
-
const memberUserId = import.meta.env.VITE_FLUXYCHAT_USER_ID?.trim() || "demo-user";
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
}
|
|
32
|
+
|
|
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)}`;
|
|
36
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
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
|
+
});
|
|
19
70
|
|
|
20
|
-
const [draft, setDraft] = useState("");
|
|
21
71
|
const [error, setError] = useState<string | null>(null);
|
|
22
|
-
const
|
|
72
|
+
const connected = connectionState.status === "connected";
|
|
23
73
|
|
|
24
|
-
async function
|
|
25
|
-
if (!text.trim()) return;
|
|
74
|
+
async function onSend(content: string) {
|
|
26
75
|
setError(null);
|
|
76
|
+
const mentionsAgent = content.toLowerCase().includes(
|
|
77
|
+
(session.agentHandle || "@assistant").replace(/^@/, "").toLowerCase(),
|
|
78
|
+
);
|
|
27
79
|
try {
|
|
28
|
-
|
|
80
|
+
if (mentionsAgent && session.agentId) {
|
|
81
|
+
await invokeAgent(content, { agentId: session.agentId });
|
|
82
|
+
} else {
|
|
83
|
+
await sendMessage(content);
|
|
84
|
+
}
|
|
29
85
|
} catch (err) {
|
|
30
86
|
setError(err instanceof Error ? err.message : "Failed to send");
|
|
31
87
|
}
|
|
32
88
|
}
|
|
33
89
|
|
|
34
|
-
async function handleAskAgent(text: string) {
|
|
35
|
-
if (!agentId) {
|
|
36
|
-
setError("No agent configured. Run pnpm setup again.");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
setError(null);
|
|
40
|
-
try {
|
|
41
|
-
await invokeAgent(text, { agentId });
|
|
42
|
-
} catch (err) {
|
|
43
|
-
setError(err instanceof Error ? err.message : "Agent invoke failed");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function submit(e: React.FormEvent) {
|
|
48
|
-
e.preventDefault();
|
|
49
|
-
const text = draft.trim();
|
|
50
|
-
if (!text) return;
|
|
51
|
-
setDraft("");
|
|
52
|
-
void handleSend(text);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
90
|
return (
|
|
56
|
-
<div className="chat-
|
|
57
|
-
<div className="
|
|
58
|
-
<span className="
|
|
59
|
-
|
|
60
|
-
|
|
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>
|
|
61
99
|
</span>
|
|
62
100
|
</div>
|
|
63
101
|
|
|
64
|
-
<div className="
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const isSelf = m.userId === memberUserId;
|
|
72
|
-
const isAgent = String(m.userId ?? "").includes("agent") || m.userId === "assistant";
|
|
73
|
-
return (
|
|
74
|
-
<div
|
|
75
|
-
key={m.id ?? `${m.createdAt}-${m.userId}`}
|
|
76
|
-
className={`message ${isSelf ? "self" : ""} ${isAgent ? "agent" : ""}`.trim()}
|
|
77
|
-
>
|
|
78
|
-
<span className="msg-author">{m.userId}</span>
|
|
79
|
-
<p className="msg-body">{m.content}</p>
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
})}
|
|
83
|
-
{agentTyping && (
|
|
84
|
-
<div className="message agent typing-indicator">
|
|
85
|
-
<span className="msg-author">{agentHandle}</span>
|
|
86
|
-
<p className="msg-body">
|
|
87
|
-
<span className="dot" />
|
|
88
|
-
<span className="dot" />
|
|
89
|
-
<span className="dot" />
|
|
90
|
-
</p>
|
|
91
|
-
</div>
|
|
92
|
-
)}
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
{error && <p className="chat-error">{error}</p>}
|
|
96
|
-
|
|
97
|
-
<form className="composer" onSubmit={submit}>
|
|
98
|
-
<input
|
|
99
|
-
value={draft}
|
|
100
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
101
|
-
placeholder={`Message #${roomId}…`}
|
|
102
|
-
aria-label="Message"
|
|
103
|
-
autoFocus
|
|
104
|
-
/>
|
|
105
|
-
<button type="submit" disabled={!draft.trim()}>
|
|
106
|
-
Send
|
|
107
|
-
</button>
|
|
108
|
-
<button
|
|
109
|
-
type="button"
|
|
110
|
-
className="btn-agent"
|
|
111
|
-
disabled={!draft.trim()}
|
|
112
|
-
onClick={() => {
|
|
113
|
-
const text = draft.trim();
|
|
114
|
-
if (!text) return;
|
|
115
|
-
setDraft("");
|
|
116
|
-
void handleAskAgent(text.startsWith("@") ? text : `${agentHandle} ${text}`);
|
|
102
|
+
<div className="chat-frame">
|
|
103
|
+
<ChatWindow
|
|
104
|
+
messages={messages}
|
|
105
|
+
online={online ?? 0}
|
|
106
|
+
typingUsers={typingUsers ?? {}}
|
|
107
|
+
onSend={(content) => {
|
|
108
|
+
void onSend(content);
|
|
117
109
|
}}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<div className="setup-card">
|
|
130
|
-
<div className="logo">
|
|
131
|
-
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
132
|
-
<rect width="32" height="32" rx="8" fill="#6366f1" />
|
|
133
|
-
<path d="M8 10h16M8 16h10M8 22h13" stroke="white" strokeWidth="2" strokeLinecap="round" />
|
|
134
|
-
</svg>
|
|
135
|
-
<span>FluxyChat</span>
|
|
136
|
-
</div>
|
|
137
|
-
<h1>Setup required</h1>
|
|
138
|
-
<p>Run the setup script to provision your credentials, then start the app.</p>
|
|
139
|
-
<pre><code>{`pnpm setup:hosted # hosted demo (no local worker)
|
|
140
|
-
# — or —
|
|
141
|
-
pnpm setup # local worker (pnpm dev in monorepo first)
|
|
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>
|
|
142
121
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
122
|
+
<div className="prompts">
|
|
123
|
+
{SUGGESTED_PROMPTS.map((prompt) => (
|
|
124
|
+
<button
|
|
125
|
+
key={prompt}
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={() => {
|
|
128
|
+
const handle = session.agentHandle || "@assistant";
|
|
129
|
+
void onSend(`${handle} ${prompt}`);
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{prompt}
|
|
133
|
+
</button>
|
|
134
|
+
))}
|
|
152
135
|
</div>
|
|
153
|
-
|
|
136
|
+
{error ? <p className="chat-error">{error}</p> : null}
|
|
137
|
+
</div>
|
|
154
138
|
);
|
|
155
139
|
}
|
|
156
140
|
|
|
157
141
|
export function App() {
|
|
158
|
-
|
|
142
|
+
const session = useMemo(() => loadCliSession() ?? sessionFromEnv(), []);
|
|
143
|
+
|
|
144
|
+
if (!session) return <SignInGate />;
|
|
159
145
|
|
|
160
146
|
return (
|
|
161
|
-
<main className="shell">
|
|
162
|
-
<header className="
|
|
163
|
-
<div className="
|
|
164
|
-
<
|
|
165
|
-
<rect width="32" height="32" rx="8" fill="#6366f1" />
|
|
166
|
-
<path d="M8 10h16M8 16h10M8 22h13" stroke="white" strokeWidth="2" strokeLinecap="round" />
|
|
167
|
-
</svg>
|
|
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} />
|
|
168
151
|
<span>FluxyChat</span>
|
|
169
152
|
</div>
|
|
170
|
-
<nav className="
|
|
171
|
-
<span className="
|
|
172
|
-
<a
|
|
173
|
-
href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`}
|
|
174
|
-
target="_blank"
|
|
175
|
-
rel="noreferrer"
|
|
176
|
-
className="nav-link"
|
|
177
|
-
>
|
|
153
|
+
<nav className="top-links">
|
|
154
|
+
<span className="meta">{session.projectName || session.projectId || "project"}</span>
|
|
155
|
+
<a href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`} target="_blank" rel="noreferrer">
|
|
178
156
|
Open console
|
|
179
157
|
</a>
|
|
180
158
|
</nav>
|
|
181
159
|
</header>
|
|
182
|
-
|
|
183
|
-
<div className="layout">
|
|
160
|
+
<div className="page">
|
|
184
161
|
<FluxyRealtimeProvider
|
|
185
|
-
workerUrl={workerUrl}
|
|
186
|
-
authTokenProvider={memberJwt}
|
|
187
|
-
userId={
|
|
162
|
+
workerUrl={session.workerUrl}
|
|
163
|
+
authTokenProvider={session.memberJwt}
|
|
164
|
+
userId={session.userId}
|
|
188
165
|
>
|
|
189
|
-
<ChatRoom />
|
|
166
|
+
<ChatRoom session={session} />
|
|
190
167
|
</FluxyRealtimeProvider>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<h3>Project</h3>
|
|
168
|
+
<aside className="side">
|
|
169
|
+
<section>
|
|
170
|
+
<h2>Project</h2>
|
|
195
171
|
<dl>
|
|
196
|
-
<div
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
172
|
+
<div>
|
|
173
|
+
<dt>ID</dt>
|
|
174
|
+
<dd>{session.projectId || "—"}</dd>
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
<dt>Room</dt>
|
|
178
|
+
<dd>{session.roomId}</dd>
|
|
179
|
+
</div>
|
|
180
|
+
<div>
|
|
181
|
+
<dt>Agent</dt>
|
|
182
|
+
<dd>{session.agentHandle}</dd>
|
|
183
|
+
</div>
|
|
200
184
|
</dl>
|
|
201
|
-
</
|
|
202
|
-
<
|
|
203
|
-
<
|
|
185
|
+
</section>
|
|
186
|
+
<section>
|
|
187
|
+
<h2>Next</h2>
|
|
204
188
|
<ul>
|
|
205
|
-
<li>
|
|
206
|
-
<li>
|
|
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>
|
|
207
191
|
<li>
|
|
208
|
-
<a
|
|
209
|
-
|
|
210
|
-
target="_blank"
|
|
211
|
-
rel="noreferrer"
|
|
212
|
-
>
|
|
213
|
-
Keep this project in the console
|
|
192
|
+
<a href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`} target="_blank" rel="noreferrer">
|
|
193
|
+
Continue onboarding in the console
|
|
214
194
|
</a>
|
|
215
195
|
</li>
|
|
216
196
|
</ul>
|
|
217
|
-
</
|
|
197
|
+
</section>
|
|
218
198
|
</aside>
|
|
219
199
|
</div>
|
|
220
200
|
</main>
|
|
@@ -1,354 +1,278 @@
|
|
|
1
|
-
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
2
4
|
|
|
3
5
|
:root {
|
|
4
|
-
--
|
|
5
|
-
--
|
|
6
|
-
--surface
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
--text-
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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%;
|
|
36
|
+
}
|
|
19
37
|
|
|
20
38
|
body {
|
|
21
|
-
|
|
22
|
-
background: var(--
|
|
23
|
-
color: var(--
|
|
24
|
-
font-
|
|
25
|
-
line-height: 1.5;
|
|
39
|
+
margin: 0;
|
|
40
|
+
background: var(--background);
|
|
41
|
+
color: var(--foreground);
|
|
42
|
+
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
26
43
|
-webkit-font-smoothing: antialiased;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/* ---- App shell ---- */
|
|
33
|
-
|
|
34
|
-
.shell {
|
|
46
|
+
.app-shell {
|
|
47
|
+
min-height: 100%;
|
|
35
48
|
display: flex;
|
|
36
49
|
flex-direction: column;
|
|
37
|
-
|
|
50
|
+
background: var(--background);
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
.
|
|
53
|
+
.topbar {
|
|
54
|
+
height: 56px;
|
|
41
55
|
display: flex;
|
|
42
56
|
align-items: center;
|
|
43
57
|
justify-content: space-between;
|
|
44
58
|
padding: 0 20px;
|
|
45
|
-
height: 52px;
|
|
46
59
|
border-bottom: 1px solid var(--border);
|
|
47
|
-
background: var(--surface);
|
|
48
|
-
flex-shrink: 0;
|
|
60
|
+
background: var(--surface-card);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
.
|
|
63
|
+
.brand {
|
|
52
64
|
display: flex;
|
|
53
65
|
align-items: center;
|
|
54
66
|
gap: 8px;
|
|
55
|
-
font-weight:
|
|
67
|
+
font-weight: 650;
|
|
56
68
|
font-size: 15px;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
.
|
|
71
|
+
.top-links {
|
|
60
72
|
display: flex;
|
|
61
73
|
align-items: center;
|
|
62
|
-
gap:
|
|
74
|
+
gap: 14px;
|
|
75
|
+
font-size: 13px;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
.
|
|
78
|
+
.top-links a,
|
|
79
|
+
.side a {
|
|
80
|
+
color: var(--fluxy-cta-color);
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
}
|
|
66
84
|
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
border: 1px solid var(--accent);
|
|
71
|
-
border-radius: 6px;
|
|
72
|
-
padding: 4px 10px;
|
|
73
|
-
transition: background 0.15s;
|
|
85
|
+
.top-links a:hover,
|
|
86
|
+
.side a:hover {
|
|
87
|
+
text-decoration: underline;
|
|
74
88
|
}
|
|
75
|
-
.nav-link:hover { background: rgba(99,102,241,0.1); text-decoration: none; }
|
|
76
89
|
|
|
77
|
-
|
|
90
|
+
.meta {
|
|
91
|
+
color: var(--text-muted);
|
|
92
|
+
}
|
|
78
93
|
|
|
79
|
-
.
|
|
80
|
-
display: flex;
|
|
94
|
+
.page {
|
|
81
95
|
flex: 1;
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
82
98
|
min-height: 0;
|
|
83
|
-
overflow: hidden;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.chat-panel {
|
|
89
|
-
flex: 1;
|
|
101
|
+
.chat-column {
|
|
90
102
|
display: flex;
|
|
91
103
|
flex-direction: column;
|
|
92
104
|
min-width: 0;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.chat-header {
|
|
97
|
-
display: flex;
|
|
98
|
-
align-items: center;
|
|
99
|
-
gap: 10px;
|
|
100
|
-
padding: 12px 20px;
|
|
101
|
-
border-bottom: 1px solid var(--border);
|
|
102
|
-
background: var(--surface);
|
|
103
|
-
flex-shrink: 0;
|
|
105
|
+
padding: 16px 20px 20px;
|
|
106
|
+
gap: 12px;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
.
|
|
107
|
-
font-
|
|
108
|
-
|
|
109
|
+
.status-row {
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
color: var(--text-muted);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
.status-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
padding: 2px 8px;
|
|
114
|
+
.status-dot {
|
|
115
|
+
width: 8px;
|
|
116
|
+
height: 8px;
|
|
115
117
|
border-radius: 99px;
|
|
116
|
-
|
|
118
|
+
display: inline-block;
|
|
117
119
|
}
|
|
118
|
-
.status-
|
|
119
|
-
.status-
|
|
120
|
+
.status-dot.on { background: #16a34a; }
|
|
121
|
+
.status-dot.off { background: #d97706; }
|
|
120
122
|
|
|
121
|
-
.
|
|
123
|
+
.chat-frame {
|
|
122
124
|
flex: 1;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.empty-state {
|
|
132
|
-
margin: auto;
|
|
133
|
-
text-align: center;
|
|
134
|
-
color: var(--text-muted);
|
|
135
|
-
font-size: 14px;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/* ---- Messages ---- */
|
|
139
|
-
|
|
140
|
-
.message {
|
|
141
|
-
display: flex;
|
|
142
|
-
flex-direction: column;
|
|
143
|
-
gap: 2px;
|
|
144
|
-
padding: 6px 10px;
|
|
145
|
-
border-radius: 8px;
|
|
146
|
-
max-width: 75%;
|
|
147
|
-
align-self: flex-start;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.message.self {
|
|
151
|
-
align-self: flex-end;
|
|
152
|
-
background: rgba(99,102,241,0.12);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.message.agent {
|
|
156
|
-
background: rgba(16,185,129,0.08);
|
|
157
|
-
border-left: 2px solid var(--agent);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.msg-author {
|
|
161
|
-
font-size: 11px;
|
|
162
|
-
font-weight: 600;
|
|
163
|
-
color: var(--text-muted);
|
|
164
|
-
text-transform: uppercase;
|
|
165
|
-
letter-spacing: 0.05em;
|
|
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);
|
|
166
131
|
}
|
|
167
132
|
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
line-height: 1.55;
|
|
174
|
-
word-break: break-word;
|
|
175
|
-
white-space: pre-wrap;
|
|
133
|
+
.chat-frame > div {
|
|
134
|
+
height: 100% !important;
|
|
135
|
+
max-width: none !important;
|
|
136
|
+
border: none !important;
|
|
137
|
+
border-radius: 0 !important;
|
|
176
138
|
}
|
|
177
139
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.typing-indicator .msg-body {
|
|
140
|
+
.prompts {
|
|
181
141
|
display: flex;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
padding: 4px 0;
|
|
142
|
+
flex-wrap: wrap;
|
|
143
|
+
gap: 8px;
|
|
185
144
|
}
|
|
186
145
|
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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;
|
|
193
154
|
}
|
|
194
|
-
.dot:nth-child(2) { animation-delay: 0.2s; }
|
|
195
|
-
.dot:nth-child(3) { animation-delay: 0.4s; }
|
|
196
155
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
156
|
+
.prompts button:hover {
|
|
157
|
+
border-color: var(--fluxy-cta-color);
|
|
158
|
+
color: var(--fluxy-cta-color);
|
|
200
159
|
}
|
|
201
160
|
|
|
202
|
-
/* ---- Composer ---- */
|
|
203
|
-
|
|
204
161
|
.chat-error {
|
|
205
|
-
|
|
206
|
-
color: var(--danger);
|
|
162
|
+
color: var(--destructive);
|
|
207
163
|
font-size: 13px;
|
|
208
164
|
}
|
|
209
165
|
|
|
210
|
-
.
|
|
166
|
+
.side {
|
|
167
|
+
border-left: 1px solid var(--border);
|
|
168
|
+
background: var(--surface-muted);
|
|
169
|
+
padding: 20px 16px;
|
|
211
170
|
display: flex;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
border-top: 1px solid var(--border);
|
|
215
|
-
background: var(--surface);
|
|
216
|
-
flex-shrink: 0;
|
|
217
|
-
align-items: center;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
gap: 16px;
|
|
218
173
|
}
|
|
219
174
|
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
background: var(--surface-2);
|
|
175
|
+
.side section {
|
|
176
|
+
background: var(--surface-card);
|
|
223
177
|
border: 1px solid var(--border);
|
|
224
|
-
border-radius:
|
|
225
|
-
padding:
|
|
226
|
-
color: var(--text);
|
|
227
|
-
font-size: 14px;
|
|
228
|
-
font-family: var(--font);
|
|
229
|
-
outline: none;
|
|
230
|
-
transition: border-color 0.15s;
|
|
231
|
-
}
|
|
232
|
-
.composer input:focus { border-color: var(--accent); }
|
|
233
|
-
.composer input::placeholder { color: var(--text-muted); }
|
|
234
|
-
|
|
235
|
-
.composer button {
|
|
236
|
-
padding: 9px 16px;
|
|
237
|
-
border-radius: var(--radius);
|
|
238
|
-
border: none;
|
|
239
|
-
font-size: 13px;
|
|
240
|
-
font-weight: 500;
|
|
241
|
-
cursor: pointer;
|
|
242
|
-
transition: background 0.15s, opacity 0.15s;
|
|
243
|
-
white-space: nowrap;
|
|
244
|
-
font-family: var(--font);
|
|
178
|
+
border-radius: 12px;
|
|
179
|
+
padding: 14px;
|
|
245
180
|
}
|
|
246
|
-
.composer button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
247
181
|
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.composer .btn-agent {
|
|
255
|
-
background: rgba(16,185,129,0.12);
|
|
256
|
-
color: var(--agent);
|
|
257
|
-
border: 1px solid rgba(16,185,129,0.3);
|
|
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;
|
|
258
188
|
}
|
|
259
|
-
.composer .btn-agent:hover:not(:disabled) { background: rgba(16,185,129,0.2); }
|
|
260
|
-
|
|
261
|
-
/* ---- Sidebar ---- */
|
|
262
189
|
|
|
263
|
-
.
|
|
264
|
-
width: 280px;
|
|
265
|
-
flex-shrink: 0;
|
|
266
|
-
overflow-y: auto;
|
|
267
|
-
padding: 20px 16px;
|
|
190
|
+
.side dl {
|
|
268
191
|
display: flex;
|
|
269
192
|
flex-direction: column;
|
|
270
|
-
gap:
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
.sidebar-card {
|
|
275
|
-
background: var(--surface-2);
|
|
276
|
-
border: 1px solid var(--border);
|
|
277
|
-
border-radius: var(--radius);
|
|
278
|
-
padding: 14px 16px;
|
|
193
|
+
gap: 8px;
|
|
194
|
+
margin: 0;
|
|
279
195
|
}
|
|
280
196
|
|
|
281
|
-
.
|
|
197
|
+
.side dt {
|
|
282
198
|
font-size: 11px;
|
|
283
|
-
font-weight: 600;
|
|
284
|
-
text-transform: uppercase;
|
|
285
|
-
letter-spacing: 0.07em;
|
|
286
199
|
color: var(--text-muted);
|
|
287
|
-
margin-bottom: 10px;
|
|
288
200
|
}
|
|
289
201
|
|
|
290
|
-
.
|
|
291
|
-
|
|
292
|
-
.sidebar-card dt { font-size: 11px; color: var(--text-muted); }
|
|
293
|
-
.sidebar-card dd {
|
|
202
|
+
.side dd {
|
|
203
|
+
margin: 0;
|
|
294
204
|
font-size: 13px;
|
|
295
|
-
|
|
296
|
-
overflow: hidden;
|
|
297
|
-
text-overflow: ellipsis;
|
|
298
|
-
white-space: nowrap;
|
|
205
|
+
word-break: break-all;
|
|
299
206
|
}
|
|
300
207
|
|
|
301
|
-
.
|
|
302
|
-
|
|
208
|
+
.side ul {
|
|
209
|
+
margin: 0;
|
|
210
|
+
padding-left: 1.1rem;
|
|
211
|
+
color: var(--text-muted);
|
|
212
|
+
font-size: 13px;
|
|
303
213
|
display: flex;
|
|
304
214
|
flex-direction: column;
|
|
305
215
|
gap: 8px;
|
|
306
216
|
}
|
|
307
|
-
.sidebar-card li { font-size: 13px; color: var(--text-muted); }
|
|
308
|
-
.sidebar-card li a { color: var(--accent); }
|
|
309
|
-
|
|
310
|
-
/* ---- Setup required ---- */
|
|
311
217
|
|
|
312
|
-
.
|
|
218
|
+
.signin-card {
|
|
313
219
|
margin: auto;
|
|
314
|
-
|
|
220
|
+
max-width: 440px;
|
|
221
|
+
width: calc(100% - 32px);
|
|
222
|
+
background: var(--surface-card);
|
|
315
223
|
border: 1px solid var(--border);
|
|
316
|
-
border-radius:
|
|
317
|
-
padding:
|
|
318
|
-
|
|
319
|
-
width: 90%;
|
|
320
|
-
display: flex;
|
|
321
|
-
flex-direction: column;
|
|
322
|
-
gap: 16px;
|
|
224
|
+
border-radius: 16px;
|
|
225
|
+
padding: 32px;
|
|
226
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
323
227
|
}
|
|
324
228
|
|
|
325
|
-
.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
229
|
+
.eyebrow {
|
|
230
|
+
font-size: 11px;
|
|
231
|
+
font-weight: 650;
|
|
232
|
+
letter-spacing: 0.08em;
|
|
233
|
+
text-transform: uppercase;
|
|
234
|
+
color: var(--fluxy-cta-color);
|
|
235
|
+
margin: 0 0 8px;
|
|
331
236
|
}
|
|
332
237
|
|
|
333
|
-
.
|
|
334
|
-
|
|
335
|
-
font-
|
|
238
|
+
.signin-card h1 {
|
|
239
|
+
margin: 0 0 8px;
|
|
240
|
+
font-size: 24px;
|
|
336
241
|
}
|
|
337
242
|
|
|
338
|
-
.
|
|
339
|
-
|
|
243
|
+
.signin-card p {
|
|
244
|
+
color: var(--text-muted);
|
|
245
|
+
font-size: 14px;
|
|
246
|
+
line-height: 1.5;
|
|
247
|
+
}
|
|
340
248
|
|
|
341
|
-
.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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;
|
|
347
259
|
}
|
|
348
260
|
|
|
349
|
-
.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
261
|
+
.btn-primary:hover {
|
|
262
|
+
background: #9a3412;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.fine-print {
|
|
266
|
+
margin-top: 12px;
|
|
267
|
+
font-size: 12px !important;
|
|
268
|
+
}
|
|
269
|
+
|
|
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
|
+
}
|
|
354
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
|
+
};
|