@fluxy-chat/create-fluxy-chat 0.5.4 → 0.5.6

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.
@@ -1,220 +1,251 @@
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 consoleOrigin(): string {
21
+ return consoleUrl.replace(/\/$/, "");
22
+ }
23
+
24
+ function clerkAuthHref(): string {
25
+ const returnTo = typeof window !== "undefined" ? window.location.origin : "http://localhost:5173";
26
+ return `${consoleOrigin()}/cli-auth?redirect_uri=${encodeURIComponent(returnTo)}`;
27
+ }
28
+
29
+ function dashboardHref(): string {
30
+ return `${consoleOrigin()}/dashboard`;
31
+ }
32
+
33
+ function sessionFromEnv(): CliSession | null {
34
+ if (!envWorkerUrl || !envJwt || !envRoomId) return null;
35
+ return {
36
+ workerUrl: envWorkerUrl,
37
+ memberJwt: envJwt,
38
+ roomId: envRoomId,
39
+ agentId: envAgentId,
40
+ agentHandle: envAgentHandle,
41
+ projectId: envProjectId,
42
+ userId: envUserId || "demo-user",
43
+ };
44
+ }
45
+
46
+ function Brand() {
47
+ return (
48
+ <div className="brand">
49
+ <img src={`${consoleOrigin()}/fluxychat-icon.svg`} alt="" width={28} height={28} />
50
+ <span>FluxyChat</span>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ const TOUR_STEPS = [
56
+ {
57
+ title: "You already have an app",
58
+ body: "This folder is a real Vite chat app. After a short sign-in we create your project and a private room. No public playground.",
59
+ },
60
+ {
61
+ title: "Realtime is the point",
62
+ body: "After you are in, open this same URL in a second tab. Send a message in one window and watch it land in the other.",
63
+ },
64
+ {
65
+ title: "Then the console",
66
+ body: "Sign in is required: that is what creates (or reuses) your project and assistant room. After that the two-tab demo works. The dashboard is a separate button once you are in chat.",
67
+ },
68
+ ] as const;
69
+
70
+ function LocalOnboarding() {
71
+ const [step, setStep] = useState(0);
72
+ const last = step === TOUR_STEPS.length - 1;
73
+ const current = TOUR_STEPS[step];
74
+
75
+ return (
76
+ <main className="app-shell">
77
+ <header className="topbar">
78
+ <Brand />
79
+ <span className="meta">
80
+ {step + 1} / {TOUR_STEPS.length}
81
+ </span>
82
+ </header>
83
+ <div className="onboard">
84
+ <div className="dots" aria-hidden>
85
+ {TOUR_STEPS.map((_, i) => (
86
+ <span key={i} className={`dot-step ${i === step ? "active" : ""}`} />
87
+ ))}
88
+ </div>
89
+ <p className="eyebrow">Quick start</p>
90
+ <h1>{current.title}</h1>
91
+ <p>{current.body}</p>
92
+ <div className="onboard-actions">
93
+ {step > 0 ? (
94
+ <button type="button" className="btn-ghost" onClick={() => setStep((s) => s - 1)}>
95
+ Back
96
+ </button>
97
+ ) : null}
98
+ {last ? (
99
+ <a className="btn-primary" href={clerkAuthHref()}>
100
+ Sign in or create account
101
+ </a>
102
+ ) : (
103
+ <button type="button" className="btn-primary" onClick={() => setStep((s) => s + 1)}>
104
+ Continue
105
+ </button>
106
+ )}
107
+ </div>
108
+ {last ? (
109
+ <p className="fine-print">
110
+ Clerk on fluxychat.com, then back here with your room ready.
111
+ </p>
112
+ ) : null}
113
+ </div>
114
+ </main>
115
+ );
116
+ }
117
+
118
+ function ChatRoom({ session }: { session: CliSession }) {
119
+ const { messages, sendMessage, invokeAgent, connectionState, agentTyping, typingUsers, online } =
120
+ useChat({
121
+ roomId: session.roomId,
122
+ agentId: session.agentId || undefined,
123
+ markReadLatest: true,
124
+ });
19
125
 
20
- const [draft, setDraft] = useState("");
21
126
  const [error, setError] = useState<string | null>(null);
22
- const isConnected = connectionState.status === "connected";
127
+ const connected = connectionState.status === "connected";
23
128
 
24
- async function handleSend(text: string) {
25
- if (!text.trim()) return;
129
+ async function onSend(content: string) {
26
130
  setError(null);
131
+ const mentionsAgent = content
132
+ .toLowerCase()
133
+ .includes((session.agentHandle || "@assistant").replace(/^@/, "").toLowerCase());
27
134
  try {
28
- await sendMessage(text);
135
+ if (mentionsAgent && session.agentId) {
136
+ await invokeAgent(content, { agentId: session.agentId });
137
+ } else {
138
+ await sendMessage(content);
139
+ }
29
140
  } catch (err) {
30
141
  setError(err instanceof Error ? err.message : "Failed to send");
31
142
  }
32
143
  }
33
144
 
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
145
  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}
146
+ <div className="chat-column">
147
+ <div className="status-row">
148
+ <span className="inline-flex items-center gap-2">
149
+ <span className={`status-dot ${connected ? "on" : "off"}`} />
150
+ <span className="font-medium text-foreground">
151
+ {connected ? "Connected" : connectionState.status}
152
+ </span>
153
+ <span className="text-muted-foreground"> · {session.roomId}</span>
61
154
  </span>
62
155
  </div>
63
156
 
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}`);
157
+ <div className="chat-frame">
158
+ <ChatWindow
159
+ messages={messages}
160
+ online={online ?? 0}
161
+ typingUsers={typingUsers ?? {}}
162
+ onSend={(content) => {
163
+ void onSend(content);
117
164
  }}
118
- >
119
- Ask agent
120
- </button>
121
- </form>
122
- </div>
123
- );
124
- }
165
+ agentTyping={Boolean(agentTyping)}
166
+ agentTypingLabel={session.agentHandle || "@assistant"}
167
+ mentionSuggestions={[
168
+ {
169
+ handle: (session.agentHandle || "@assistant").replace(/^@/, ""),
170
+ label: session.agentHandle || "@assistant",
171
+ agentId: session.agentId,
172
+ },
173
+ ]}
174
+ />
175
+ </div>
125
176
 
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)
142
-
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>
177
+ <div className="prompts">
178
+ {SUGGESTED_PROMPTS.map((prompt) => (
179
+ <button
180
+ key={prompt}
181
+ type="button"
182
+ onClick={() => {
183
+ const handle = session.agentHandle || "@assistant";
184
+ void onSend(`${handle} ${prompt}`);
185
+ }}
186
+ >
187
+ {prompt}
188
+ </button>
189
+ ))}
152
190
  </div>
153
- </main>
191
+ {error ? <p className="chat-error">{error}</p> : null}
192
+ </div>
154
193
  );
155
194
  }
156
195
 
157
196
  export function App() {
158
- if (!workerUrl || !memberJwt) return <SetupRequired />;
197
+ const session = useMemo(() => loadCliSession() ?? sessionFromEnv(), []);
198
+
199
+ if (!session) return <LocalOnboarding />;
159
200
 
160
201
  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>
168
- <span>FluxyChat</span>
169
- </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
- >
178
- Open console
202
+ <main className="app-shell">
203
+ <header className="topbar">
204
+ <Brand />
205
+ <nav className="top-links">
206
+ <span className="meta">{session.projectName || session.projectId || "your room"}</span>
207
+ <a href={dashboardHref()} target="_blank" rel="noreferrer">
208
+ Open dashboard
179
209
  </a>
180
210
  </nav>
181
211
  </header>
182
-
183
- <div className="layout">
212
+ <div className="page">
184
213
  <FluxyRealtimeProvider
185
- workerUrl={workerUrl}
186
- authTokenProvider={memberJwt}
187
- userId={memberUserId}
214
+ workerUrl={session.workerUrl}
215
+ authTokenProvider={session.memberJwt}
216
+ userId={session.userId}
188
217
  >
189
- <ChatRoom />
218
+ <ChatRoom session={session} />
190
219
  </FluxyRealtimeProvider>
191
-
192
- <aside className="sidebar">
193
- <div className="sidebar-card">
194
- <h3>Project</h3>
195
- <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>
200
- </dl>
201
- </div>
202
- <div className="sidebar-card">
203
- <h3>Try it</h3>
220
+ <aside className="side">
221
+ <section>
222
+ <h2>Try this</h2>
204
223
  <ul>
205
- <li>Open a second tab to see realtime sync</li>
206
- <li>Ask the agent about FluxyChat</li>
207
- <li>
208
- <a
209
- href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`}
210
- target="_blank"
211
- rel="noreferrer"
212
- >
213
- Keep this project in the console
214
- </a>
215
- </li>
224
+ <li>Open a second tab on this same URL</li>
225
+ <li>Send a message in one tab. It should appear in the other.</li>
226
+ <li>Mention the agent to get a reply</li>
216
227
  </ul>
217
- </div>
228
+ </section>
229
+ <section>
230
+ <h2>Project</h2>
231
+ <dl>
232
+ <div>
233
+ <dt>Room</dt>
234
+ <dd>{session.roomId}</dd>
235
+ </div>
236
+ <div>
237
+ <dt>Agent</dt>
238
+ <dd>{session.agentHandle}</dd>
239
+ </div>
240
+ </dl>
241
+ </section>
242
+ <section>
243
+ <h2>Console</h2>
244
+ <p className="side-copy">Rooms, agents, and settings live in the dashboard.</p>
245
+ <a className="btn-primary side-btn" href={dashboardHref()} target="_blank" rel="noreferrer">
246
+ Open dashboard
247
+ </a>
248
+ </section>
218
249
  </aside>
219
250
  </div>
220
251
  </main>