@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 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluxy-chat/create-fluxy-chat",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Scaffold a new FluxyChat bot project with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,8 @@
17
17
  "@fluxy-chat/react": "^0.1.1",
18
18
  "@fluxy-chat/sdk": "^0.6.2",
19
19
  "react": "^19.0.0",
20
- "react-dom": "^19.0.0"
20
+ "react-dom": "^19.0.0",
21
+ "zustand": "^5.0.14"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/react": "^19.0.0",
@@ -1,219 +1,218 @@
1
- import { useMemo, useState } from "react";
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() || "http://localhost:3000";
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 [invokeError, setInvokeError] = useState<string | null>(null);
21
+ const [error, setError] = useState<string | null>(null);
22
+ const isConnected = connectionState.status === "connected";
32
23
 
33
- async function handleSendMessage(text: string) {
34
- setInvokeError(null);
35
- await sendMessage(text);
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 handleInvokeAgent(text: string) {
34
+ async function handleAskAgent(text: string) {
39
35
  if (!agentId) {
40
- setInvokeError("Set VITE_FLUXYCHAT_AGENT_ID in .env (run pnpm setup).");
36
+ setError("No agent configured. Run pnpm setup again.");
41
37
  return;
42
38
  }
43
- setInvokeError(null);
39
+ setError(null);
44
40
  try {
45
41
  await invokeAgent(text, { agentId });
46
42
  } catch (err) {
47
- setInvokeError(err instanceof Error ? err.message : "Agent invoke failed");
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
- <section className="chat-panel">
53
- <header className="chat-header">
54
- <strong>{roomId}</strong>
55
- <span className="status">{connectionState.status}</span>
56
- </header>
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
- <ul className="messages">
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 || m.userId === "first-message-user";
61
- const isAgent = m.userId?.includes("agent") || m.userId === "assistant";
71
+ const isSelf = m.userId === memberUserId;
72
+ const isAgent = String(m.userId ?? "").includes("agent") || m.userId === "assistant";
62
73
  return (
63
- <li
74
+ <div
64
75
  key={m.id ?? `${m.createdAt}-${m.userId}`}
65
- className={`message${isSelf ? " self" : ""}${isAgent ? " agent" : ""}`}
76
+ className={`message ${isSelf ? "self" : ""} ${isAgent ? "agent" : ""}`.trim()}
66
77
  >
67
- <span className="author">{m.userId}</span>
68
- <span>{m.content}</span>
69
- </li>
78
+ <span className="msg-author">{m.userId}</span>
79
+ <p className="msg-body">{m.content}</p>
80
+ </div>
70
81
  );
71
82
  })}
72
- </ul>
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
- {agentTyping ? <p className="typing">{agentHandle} is thinking…</p> : null}
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
- {(toolThreadEvents.length > 0 || lastAgentRun?.toolCalls?.length) ? (
77
- <div className="tools">
78
- <h3>Agent tools</h3>
79
- <ul>
80
- {toolThreadEvents.map((ev) => (
81
- <li key={ev.key}>
82
- {String(ev.kind ?? "tool")}: {String(ev.title ?? ev.toolName ?? ev.key)}
83
- </li>
84
- ))}
85
- {(lastAgentRun?.toolCalls ?? []).map((tc) => (
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
- Realtime via WebSocket · Agent replies stream in-room · Tool calls appear above
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
- </form>
133
- </section>
152
+ </div>
153
+ </main>
134
154
  );
135
155
  }
136
156
 
137
157
  export function App() {
138
- const client = useMemo(() => {
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
- <div className="hero">
179
- <h1>FluxyChat — full stack starter</h1>
180
- <p>
181
- Your app · Worker {workerUrl} ·{" "}
182
- <a href={`${consoleUrl.replace(/\/$/, "")}/onboarding?from=cli`} target="_blank" rel="noreferrer">
183
- Keep this project
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
- </p>
186
- </div>
180
+ </nav>
181
+ </header>
187
182
 
188
183
  <div className="layout">
189
- <FluxyRealtimeProvider client={client}>
184
+ <FluxyRealtimeProvider
185
+ workerUrl={workerUrl}
186
+ authTokenProvider={memberJwt}
187
+ userId={memberUserId}
188
+ >
190
189
  <ChatRoom />
191
190
  </FluxyRealtimeProvider>
192
191
 
193
- <aside className="side">
194
- <div className="card">
195
- <h2>Project</h2>
192
+ <aside className="sidebar">
193
+ <div className="sidebar-card">
194
+ <h3>Project</h3>
196
195
  <dl>
197
- <div>
198
- <dt>Project ID</dt>
199
- <dd>{projectId || "—"}</dd>
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
- <h2>Try next</h2>
213
- <ul style={{ margin: 0, paddingLeft: "1.1rem" }}>
214
- <li>Open a second tab same URL — for realtime</li>
215
- <li>Ask the agent about FluxyChat architecture</li>
216
- <li>Manage rooms & agents in the console</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
- font-family: system-ui, -apple-system, sans-serif;
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
- color: #0f172a;
5
- background: #fdfbf9;
26
+ -webkit-font-smoothing: antialiased;
6
27
  }
7
28
 
8
- * {
9
- box-sizing: border-box;
10
- }
29
+ a { color: var(--accent); text-decoration: none; }
30
+ a:hover { text-decoration: underline; }
11
31
 
12
- body {
13
- margin: 0;
14
- }
32
+ /* ---- App shell ---- */
15
33
 
16
34
  .shell {
17
- max-width: 880px;
18
- margin: 0 auto;
19
- padding: 1.5rem;
35
+ display: flex;
36
+ flex-direction: column;
37
+ height: 100%;
20
38
  }
21
39
 
22
- .hero {
23
- margin-bottom: 1rem;
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
- .hero h1 {
27
- font-size: 1.35rem;
28
- margin: 0 0 0.35rem;
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
- .hero p {
32
- margin: 0;
33
- font-size: 0.875rem;
34
- color: #64748b;
59
+ .app-nav {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 16px;
35
63
  }
36
64
 
37
- .hero a {
38
- color: #c2410c;
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: grid;
43
- gap: 1rem;
80
+ display: flex;
81
+ flex: 1;
82
+ min-height: 0;
83
+ overflow: hidden;
44
84
  }
45
85
 
46
- @media (min-width: 900px) {
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-height: 520px;
57
- border: 1px solid #e7e5e4;
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
- padding: 0.75rem 1rem;
69
- border-bottom: 1px solid #e7e5e4;
70
- background: #0f172a;
71
- color: #fff;
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: 0.75rem;
76
- opacity: 0.85;
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
- list-style: none;
83
- margin: 0;
84
- padding: 1rem;
124
+ padding: 20px;
85
125
  display: flex;
86
126
  flex-direction: column;
87
- gap: 0.5rem;
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: 0.15rem;
94
- padding: 0.6rem 0.85rem;
95
- border-radius: 10px;
96
- background: #f8fafc;
97
- max-width: 85%;
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: #fff7ed;
103
- border: 1px solid #fed7aa;
152
+ background: rgba(99,102,241,0.12);
104
153
  }
105
154
 
106
155
  .message.agent {
107
- background: #f1f5f9;
108
- border: 1px solid #e2e8f0;
156
+ background: rgba(16,185,129,0.08);
157
+ border-left: 2px solid var(--agent);
109
158
  }
110
159
 
111
- .author {
112
- font-size: 0.7rem;
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
- .composer {
118
- display: flex;
119
- flex-direction: column;
120
- gap: 0.5rem;
121
- padding: 0.75rem;
122
- border-top: 1px solid #e7e5e4;
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
- .composer-row {
178
+ /* ---- Typing indicator ---- */
179
+
180
+ .typing-indicator .msg-body {
126
181
  display: flex;
127
- gap: 0.5rem;
182
+ gap: 4px;
183
+ align-items: center;
184
+ padding: 4px 0;
128
185
  }
129
186
 
130
- .composer input {
131
- flex: 1;
132
- border: 1px solid #cbd5e1;
133
- border-radius: 10px;
134
- padding: 0.55rem 0.75rem;
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
- .composer button {
138
- border: none;
139
- border-radius: 10px;
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
- .composer button.secondary {
148
- background: #0f172a;
149
- }
202
+ /* ---- Composer ---- */
150
203
 
151
- .composer button:disabled {
152
- opacity: 0.55;
153
- cursor: not-allowed;
204
+ .chat-error {
205
+ margin: 0 20px 8px;
206
+ color: var(--danger);
207
+ font-size: 13px;
154
208
  }
155
209
 
156
- .hint {
157
- font-size: 0.75rem;
158
- color: #64748b;
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
- .tools {
162
- margin: 0 1rem 1rem;
163
- padding: 0.75rem;
164
- border-radius: 10px;
165
- border: 1px solid #e2e8f0;
166
- background: #f8fafc;
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
- .tools h3 {
170
- margin: 0 0 0.5rem;
171
- font-size: 0.7rem;
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
- .tools ul {
178
- margin: 0;
179
- padding-left: 1rem;
180
- font-size: 0.8rem;
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
- .side {
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: 0.75rem;
270
+ gap: 12px;
271
+ background: var(--surface);
187
272
  }
188
273
 
189
- .card {
190
- border: 1px solid #e7e5e4;
191
- border-radius: 12px;
192
- padding: 0.85rem 1rem;
193
- background: #fff;
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 h2 {
198
- margin: 0 0 0.5rem;
199
- font-size: 0.75rem;
281
+ .sidebar-card h3 {
282
+ font-size: 11px;
283
+ font-weight: 600;
200
284
  text-transform: uppercase;
201
- letter-spacing: 0.05em;
202
- color: #64748b;
285
+ letter-spacing: 0.07em;
286
+ color: var(--text-muted);
287
+ margin-bottom: 10px;
203
288
  }
204
289
 
205
- .card dl {
206
- margin: 0;
207
- display: grid;
208
- gap: 0.35rem;
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 dt {
212
- color: #94a3b8;
213
- font-size: 0.7rem;
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
- .card dd {
217
- margin: 0;
218
- word-break: break-all;
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 a {
222
- color: #c2410c;
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
- .error-box {
226
- border: 1px solid #fecaca;
227
- background: #fef2f2;
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
- .error-box code {
235
- font-size: 0.8rem;
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
- .typing {
239
- padding: 0 1rem 0.5rem;
240
- font-size: 0.75rem;
241
- color: #64748b;
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
  }