@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluxy-chat/create-fluxy-chat",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Scaffold a new FluxyChat bot project with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,66 +1,47 @@
1
- # FluxyChat full stack starter
1
+ # FluxyChat full stack starter
2
2
 
3
- Chat + AI agent + tool calls in one Vite app. Provisions against a local FluxyChat worker in ~60 seconds.
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
- **Local (full control):**
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` | Guest JWT via `GET /demo/session` on api.fluxychat.com |
45
- | `pnpm setup:local` | Local `/dev/provision` (same as `pnpm setup`) |
46
- | `pnpm doctor` | Health check: .env, worker, room |
47
- | `pnpm dev` | Start Vite; auto-start worker if FluxyChat monorepo is detected nearby |
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
- See `.env.example`. Generated by `pnpm setup`:
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
- Use [fluxychat.com/onboarding](https://fluxychat.com/onboarding) and copy values into `.env` manually.
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
  }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -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
- if (!jwt) fail("VITE_FLUXYCHAT_MEMBER_JWT unset");
75
- else pass(`JWT present (${jwt.length} chars)`);
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: session.token,
213
- roomId: session.roomId,
214
- agentId: session.agentId ?? "",
215
- agentHandle: session.agentHandle ?? "@assistant",
216
- projectId: "hosted-demo",
217
- userId: session.userId ?? "demo-guest",
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: "hosted-demo",
225
- roomId: session.roomId,
226
- agentId: session.agentId ?? null,
227
- agentHandle: session.agentHandle ?? "@assistant",
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(` Keep this project: ${consoleUrl}/onboarding?from=cli`));
234
- console.log(dim(" Paste your .env in the console to continue onboarding."));
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
- const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim() ?? "";
5
- const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim() ?? "";
6
- const roomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "general";
7
- const agentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() || "";
8
- const agentHandle = import.meta.env.VITE_FLUXYCHAT_AGENT_HANDLE?.trim() || "@assistant";
9
- const projectId = import.meta.env.VITE_FLUXYCHAT_PROJECT_ID?.trim() || "";
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
- function ChatRoom() {
14
- const { messages, sendMessage, invokeAgent, connectionState, agentTyping } = useChat({
15
- roomId,
16
- agentId: agentId || undefined,
17
- markReadLatest: true,
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 isConnected = connectionState.status === "connected";
72
+ const connected = connectionState.status === "connected";
23
73
 
24
- async function handleSend(text: string) {
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
- await sendMessage(text);
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-panel">
57
- <div className="chat-header">
58
- <span className="room-name"># {roomId}</span>
59
- <span className={`status-badge ${isConnected ? "connected" : "disconnected"}`}>
60
- {isConnected ? "live" : connectionState.status}
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="messages-list">
65
- {messages.length === 0 && (
66
- <div className="empty-state">
67
- Send a message or ask the agent something below.
68
- </div>
69
- )}
70
- {messages.map((m) => {
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
- Ask agent
120
- </button>
121
- </form>
122
- </div>
123
- );
124
- }
125
-
126
- function SetupRequired() {
127
- return (
128
- <main className="shell">
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
- pnpm dev`}</code></pre>
144
- <p className="hint">
145
- Or copy <code>.env.example</code> to <code>.env</code> and fill in your credentials
146
- from{" "}
147
- <a href={`${consoleUrl}/onboarding`} target="_blank" rel="noreferrer">
148
- the console
149
- </a>
150
- .
151
- </p>
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
- </main>
136
+ {error ? <p className="chat-error">{error}</p> : null}
137
+ </div>
154
138
  );
155
139
  }
156
140
 
