@fluxy-chat/create-fluxy-chat 0.5.3 → 0.5.4
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/package.json +2 -1
- package/templates/full/src/App.tsx +160 -161
- package/templates/full/src/index.css +258 -146
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.4] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Add `zustand` to `full` template dependencies so `pnpm install` gets everything without manual intervention.
|
|
8
|
+
- Fix `FluxyRealtimeProvider` usage: pass `workerUrl` + `authTokenProvider` instead of non-existent `client` prop (fixes "disconnected" state and message sending).
|
|
9
|
+
- Restyle full template UI to match FluxyChat dark theme (indigo/emerald palette, proper message bubbles, live/disconnected badge).
|
|
10
|
+
|
|
3
11
|
## [0.5.3] - 2026-08-19
|
|
4
12
|
|
|
5
13
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,219 +1,218 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FluxyChatClient } from "@fluxy-chat/sdk";
|
|
1
|
+
import { useState } from "react";
|
|
3
2
|
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
|
|
4
3
|
|
|
5
|
-
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
|
|
6
|
-
const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
|
|
4
|
+
const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim() ?? "";
|
|
5
|
+
const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim() ?? "";
|
|
7
6
|
const roomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "general";
|
|
8
7
|
const agentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() || "";
|
|
9
8
|
const agentHandle = import.meta.env.VITE_FLUXYCHAT_AGENT_HANDLE?.trim() || "@assistant";
|
|
10
9
|
const projectId = import.meta.env.VITE_FLUXYCHAT_PROJECT_ID?.trim() || "";
|
|
11
|
-
const consoleUrl = import.meta.env.VITE_FLUXYCHAT_CONSOLE_URL?.trim() || "
|
|
12
|
-
const memberUserId =
|
|
13
|
-
import.meta.env.VITE_FLUXYCHAT_USER_ID?.trim() || "demo-user";
|
|
10
|
+
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";
|
|
14
12
|
|
|
15
13
|
function ChatRoom() {
|
|
16
|
-
const {
|
|
17
|
-
messages,
|
|
18
|
-
sendMessage,
|
|
19
|
-
invokeAgent,
|
|
20
|
-
connectionState,
|
|
21
|
-
agentTyping,
|
|
22
|
-
toolThreadEvents,
|
|
23
|
-
lastAgentRun,
|
|
24
|
-
} = useChat({
|
|
14
|
+
const { messages, sendMessage, invokeAgent, connectionState, agentTyping } = useChat({
|
|
25
15
|
roomId,
|
|
26
16
|
agentId: agentId || undefined,
|
|
27
17
|
markReadLatest: true,
|
|
28
18
|
});
|
|
29
19
|
|
|
30
20
|
const [draft, setDraft] = useState("");
|
|
31
|
-
const [
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
const isConnected = connectionState.status === "connected";
|
|
32
23
|
|
|
33
|
-
async function
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
async function handleSend(text: string) {
|
|
25
|
+
if (!text.trim()) return;
|
|
26
|
+
setError(null);
|
|
27
|
+
try {
|
|
28
|
+
await sendMessage(text);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setError(err instanceof Error ? err.message : "Failed to send");
|
|
31
|
+
}
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
async function
|
|
34
|
+
async function handleAskAgent(text: string) {
|
|
39
35
|
if (!agentId) {
|
|
40
|
-
|
|
36
|
+
setError("No agent configured. Run pnpm setup again.");
|
|
41
37
|
return;
|
|
42
38
|
}
|
|
43
|
-
|
|
39
|
+
setError(null);
|
|
44
40
|
try {
|
|
45
41
|
await invokeAgent(text, { agentId });
|
|
46
42
|
} catch (err) {
|
|
47
|
-
|
|
43
|
+
setError(err instanceof Error ? err.message : "Agent invoke failed");
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
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
|
+
|
|
51
55
|
return (
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
<span className=
|
|
56
|
-
|
|
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}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
57
63
|
|
|
58
|
-
<
|
|
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
|
+
)}
|
|
59
70
|
{messages.map((m) => {
|
|
60
|
-
const isSelf = m.userId === memberUserId
|
|
61
|
-
const isAgent = m.userId
|
|
71
|
+
const isSelf = m.userId === memberUserId;
|
|
72
|
+
const isAgent = String(m.userId ?? "").includes("agent") || m.userId === "assistant";
|
|
62
73
|
return (
|
|
63
|
-
<
|
|
74
|
+
<div
|
|
64
75
|
key={m.id ?? `${m.createdAt}-${m.userId}`}
|
|
65
|
-
className={`message${isSelf ? "
|
|
76
|
+
className={`message ${isSelf ? "self" : ""} ${isAgent ? "agent" : ""}`.trim()}
|
|
66
77
|
>
|
|
67
|
-
<span className="author">{m.userId}</span>
|
|
68
|
-
<
|
|
69
|
-
</
|
|
78
|
+
<span className="msg-author">{m.userId}</span>
|
|
79
|
+
<p className="msg-body">{m.content}</p>
|
|
80
|
+
</div>
|
|
70
81
|
);
|
|
71
82
|
})}
|
|
72
|
-
|
|
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>
|
|
73
94
|
|
|
74
|
-
{
|
|
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}`);
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
Ask agent
|
|
120
|
+
</button>
|
|
121
|
+
</form>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
75
125
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<li key={tc.id}>
|
|
87
|
-
{tc.name}: {tc.status ?? "done"}
|
|
88
|
-
</li>
|
|
89
|
-
))}
|
|
90
|
-
</ul>
|
|
91
|
-
</div>
|
|
92
|
-
) : null}
|
|
93
|
-
|
|
94
|
-
{invokeError ? <p className="hint" style={{ color: "#b91c1c", padding: "0 1rem" }}>{invokeError}</p> : null}
|
|
95
|
-
|
|
96
|
-
<form
|
|
97
|
-
className="composer"
|
|
98
|
-
onSubmit={(e) => {
|
|
99
|
-
e.preventDefault();
|
|
100
|
-
const text = draft.trim();
|
|
101
|
-
if (!text) return;
|
|
102
|
-
void handleSendMessage(text);
|
|
103
|
-
setDraft("");
|
|
104
|
-
}}
|
|
105
|
-
>
|
|
106
|
-
<div className="composer-row">
|
|
107
|
-
<input
|
|
108
|
-
value={draft}
|
|
109
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
110
|
-
placeholder="Message or ask @assistant…"
|
|
111
|
-
aria-label="Message"
|
|
112
|
-
/>
|
|
113
|
-
<button type="submit">Send</button>
|
|
114
|
-
<button
|
|
115
|
-
type="button"
|
|
116
|
-
className="secondary"
|
|
117
|
-
disabled={!draft.trim()}
|
|
118
|
-
onClick={() => {
|
|
119
|
-
const text = draft.trim();
|
|
120
|
-
if (!text) return;
|
|
121
|
-
const payload = text.startsWith("@") ? text : `${agentHandle} ${text}`;
|
|
122
|
-
void handleInvokeAgent(payload);
|
|
123
|
-
setDraft("");
|
|
124
|
-
}}
|
|
125
|
-
>
|
|
126
|
-
Ask agent
|
|
127
|
-
</button>
|
|
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>
|
|
128
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>
|
|
129
144
|
<p className="hint">
|
|
130
|
-
|
|
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
|
+
.
|
|
131
151
|
</p>
|
|
132
|
-
</
|
|
133
|
-
</
|
|
152
|
+
</div>
|
|
153
|
+
</main>
|
|
134
154
|
);
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
export function App() {
|
|
138
|
-
|
|
139
|
-
if (!workerUrl || !memberJwt) return null;
|
|
140
|
-
return new FluxyChatClient({
|
|
141
|
-
baseUrl: workerUrl,
|
|
142
|
-
userId: memberUserId,
|
|
143
|
-
token: memberJwt,
|
|
144
|
-
});
|
|
145
|
-
}, []);
|
|
146
|
-
|
|
147
|
-
if (!workerUrl || !memberJwt) {
|
|
148
|
-
return (
|
|
149
|
-
<main className="shell">
|
|
150
|
-
<div className="error-box">
|
|
151
|
-
<p>
|
|
152
|
-
<strong>Setup required.</strong> Run provisioning against a local FluxyChat worker:
|
|
153
|
-
</p>
|
|
154
|
-
<pre>
|
|
155
|
-
<code>{`# Terminal 1 — from FluxyChat monorepo
|
|
156
|
-
pnpm --filter @fluxy-chat/worker dev
|
|
157
|
-
|
|
158
|
-
# Terminal 2 — in this project
|
|
159
|
-
pnpm setup
|
|
160
|
-
pnpm dev`}</code>
|
|
161
|
-
</pre>
|
|
162
|
-
<p>
|
|
163
|
-
Or copy <code>.env.example</code> → <code>.env</code> with credentials from{" "}
|
|
164
|
-
<a href="https://fluxychat.com/onboarding" target="_blank" rel="noreferrer">
|
|
165
|
-
fluxychat.com/onboarding
|
|
166
|
-
</a>
|
|
167
|
-
.
|
|
168
|
-
</p>
|
|
169
|
-
</div>
|
|
170
|
-
</main>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!client) return null;
|
|
158
|
+
if (!workerUrl || !memberJwt) return <SetupRequired />;
|
|
175
159
|
|
|
176
160
|
return (
|
|
177
161
|
<main className="shell">
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
184
179
|
</a>
|
|
185
|
-
</
|
|
186
|
-
</
|
|
180
|
+
</nav>
|
|
181
|
+
</header>
|
|
187
182
|
|
|
188
183
|
<div className="layout">
|
|
189
|
-
<FluxyRealtimeProvider
|
|
184
|
+
<FluxyRealtimeProvider
|
|
185
|
+
workerUrl={workerUrl}
|
|
186
|
+
authTokenProvider={memberJwt}
|
|
187
|
+
userId={memberUserId}
|
|
188
|
+
>
|
|
190
189
|
<ChatRoom />
|
|
191
190
|
</FluxyRealtimeProvider>
|
|
192
191
|
|
|
193
|
-
<aside className="
|
|
194
|
-
<div className="card">
|
|
195
|
-
<
|
|
192
|
+
<aside className="sidebar">
|
|
193
|
+
<div className="sidebar-card">
|
|
194
|
+
<h3>Project</h3>
|
|
196
195
|
<dl>
|
|
197
|
-
<div>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
</div>
|
|
201
|
-
<div>
|
|
202
|
-
<dt>Room</dt>
|
|
203
|
-
<dd>{roomId}</dd>
|
|
204
|
-
</div>
|
|
205
|
-
<div>
|
|
206
|
-
<dt>Agent</dt>
|
|
207
|
-
<dd>{agentHandle}{agentId ? ` (${agentId.slice(0, 12)}…)` : ""}</dd>
|
|
208
|
-
</div>
|
|
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>
|
|
209
200
|
</dl>
|
|
210
201
|
</div>
|
|
211
|
-
<div className="card">
|
|
212
|
-
<
|
|
213
|
-
<ul
|
|
214
|
-
<li>Open a second tab
|
|
215
|
-
<li>Ask the agent about FluxyChat
|
|
216
|
-
<li>
|
|
202
|
+
<div className="sidebar-card">
|
|
203
|
+
<h3>Try it</h3>
|
|
204
|
+
<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>
|
|
217
216
|
</ul>
|
|
218
217
|
</div>
|
|
219
218
|
</aside>
|
|
@@ -1,242 +1,354 @@
|
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2
|
+
|
|
1
3
|
:root {
|
|
2
|
-
|
|
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%; }
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
font-family: var(--font);
|
|
22
|
+
background: var(--bg);
|
|
23
|
+
color: var(--text);
|
|
24
|
+
font-size: 14px;
|
|
3
25
|
line-height: 1.5;
|
|
4
|
-
|
|
5
|
-
background: #fdfbf9;
|
|
26
|
+
-webkit-font-smoothing: antialiased;
|
|
6
27
|
}
|
|
7
28
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
29
|
+
a { color: var(--accent); text-decoration: none; }
|
|
30
|
+
a:hover { text-decoration: underline; }
|
|
11
31
|
|
|
12
|
-
|
|
13
|
-
margin: 0;
|
|
14
|
-
}
|
|
32
|
+
/* ---- App shell ---- */
|
|
15
33
|
|
|
16
34
|
.shell {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
height: 100%;
|
|
20
38
|
}
|
|
21
39
|
|
|
22
|
-
.
|
|
23
|
-
|
|
40
|
+
.app-header {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
padding: 0 20px;
|
|
45
|
+
height: 52px;
|
|
46
|
+
border-bottom: 1px solid var(--border);
|
|
47
|
+
background: var(--surface);
|
|
48
|
+
flex-shrink: 0;
|
|
24
49
|
}
|
|
25
50
|
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
.app-logo {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
font-size: 15px;
|
|
29
57
|
}
|
|
30
58
|
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
.app-nav {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 16px;
|
|
35
63
|
}
|
|
36
64
|
|
|
37
|
-
.
|
|
38
|
-
|
|
65
|
+
.nav-item { color: var(--text-muted); font-size: 13px; }
|
|
66
|
+
|
|
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;
|
|
39
74
|
}
|
|
75
|
+
.nav-link:hover { background: rgba(99,102,241,0.1); text-decoration: none; }
|
|
76
|
+
|
|
77
|
+
/* ---- Layout ---- */
|
|
40
78
|
|
|
41
79
|
.layout {
|
|
42
|
-
display:
|
|
43
|
-
|
|
80
|
+
display: flex;
|
|
81
|
+
flex: 1;
|
|
82
|
+
min-height: 0;
|
|
83
|
+
overflow: hidden;
|
|
44
84
|
}
|
|
45
85
|
|
|
46
|
-
|
|
47
|
-
.layout {
|
|
48
|
-
grid-template-columns: 1fr 240px;
|
|
49
|
-
align-items: start;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
86
|
+
/* ---- Chat panel ---- */
|
|
52
87
|
|
|
53
88
|
.chat-panel {
|
|
89
|
+
flex: 1;
|
|
54
90
|
display: flex;
|
|
55
91
|
flex-direction: column;
|
|
56
|
-
min-
|
|
57
|
-
border: 1px solid
|
|
58
|
-
border-radius: 16px;
|
|
59
|
-
background: #fff;
|
|
60
|
-
overflow: hidden;
|
|
61
|
-
box-shadow: 0 8px 30px rgb(15 23 42 / 6%);
|
|
92
|
+
min-width: 0;
|
|
93
|
+
border-right: 1px solid var(--border);
|
|
62
94
|
}
|
|
63
95
|
|
|
64
96
|
.chat-header {
|
|
65
97
|
display: flex;
|
|
66
|
-
justify-content: space-between;
|
|
67
98
|
align-items: center;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
99
|
+
gap: 10px;
|
|
100
|
+
padding: 12px 20px;
|
|
101
|
+
border-bottom: 1px solid var(--border);
|
|
102
|
+
background: var(--surface);
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.room-name {
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
font-size: 15px;
|
|
72
109
|
}
|
|
73
110
|
|
|
74
|
-
.status {
|
|
75
|
-
font-size:
|
|
76
|
-
|
|
111
|
+
.status-badge {
|
|
112
|
+
font-size: 11px;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
padding: 2px 8px;
|
|
115
|
+
border-radius: 99px;
|
|
116
|
+
letter-spacing: 0.03em;
|
|
77
117
|
}
|
|
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); }
|
|
78
120
|
|
|
79
|
-
.messages {
|
|
121
|
+
.messages-list {
|
|
80
122
|
flex: 1;
|
|
81
123
|
overflow-y: auto;
|
|
82
|
-
|
|
83
|
-
margin: 0;
|
|
84
|
-
padding: 1rem;
|
|
124
|
+
padding: 20px;
|
|
85
125
|
display: flex;
|
|
86
126
|
flex-direction: column;
|
|
87
|
-
gap:
|
|
127
|
+
gap: 2px;
|
|
128
|
+
scroll-behavior: smooth;
|
|
88
129
|
}
|
|
89
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
|
+
|
|
90
140
|
.message {
|
|
91
141
|
display: flex;
|
|
92
142
|
flex-direction: column;
|
|
93
|
-
gap:
|
|
94
|
-
padding:
|
|
95
|
-
border-radius:
|
|
96
|
-
|
|
97
|
-
|
|
143
|
+
gap: 2px;
|
|
144
|
+
padding: 6px 10px;
|
|
145
|
+
border-radius: 8px;
|
|
146
|
+
max-width: 75%;
|
|
147
|
+
align-self: flex-start;
|
|
98
148
|
}
|
|
99
149
|
|
|
100
150
|
.message.self {
|
|
101
151
|
align-self: flex-end;
|
|
102
|
-
background:
|
|
103
|
-
border: 1px solid #fed7aa;
|
|
152
|
+
background: rgba(99,102,241,0.12);
|
|
104
153
|
}
|
|
105
154
|
|
|
106
155
|
.message.agent {
|
|
107
|
-
background:
|
|
108
|
-
border:
|
|
156
|
+
background: rgba(16,185,129,0.08);
|
|
157
|
+
border-left: 2px solid var(--agent);
|
|
109
158
|
}
|
|
110
159
|
|
|
111
|
-
.author {
|
|
112
|
-
font-size:
|
|
113
|
-
color: #64748b;
|
|
160
|
+
.msg-author {
|
|
161
|
+
font-size: 11px;
|
|
114
162
|
font-weight: 600;
|
|
163
|
+
color: var(--text-muted);
|
|
164
|
+
text-transform: uppercase;
|
|
165
|
+
letter-spacing: 0.05em;
|
|
115
166
|
}
|
|
116
167
|
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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;
|
|
123
176
|
}
|
|
124
177
|
|
|
125
|
-
|
|
178
|
+
/* ---- Typing indicator ---- */
|
|
179
|
+
|
|
180
|
+
.typing-indicator .msg-body {
|
|
126
181
|
display: flex;
|
|
127
|
-
gap:
|
|
182
|
+
gap: 4px;
|
|
183
|
+
align-items: center;
|
|
184
|
+
padding: 4px 0;
|
|
128
185
|
}
|
|
129
186
|
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
border-radius:
|
|
134
|
-
|
|
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;
|
|
135
193
|
}
|
|
194
|
+
.dot:nth-child(2) { animation-delay: 0.2s; }
|
|
195
|
+
.dot:nth-child(3) { animation-delay: 0.4s; }
|
|
136
196
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
background: #c2410c;
|
|
141
|
-
color: #fff;
|
|
142
|
-
padding: 0.55rem 1rem;
|
|
143
|
-
cursor: pointer;
|
|
144
|
-
font-weight: 600;
|
|
197
|
+
@keyframes bounce {
|
|
198
|
+
0%, 80%, 100% { transform: translateY(0); opacity: 0.5; }
|
|
199
|
+
40% { transform: translateY(-4px); opacity: 1; }
|
|
145
200
|
}
|
|
146
201
|
|
|
147
|
-
|
|
148
|
-
background: #0f172a;
|
|
149
|
-
}
|
|
202
|
+
/* ---- Composer ---- */
|
|
150
203
|
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
204
|
+
.chat-error {
|
|
205
|
+
margin: 0 20px 8px;
|
|
206
|
+
color: var(--danger);
|
|
207
|
+
font-size: 13px;
|
|
154
208
|
}
|
|
155
209
|
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
210
|
+
.composer {
|
|
211
|
+
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;
|
|
159
218
|
}
|
|
160
219
|
|
|
161
|
-
.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
border
|
|
165
|
-
border:
|
|
166
|
-
|
|
220
|
+
.composer input {
|
|
221
|
+
flex: 1;
|
|
222
|
+
background: var(--surface-2);
|
|
223
|
+
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);
|
|
167
245
|
}
|
|
246
|
+
.composer button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
168
247
|
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
letter-spacing: 0.06em;
|
|
173
|
-
text-transform: uppercase;
|
|
174
|
-
color: #64748b;
|
|
248
|
+
.composer button[type="submit"] {
|
|
249
|
+
background: var(--accent);
|
|
250
|
+
color: #fff;
|
|
175
251
|
}
|
|
252
|
+
.composer button[type="submit"]:hover:not(:disabled) { background: var(--accent-hover); }
|
|
176
253
|
|
|
177
|
-
.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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);
|
|
181
258
|
}
|
|
259
|
+
.composer .btn-agent:hover:not(:disabled) { background: rgba(16,185,129,0.2); }
|
|
260
|
+
|
|
261
|
+
/* ---- Sidebar ---- */
|
|
182
262
|
|
|
183
|
-
.
|
|
263
|
+
.sidebar {
|
|
264
|
+
width: 280px;
|
|
265
|
+
flex-shrink: 0;
|
|
266
|
+
overflow-y: auto;
|
|
267
|
+
padding: 20px 16px;
|
|
184
268
|
display: flex;
|
|
185
269
|
flex-direction: column;
|
|
186
|
-
gap:
|
|
270
|
+
gap: 12px;
|
|
271
|
+
background: var(--surface);
|
|
187
272
|
}
|
|
188
273
|
|
|
189
|
-
.card {
|
|
190
|
-
|
|
191
|
-
border
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
font-size: 0.8rem;
|
|
274
|
+
.sidebar-card {
|
|
275
|
+
background: var(--surface-2);
|
|
276
|
+
border: 1px solid var(--border);
|
|
277
|
+
border-radius: var(--radius);
|
|
278
|
+
padding: 14px 16px;
|
|
195
279
|
}
|
|
196
280
|
|
|
197
|
-
.card
|
|
198
|
-
|
|
199
|
-
font-
|
|
281
|
+
.sidebar-card h3 {
|
|
282
|
+
font-size: 11px;
|
|
283
|
+
font-weight: 600;
|
|
200
284
|
text-transform: uppercase;
|
|
201
|
-
letter-spacing: 0.
|
|
202
|
-
color:
|
|
285
|
+
letter-spacing: 0.07em;
|
|
286
|
+
color: var(--text-muted);
|
|
287
|
+
margin-bottom: 10px;
|
|
203
288
|
}
|
|
204
289
|
|
|
205
|
-
.card dl {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 {
|
|
294
|
+
font-size: 13px;
|
|
295
|
+
color: var(--text);
|
|
296
|
+
overflow: hidden;
|
|
297
|
+
text-overflow: ellipsis;
|
|
298
|
+
white-space: nowrap;
|
|
209
299
|
}
|
|
210
300
|
|
|
211
|
-
.card
|
|
212
|
-
|
|
213
|
-
|
|
301
|
+
.sidebar-card ul {
|
|
302
|
+
list-style: none;
|
|
303
|
+
display: flex;
|
|
304
|
+
flex-direction: column;
|
|
305
|
+
gap: 8px;
|
|
214
306
|
}
|
|
307
|
+
.sidebar-card li { font-size: 13px; color: var(--text-muted); }
|
|
308
|
+
.sidebar-card li a { color: var(--accent); }
|
|
215
309
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
310
|
+
/* ---- Setup required ---- */
|
|
311
|
+
|
|
312
|
+
.setup-card {
|
|
313
|
+
margin: auto;
|
|
314
|
+
background: var(--surface);
|
|
315
|
+
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;
|
|
219
323
|
}
|
|
220
324
|
|
|
221
|
-
.card
|
|
222
|
-
|
|
325
|
+
.setup-card .logo {
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
gap: 8px;
|
|
329
|
+
font-weight: 700;
|
|
330
|
+
font-size: 16px;
|
|
223
331
|
}
|
|
224
332
|
|
|
225
|
-
.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
color: #991b1b;
|
|
229
|
-
border-radius: 12px;
|
|
230
|
-
padding: 1rem;
|
|
231
|
-
font-size: 0.875rem;
|
|
333
|
+
.setup-card h1 {
|
|
334
|
+
font-size: 22px;
|
|
335
|
+
font-weight: 700;
|
|
232
336
|
}
|
|
233
337
|
|
|
234
|
-
.
|
|
235
|
-
|
|
338
|
+
.setup-card p { color: var(--text-muted); font-size: 14px; }
|
|
339
|
+
.setup-card .hint { font-size: 13px; }
|
|
340
|
+
|
|
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;
|
|
236
347
|
}
|
|
237
348
|
|
|
238
|
-
.
|
|
239
|
-
|
|
240
|
-
font-size:
|
|
241
|
-
color:
|
|
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;
|
|
242
354
|
}
|