@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.
- package/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/templates/full/README.md +13 -32
- package/templates/full/index.html +3 -0
- package/templates/full/package.json +15 -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 +214 -183
- package/templates/full/src/index.css +245 -244
- package/templates/full/src/session.ts +54 -0
- package/templates/full/tailwind.config.js +41 -0
- package/templates/full/vite.config.ts +7 -0
|
@@ -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
|
-
|
|
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 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
|
|
127
|
+
const connected = connectionState.status === "connected";
|
|
23
128
|
|
|
24
|
-
async function
|
|
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
|
-
|
|
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-
|
|
57
|
-
<div className="
|
|
58
|
-
<span className="
|
|
59
|
-
|
|
60
|
-
|
|
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="
|
|
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}`);
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
191
|
+
{error ? <p className="chat-error">{error}</p> : null}
|
|
192
|
+
</div>
|
|
154
193
|
);
|
|
155
194
|
}
|
|
156
195
|
|
|
157
196
|
export function App() {
|
|
158
|
-
|
|
197
|
+
const session = useMemo(() => loadCliSession() ?? sessionFromEnv(), []);
|
|
198
|
+
|
|
199
|
+
if (!session) return <LocalOnboarding />;
|
|
159
200
|
|
|
160
201
|
return (
|
|
161
|
-
<main className="shell">
|
|
162
|
-
<header className="
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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={
|
|
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
|
-
|
|
193
|
-
|
|
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
|
|
206
|
-
<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
|
-
</
|
|
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>
|