157
141
  export function App() {
158
- if (!workerUrl || !memberJwt) return <SetupRequired />;
142
+ const session = useMemo(() => loadCliSession() ?? sessionFromEnv(), []);
143
+
144
+ if (!session) return <SignInGate />;
159
145
 
160
146
  return (
161
- <main className="shell">
162
- <header className="app-header">
163
- <div className="app-logo">
164
- <svg width="24" height="24" viewBox="0 0 32 32" fill="none">
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="app-nav">
171
- <span className="nav-item">Project: {projectId || "demo"}</span>
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={memberUserId}
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
- <aside className="sidebar">
193
- <div className="sidebar-card">
194
- <h3>Project</h3>
168
+ <aside className="side">
169
+ <section>
170
+ <h2>Project</h2>
195
171
  <dl>
196
- <div><dt>ID</dt><dd>{projectId || "hosted-demo"}</dd></div>
197
- <div><dt>Room</dt><dd>{roomId}</dd></div>
198
- <div><dt>Agent</dt><dd>{agentHandle}</dd></div>
199
- <div><dt>Worker</dt><dd>{workerUrl}</dd></div>
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
- </div>
202
- <div className="sidebar-card">
203
- <h3>Try it</h3>
185
+ </section>
186
+ <section>
187
+ <h2>Next</h2>
204
188
  <ul>
205
- <li>Open a second tab to see realtime sync</li>
206
- <li>Ask the agent about FluxyChat</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
- href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`}
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
- </div>
197
+ </section>
218
198
  </aside>
219
199
  </div>
220
200
  </main>
@@ -1,354 +1,278 @@
1
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
2
4
 
3
5
  :root {
4
- --bg: #0f0f10;
5
- --surface: #18181b;
6
- --surface-2: #27272a;
7
- --border: #3f3f46;
8
- --text: #fafafa;
9
- --text-muted: #a1a1aa;
10
- --accent: #6366f1;
11
- --accent-hover: #4f46e5;
12
- --agent: #10b981;
13
- --danger: #ef4444;
14
- --radius: 10px;
15
- --font: "Inter", system-ui, -apple-system, sans-serif;
16
- }
17
-
18
- html, body, #root { height: 100%; }
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
- font-family: var(--font);
22
- background: var(--bg);
23
- color: var(--text);
24
- font-size: 14px;
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
- a { color: var(--accent); text-decoration: none; }
30
- a:hover { text-decoration: underline; }
31
-
32
- /* ---- App shell ---- */
33
-
34
- .shell {
46
+ .app-shell {
47
+ min-height: 100%;
35
48
  display: flex;
36
49
  flex-direction: column;
37
- height: 100%;
50
+ background: var(--background);
38
51
  }
39
52
 
40
- .app-header {
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
- .app-logo {
63
+ .brand {
52
64
  display: flex;
53
65
  align-items: center;
54
66
  gap: 8px;
55
- font-weight: 600;
67
+ font-weight: 650;
56
68
  font-size: 15px;
57
69
  }
58
70
 
59
- .app-nav {
71
+ .top-links {
60
72
  display: flex;
61
73
  align-items: center;
62
- gap: 16px;
74
+ gap: 14px;
75
+ font-size: 13px;
63
76
  }
64
77
 
65
- .nav-item { color: var(--text-muted); font-size: 13px; }
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
- .nav-link {
68
- font-size: 13px;
69
- color: var(--accent);
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
- /* ---- Layout ---- */
90
+ .meta {
91
+ color: var(--text-muted);
92
+ }
78
93
 
79
- .layout {
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
- /* ---- Chat panel ---- */
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
- border-right: 1px solid var(--border);
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
- .room-name {
107
- font-weight: 600;
108
- font-size: 15px;
109
+ .status-row {
110
+ font-size: 12px;
111
+ color: var(--text-muted);
109
112
  }
110
113
 
111
- .status-badge {
112
- font-size: 11px;
113
- font-weight: 500;
114
- padding: 2px 8px;
114
+ .status-dot {
115
+ width: 8px;
116
+ height: 8px;
115
117
  border-radius: 99px;
116
- letter-spacing: 0.03em;
118
+ display: inline-block;
117
119
  }
118
- .status-badge.connected { background: rgba(16,185,129,0.15); color: var(--agent); }
119
- .status-badge.disconnected { background: rgba(239,68,68,0.15); color: var(--danger); }
120
+ .status-dot.on { background: #16a34a; }
121
+ .status-dot.off { background: #d97706; }
120
122
 
121
- .messages-list {
123
+ .chat-frame {
122
124
  flex: 1;
123
- overflow-y: auto;
124
- padding: 20px;
125
- display: flex;
126
- flex-direction: column;
127
- gap: 2px;
128
- scroll-behavior: smooth;
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
- .message.self .msg-author { color: var(--accent); }
169
- .message.agent .msg-author { color: var(--agent); }
170
-
171
- .msg-body {
172
- color: var(--text);
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
- /* ---- Typing indicator ---- */
179
-
180
- .typing-indicator .msg-body {
140
+ .prompts {
181
141
  display: flex;
182
- gap: 4px;
183
- align-items: center;
184
- padding: 4px 0;
142
+ flex-wrap: wrap;
143
+ gap: 8px;
185
144
  }
186
145
 
187
- .dot {
188
- width: 6px;
189
- height: 6px;
190
- border-radius: 50%;
191
- background: var(--agent);
192
- animation: bounce 1.2s infinite ease-in-out;
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
- @keyframes bounce {
198
- 0%, 80%, 100% { transform: translateY(0); opacity: 0.5; }
199
- 40% { transform: translateY(-4px); opacity: 1; }
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
- margin: 0 20px 8px;
206
- color: var(--danger);
162
+ color: var(--destructive);
207
163
  font-size: 13px;
208
164
  }
209
165
 
210
- .composer {
166
+ .side {
167
+ border-left: 1px solid var(--border);
168
+ background: var(--surface-muted);
169
+ padding: 20px 16px;
211
170
  display: flex;
212
- gap: 8px;
213
- padding: 14px 20px;
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
- .composer input {
221
- flex: 1;
222
- background: var(--surface-2);
175
+ .side section {
176
+ background: var(--surface-card);
223
177
  border: 1px solid var(--border);
224
- border-radius: var(--radius);
225
- padding: 9px 14px;
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
- .composer button[type="submit"] {
249
- background: var(--accent);
250
- color: #fff;
251
- }
252
- .composer button[type="submit"]:hover:not(:disabled) { background: var(--accent-hover); }
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
- .sidebar {
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: 12px;
271
- background: var(--surface);
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
- .sidebar-card h3 {
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
- .sidebar-card dl { display: flex; flex-direction: column; gap: 6px; }
291
- .sidebar-card dl > div { display: flex; flex-direction: column; gap: 1px; }
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
- color: var(--text);
296
- overflow: hidden;
297
- text-overflow: ellipsis;
298
- white-space: nowrap;
205
+ word-break: break-all;
299
206
  }
300
207
 
301
- .sidebar-card ul {
302
- list-style: none;
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
- .setup-card {
218
+ .signin-card {
313
219
  margin: auto;
314
- background: var(--surface);
220
+ max-width: 440px;
221
+ width: calc(100% - 32px);
222
+ background: var(--surface-card);
315
223
  border: 1px solid var(--border);
316
- border-radius: 14px;
317
- padding: 36px 40px;
318
- max-width: 480px;
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
- .setup-card .logo {
326
- display: flex;
327
- align-items: center;
328
- gap: 8px;
329
- font-weight: 700;
330
- font-size: 16px;
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
- .setup-card h1 {
334
- font-size: 22px;
335
- font-weight: 700;
238
+ .signin-card h1 {
239
+ margin: 0 0 8px;
240
+ font-size: 24px;
336
241
  }
337
242
 
338
- .setup-card p { color: var(--text-muted); font-size: 14px; }
339
- .setup-card .hint { font-size: 13px; }
243
+ .signin-card p {
244
+ color: var(--text-muted);
245
+ font-size: 14px;
246
+ line-height: 1.5;
247
+ }
340
248
 
341
- .setup-card pre {
342
- background: var(--surface-2);
343
- border: 1px solid var(--border);
344
- border-radius: 8px;
345
- padding: 14px 16px;
346
- overflow-x: auto;
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
- .setup-card code {
350
- font-family: "Fira Code", "Cascadia Code", monospace;
351
- font-size: 13px;
352
- color: var(--text);
353
- line-height: 1.8;
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
+ };