Berkkirik commited on
Commit
178db06
Β·
1 Parent(s): a737f01

revert: restore Twilight Atelier (drop Risograph Folio)

Browse files
app/documents/page.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
 
3
- import { api } from "@/lib/api";
4
  import type { DocumentMeta, ReindexResponse } from "@/lib/types";
5
  import {
6
  useMutation,
@@ -54,70 +54,58 @@ export default function DocumentsPage() {
54
 
55
  return (
56
  <div className="flex-1 overflow-y-auto">
57
- <div className="container-app py-10 space-y-10">
58
- {/* Editorial masthead */}
59
- <header className="animate-fade-in">
60
- <div className="flex items-baseline justify-between mb-3">
61
- <span className="folio-chrome">Folio II Β· Library</span>
62
- <span className="folio-chrome">
63
- {data ? `${data.count.toLocaleString()} entries` : "Loading…"}
64
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
65
  </div>
66
- <h1
67
- className="text-ink"
68
- style={{
69
- fontSize: "clamp(56px, 9vw, 110px)",
70
- lineHeight: 0.92,
71
- fontWeight: 300,
72
- letterSpacing: "-0.04em",
73
- fontFamily: "var(--font-fraunces)",
74
- }}
75
- >
76
- The <span className="italic-display text-rust">corpus.</span>
77
- </h1>
78
- <div className="hairline--double mt-6 animate-rule" />
79
- <div className="grid md:grid-cols-[1fr_auto] gap-6 items-end mt-6 animate-stagger-2">
80
- <p
81
- className="text-ink max-w-[60ch]"
82
- style={{
83
- fontFamily: "var(--font-fraunces)",
84
- fontSize: "20px",
85
- lineHeight: 1.45,
86
- fontWeight: 300,
87
- }}
88
  >
89
- The standing collection of Etiya BSS documents, persisted at{" "}
90
- <span className="font-mono text-ink-70">/data/docs/</span>. Use
91
- the search to query the catalog, or contribute a new entry.
92
- </p>
93
- <div className="flex items-center gap-3">
94
- <button
95
- onClick={() => reindex.mutate()}
96
- disabled={reindex.isPending}
97
- className="btn-secondary"
98
- >
99
- {reindex.isPending ? "Re-indexing…" : "Re-index"}
100
- </button>
101
- <button
102
- onClick={() => setShowAdd((v) => !v)}
103
- className="btn-rust"
104
- >
105
- {showAdd ? "Close" : "Add entry"}
106
- </button>
107
- </div>
108
  </div>
109
  </header>
110
 
111
  {/* Reindex banner */}
112
  {reindexResult && (
113
  <div
114
- className="card-rust flex items-center justify-between gap-3 animate-fade-up"
 
 
 
 
115
  >
116
- <div>
117
- <span className="folio-chrome--rust folio-chrome mr-3">
118
- Re-index complete
119
  </span>
120
- <span className="font-mono text-caption text-ink-70">
121
  mode={reindexResult.mode ?? "none"} Β· added=
122
  {reindexResult.added} Β· removed={reindexResult.removed} Β·
123
  indexed={reindexResult.indexed_count} Β·{" "}
@@ -126,67 +114,62 @@ export default function DocumentsPage() {
126
  </div>
127
  <button
128
  onClick={() => setReindexResult(null)}
129
- className="folio-chrome hover:text-rust"
130
  >
131
- Dismiss
132
  </button>
133
  </div>
134
  )}
135
 
136
- {/* Add entry form */}
137
  {showAdd && (
138
  <AddDocumentForm
139
- onCreated={() => qc.invalidateQueries({ queryKey: ["documents"] })}
 
 
140
  />
141
  )}
142
 
143
- {/* Search */}
144
- <section className="animate-fade-up-slow">
145
- <div className="flex items-baseline justify-between mb-3">
146
- <span className="folio-chrome">Index</span>
147
- <span className="folio-chrome">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  {filtered.length.toLocaleString()} match
149
  {filtered.length === 1 ? "" : "es"}
150
  </span>
151
  </div>
152
 
153
- <div className="relative max-w-[640px] mb-6">
154
- <svg
155
- className="absolute left-4 top-1/2 -translate-y-1/2 text-ink-50"
156
- width="14"
157
- height="14"
158
- viewBox="0 0 14 14"
159
- fill="none"
160
- >
161
- <circle cx="6" cy="6" r="4" stroke="currentColor" strokeWidth="1.4" />
162
- <path
163
- d="m9.5 9.5 3 3"
164
- stroke="currentColor"
165
- strokeWidth="1.4"
166
- strokeLinecap="round"
167
- />
168
- </svg>
169
- <input
170
- type="text"
171
- placeholder="Query the catalog…"
172
- value={filter}
173
- onChange={(e) => {
174
- setFilter(e.target.value);
175
- setPage(0);
176
- }}
177
- className="input-print !pl-11"
178
  style={{
179
- fontFamily: "var(--font-fraunces)",
180
- fontSize: "17px",
181
- fontWeight: 400,
182
  }}
183
- />
184
- </div>
185
-
186
- {error && (
187
- <div className="card-rust mb-4">
188
- <div className="folio-chrome--rust folio-chrome mb-1">Errata</div>
189
- <p className="text-body-strong text-ink">
190
  Failed to load documents
191
  </p>
192
  <p className="text-caption text-ink-70 mt-1">
@@ -196,59 +179,54 @@ export default function DocumentsPage() {
196
  )}
197
 
198
  {isLoading && (
199
- <div className="text-ink-50 italic-display py-12 text-center" style={{ fontSize: "22px" }}>
200
- Loading the catalog…
201
  </div>
202
  )}
203
 
204
  {visible.length > 0 && (
205
- <ul className="border-t-2 border-ink/30">
206
- {visible.map((d, i) => {
207
- const folio = String(page * PAGE_SIZE + i + 1).padStart(4, "0");
208
- return (
209
- <li
210
- key={d.doc_id}
211
- className="grid grid-cols-[80px_1fr_auto] gap-4 sm:gap-6 py-4 border-b border-ink/12 group"
212
- >
213
- <div className="folio-chrome--ink folio-chrome pt-0.5">
214
- β„– {folio}
215
  </div>
216
- <div className="min-w-0">
217
- <div
218
- className="italic-display text-ink truncate group-hover:text-rust transition-colors"
219
- style={{ fontSize: "19px", lineHeight: 1.3 }}
220
- >
221
- {d.name}
222
- </div>
223
- <div className="folio-chrome mt-1 truncate">
224
- {d.doc_id} Β· {d.length_chars.toLocaleString()} chars Β·{" "}
225
  {new Date(d.created_at * 1000).toLocaleDateString()}
226
- </div>
227
  </div>
228
- <button
229
- onClick={() => setConfirmDelete(d)}
230
- className="opacity-0 group-hover:opacity-100 transition-opacity folio-chrome hover:text-rust"
231
- >
232
- Withdraw
233
- </button>
234
- </li>
235
- );
236
- })}
237
  </ul>
238
  )}
239
 
240
  {totalPages > 1 && (
241
- <div className="flex items-center justify-between mt-6">
242
  <button
243
  onClick={() => setPage((p) => Math.max(0, p - 1))}
244
  disabled={page === 0}
245
  className="btn-ghost disabled:opacity-30"
246
  >
247
- ← Previous
248
  </button>
249
- <span className="folio-chrome">
250
- Folio {String(page + 1).padStart(2, "0")} Β·{" "}
251
- {String(totalPages).padStart(2, "0")}
252
  </span>
253
  <button
254
  onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
@@ -278,7 +256,10 @@ function AddDocumentForm({ onCreated }: { onCreated: () => void }) {
278
  const [name, setName] = useState("");
279
  const [text, setText] = useState("");
280
  const [doReindex, setDoReindex] = useState(true);
281
- const [feedback, setFeedback] = useState<{ kind: "ok" | "err"; msg: string } | null>(null);
 
 
 
282
 
283
  const create = useMutation({
284
  mutationFn: async (payload: { text: string; name?: string }) => {
@@ -289,7 +270,7 @@ function AddDocumentForm({ onCreated }: { onCreated: () => void }) {
289
  onSuccess: (created) => {
290
  setFeedback({
291
  kind: "ok",
292
- msg: `Filed: ${created.name} (${created.doc_id.slice(0, 12)}…)`,
293
  });
294
  setText("");
295
  setName("");
@@ -303,23 +284,11 @@ function AddDocumentForm({ onCreated }: { onCreated: () => void }) {
303
 
304
  return (
305
  <section className="card animate-fade-up">
306
- <div className="flex items-baseline justify-between mb-2">
307
- <span className="folio-chrome">Submission Β· New entry</span>
308
- </div>
309
- <h2
310
- className="text-ink"
311
- style={{
312
- fontFamily: "var(--font-fraunces)",
313
- fontSize: "32px",
314
- fontWeight: 500,
315
- letterSpacing: "-0.018em",
316
- }}
317
- >
318
- File a new <span className="italic-display text-rust">entry.</span>
319
  </h2>
320
- <p className="text-caption text-ink-70 mt-1.5 max-w-[60ch]">
321
- Documents persist immediately; re-indexing syncs them into the
322
- retrieval atelier.
323
  </p>
324
 
325
  <form
@@ -328,25 +297,25 @@ function AddDocumentForm({ onCreated }: { onCreated: () => void }) {
328
  if (!text.trim()) return;
329
  create.mutate({ text: text.trim(), name: name.trim() || undefined });
330
  }}
331
- className="mt-6 space-y-4"
332
  >
333
  <div>
334
- <label className="folio-chrome--ink folio-chrome block mb-1.5">
335
- Title <span className="text-ink-50 normal-case tracking-normal">(optional)</span>
336
  </label>
337
  <input
338
  type="text"
339
  value={name}
340
  onChange={(e) => setName(e.target.value)}
341
  placeholder="e.g. Customer Onboarding Flow v2"
342
- className="input-print"
343
  disabled={create.isPending}
344
  />
345
  </div>
346
 
347
  <div>
348
- <label className="folio-chrome--ink folio-chrome block mb-1.5">
349
- Body
350
  </label>
351
  <textarea
352
  value={text}
@@ -354,41 +323,40 @@ function AddDocumentForm({ onCreated }: { onCreated: () => void }) {
354
  required
355
  rows={8}
356
  placeholder="Paste markdown or plain text…"
357
- className="input-print resize-y"
358
  disabled={create.isPending}
359
- style={{ fontFamily: "var(--font-mono)", fontSize: "13px" }}
360
  />
361
- <p className="folio-chrome mt-1">
362
  {text.length.toLocaleString()} chars
363
  </p>
364
  </div>
365
 
366
- <label className="flex items-center gap-2.5 cursor-pointer">
367
  <input
368
  type="checkbox"
369
  checked={doReindex}
370
  onChange={(e) => setDoReindex(e.target.checked)}
371
- className="w-4 h-4 accent-rust"
372
  />
373
- <span className="text-caption text-ink">
374
- Auto re-index after filing
 
375
  </span>
376
- <span className="folio-chrome">~350ms</span>
377
  </label>
378
 
379
  <div className="flex items-center gap-4 pt-2">
380
  <button
381
  type="submit"
382
  disabled={create.isPending || !text.trim()}
383
- className="btn-rust"
384
  >
385
- {create.isPending ? "Filing…" : "File entry"}
386
  </button>
387
  {feedback && (
388
  <span
389
  className={clsx(
390
  "text-caption",
391
- feedback.kind === "ok" ? "text-status-ok" : "text-rust"
392
  )}
393
  >
394
  {feedback.msg}
@@ -413,29 +381,23 @@ function ConfirmDelete({
413
  }) {
414
  return (
415
  <div
416
- className="fixed inset-0 z-50 flex items-center justify-center p-6 animate-fade-in"
417
- style={{ background: "rgba(25,23,19,0.32)" }}
418
  onClick={onCancel}
419
  >
420
  <div
421
  onClick={(e) => e.stopPropagation()}
422
- className="card max-w-[460px] w-full animate-fade-up"
423
- style={{ background: "var(--paper)" }}
424
  >
425
- <div className="folio-chrome--rust folio-chrome mb-2">
426
- Permanent withdrawal
 
 
 
 
 
427
  </div>
428
- <h3
429
- className="italic-display text-ink"
430
- style={{ fontSize: "28px", lineHeight: 1.2 }}
431
- >
432
- Withdraw entry?
433
- </h3>
434
- <div className="hairline mt-3 mb-3" />
435
- <p className="text-body-strong text-ink truncate">{doc.name}</p>
436
- <p className="folio-chrome mt-1 truncate">{doc.doc_id}</p>
437
- <p className="text-caption text-ink-70 mt-4">
438
- Re-indexing will remove it from retrieval.
439
  </p>
440
  <div className="mt-6 flex items-center justify-end gap-3">
441
  <button onClick={onCancel} className="btn-secondary">
@@ -444,12 +406,16 @@ function ConfirmDelete({
444
  <button
445
  onClick={onConfirm}
446
  disabled={loading}
447
- className="btn-rust"
448
  >
449
- {loading ? "Withdrawing…" : "Withdraw"}
450
  </button>
451
  </div>
452
  </div>
453
  </div>
454
  );
455
  }
 
 
 
 
 
1
  "use client";
2
 
3
+ import { api, ApiError } from "@/lib/api";
4
  import type { DocumentMeta, ReindexResponse } from "@/lib/types";
5
  import {
6
  useMutation,
 
54
 
55
  return (
56
  <div className="flex-1 overflow-y-auto">
57
+ <div className="container-app py-10 space-y-8">
58
+ {/* Hero strip */}
59
+ <header className="flex items-end justify-between flex-wrap gap-4 animate-fade-in">
60
+ <div>
61
+ <h1 className="text-display-lg text-ink">
62
+ Documents <span className="serif-italic text-amber">corpus.</span>
63
+ </h1>
64
+ <p className="text-body text-ink-50 mt-2">
65
+ {data ? (
66
+ <>
67
+ <span className="font-mono text-ink">
68
+ {data.count.toLocaleString()}
69
+ </span>{" "}
70
+ indexed Β· persistent at{" "}
71
+ <span className="font-mono">/data/docs/</span>
72
+ </>
73
+ ) : (
74
+ "Loading…"
75
+ )}
76
+ </p>
77
  </div>
78
+ <div className="flex items-center gap-3">
79
+ <button
80
+ onClick={() => reindex.mutate()}
81
+ disabled={reindex.isPending}
82
+ className="btn-secondary"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  >
84
+ {reindex.isPending ? "Re-indexing…" : "Re-index now"}
85
+ </button>
86
+ <button
87
+ onClick={() => setShowAdd((v) => !v)}
88
+ className="btn-primary"
89
+ >
90
+ {showAdd ? "Close" : "Add document"}
91
+ </button>
 
 
 
 
 
 
 
 
 
 
 
92
  </div>
93
  </header>
94
 
95
  {/* Reindex banner */}
96
  {reindexResult && (
97
  <div
98
+ className="rounded-md p-4 flex items-center justify-between gap-3 animate-fade-up"
99
+ style={{
100
+ background: "rgba(93, 214, 168, 0.08)",
101
+ boxShadow: "0 0 0 1px rgba(93, 214, 168, 0.28)",
102
+ }}
103
  >
104
+ <div className="text-caption">
105
+ <span className="text-status-ok font-mono uppercase tracking-[0.12em] mr-3">
106
+ re-index ok
107
  </span>
108
+ <span className="font-mono text-ink-70">
109
  mode={reindexResult.mode ?? "none"} Β· added=
110
  {reindexResult.added} Β· removed={reindexResult.removed} Β·
111
  indexed={reindexResult.indexed_count} Β·{" "}
 
114
  </div>
115
  <button
116
  onClick={() => setReindexResult(null)}
117
+ className="text-caption text-ink-50 hover:text-ink"
118
  >
119
+ dismiss
120
  </button>
121
  </div>
122
  )}
123
 
124
+ {/* Add document panel */}
125
  {showAdd && (
126
  <AddDocumentForm
127
+ onCreated={() => {
128
+ qc.invalidateQueries({ queryKey: ["documents"] });
129
+ }}
130
  />
131
  )}
132
 
133
+ {/* Search + list */}
134
+ <section className="card animate-fade-up-slow">
135
+ <div className="flex items-center justify-between gap-4 mb-5 flex-wrap">
136
+ <div className="relative flex-1 min-w-[260px] max-w-[480px]">
137
+ <svg
138
+ className="absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-50"
139
+ width="14"
140
+ height="14"
141
+ viewBox="0 0 14 14"
142
+ fill="none"
143
+ >
144
+ <circle cx="6" cy="6" r="4" stroke="currentColor" strokeWidth="1.4" />
145
+ <path d="m9.5 9.5 3 3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
146
+ </svg>
147
+ <input
148
+ type="text"
149
+ placeholder="Search by name or doc_id…"
150
+ value={filter}
151
+ onChange={(e) => {
152
+ setFilter(e.target.value);
153
+ setPage(0);
154
+ }}
155
+ className="input-glass !pl-10"
156
+ />
157
+ </div>
158
+ <span className="text-caption text-ink-50 font-mono shrink-0 uppercase tracking-[0.12em]">
159
  {filtered.length.toLocaleString()} match
160
  {filtered.length === 1 ? "" : "es"}
161
  </span>
162
  </div>
163
 
164
+ {error && (
165
+ <div
166
+ className="p-4 rounded-md mb-4"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  style={{
168
+ background: "rgba(255, 122, 122, 0.06)",
169
+ boxShadow: "0 0 0 1px rgba(255, 122, 122, 0.28)",
 
170
  }}
171
+ >
172
+ <p className="text-status-err text-body-strong">
 
 
 
 
 
173
  Failed to load documents
174
  </p>
175
  <p className="text-caption text-ink-70 mt-1">
 
179
  )}
180
 
181
  {isLoading && (
182
+ <div className="text-ink-50 text-body py-12 text-center text-shimmer">
183
+ Loading documents…
184
  </div>
185
  )}
186
 
187
  {visible.length > 0 && (
188
+ <ul className="divide-y divide-glass-border">
189
+ {visible.map((d) => (
190
+ <li
191
+ key={d.doc_id}
192
+ className="flex items-center justify-between gap-4 py-3.5 group"
193
+ >
194
+ <div className="min-w-0 flex-1">
195
+ <div className="text-body-strong text-ink truncate">
196
+ {d.name}
 
197
  </div>
198
+ <div className="flex items-center gap-2.5 mt-0.5 text-micro text-ink-50 font-mono uppercase tracking-[0.10em]">
199
+ <span className="truncate">{d.doc_id}</span>
200
+ <Sep />
201
+ <span>{d.length_chars.toLocaleString()} chars</span>
202
+ <Sep />
203
+ <span>
 
 
 
204
  {new Date(d.created_at * 1000).toLocaleDateString()}
205
+ </span>
206
  </div>
207
+ </div>
208
+ <button
209
+ onClick={() => setConfirmDelete(d)}
210
+ className="opacity-0 group-hover:opacity-100 transition-opacity text-caption text-status-err hover:bg-status-err-glow px-2.5 py-1 rounded-md"
211
+ >
212
+ Delete
213
+ </button>
214
+ </li>
215
+ ))}
216
  </ul>
217
  )}
218
 
219
  {totalPages > 1 && (
220
+ <div className="flex items-center justify-between mt-5">
221
  <button
222
  onClick={() => setPage((p) => Math.max(0, p - 1))}
223
  disabled={page === 0}
224
  className="btn-ghost disabled:opacity-30"
225
  >
226
+ ← Prev
227
  </button>
228
+ <span className="text-caption text-ink-50 font-mono uppercase tracking-[0.12em]">
229
+ page {page + 1} of {totalPages}
 
230
  </span>
231
  <button
232
  onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
 
256
  const [name, setName] = useState("");
257
  const [text, setText] = useState("");
258
  const [doReindex, setDoReindex] = useState(true);
259
+ const [feedback, setFeedback] = useState<{
260
+ kind: "ok" | "err";
261
+ msg: string;
262
+ } | null>(null);
263
 
264
  const create = useMutation({
265
  mutationFn: async (payload: { text: string; name?: string }) => {
 
270
  onSuccess: (created) => {
271
  setFeedback({
272
  kind: "ok",
273
+ msg: `Added: ${created.name} (${created.doc_id.slice(0, 12)}…)`,
274
  });
275
  setText("");
276
  setName("");
 
284
 
285
  return (
286
  <section className="card animate-fade-up">
287
+ <h2 className="text-display-sm text-ink">
288
+ Add <span className="serif-italic text-amber">document</span>
 
 
 
 
 
 
 
 
 
 
 
289
  </h2>
290
+ <p className="text-caption text-ink-50 mt-1">
291
+ Documents persist immediately; re-indexing syncs them into RAG retrieval.
 
292
  </p>
293
 
294
  <form
 
297
  if (!text.trim()) return;
298
  create.mutate({ text: text.trim(), name: name.trim() || undefined });
299
  }}
300
+ className="mt-5 space-y-4"
301
  >
302
  <div>
303
+ <label className="text-caption-strong text-ink block mb-1.5">
304
+ Name <span className="text-ink-50 font-normal">(optional)</span>
305
  </label>
306
  <input
307
  type="text"
308
  value={name}
309
  onChange={(e) => setName(e.target.value)}
310
  placeholder="e.g. Customer Onboarding Flow v2"
311
+ className="input-glass"
312
  disabled={create.isPending}
313
  />
314
  </div>
315
 
316
  <div>
317
+ <label className="text-caption-strong text-ink block mb-1.5">
318
+ Content
319
  </label>
320
  <textarea
321
  value={text}
 
323
  required
324
  rows={8}
325
  placeholder="Paste markdown or plain text…"
326
+ className="input-glass resize-y"
327
  disabled={create.isPending}
 
328
  />
329
+ <p className="text-micro text-ink-50 mt-1 font-mono uppercase tracking-[0.10em]">
330
  {text.length.toLocaleString()} chars
331
  </p>
332
  </div>
333
 
334
+ <label className="flex items-center gap-2.5 text-caption text-ink cursor-pointer">
335
  <input
336
  type="checkbox"
337
  checked={doReindex}
338
  onChange={(e) => setDoReindex(e.target.checked)}
339
+ className="w-4 h-4 accent-amber"
340
  />
341
+ <span>Auto re-index after add</span>
342
+ <span className="text-ink-50 text-micro font-mono">
343
+ (~350ms)
344
  </span>
 
345
  </label>
346
 
347
  <div className="flex items-center gap-4 pt-2">
348
  <button
349
  type="submit"
350
  disabled={create.isPending || !text.trim()}
351
+ className="btn-primary"
352
  >
353
+ {create.isPending ? "Saving…" : "Save document"}
354
  </button>
355
  {feedback && (
356
  <span
357
  className={clsx(
358
  "text-caption",
359
+ feedback.kind === "ok" ? "text-status-ok" : "text-status-err"
360
  )}
361
  >
362
  {feedback.msg}
 
381
  }) {
382
  return (
383
  <div
384
+ className="fixed inset-0 z-50 bg-canvas-deep/70 backdrop-blur-sm flex items-center justify-center p-6 animate-fade-in"
 
385
  onClick={onCancel}
386
  >
387
  <div
388
  onClick={(e) => e.stopPropagation()}
389
+ className="glass-strong rounded-lg max-w-[460px] w-full p-7 animate-fade-up"
 
390
  >
391
+ <h3 className="text-display-sm">Delete document?</h3>
392
+ <p className="text-caption text-ink-50 mt-1">This is permanent.</p>
393
+ <div className="mt-4 p-3 rounded-md bg-glass">
394
+ <div className="text-body-strong text-ink truncate">{doc.name}</div>
395
+ <div className="text-micro text-ink-50 font-mono mt-0.5 truncate">
396
+ {doc.doc_id}
397
+ </div>
398
  </div>
399
+ <p className="text-caption text-ink-50 mt-4">
400
+ Re-indexing will remove it from /ask_smart retrieval.
 
 
 
 
 
 
 
 
 
401
  </p>
402
  <div className="mt-6 flex items-center justify-end gap-3">
403
  <button onClick={onCancel} className="btn-secondary">
 
406
  <button
407
  onClick={onConfirm}
408
  disabled={loading}
409
+ className="btn-danger"
410
  >
411
+ {loading ? "Deleting…" : "Delete"}
412
  </button>
413
  </div>
414
  </div>
415
  </div>
416
  );
417
  }
418
+
419
+ function Sep() {
420
+ return <span className="text-ink-30">Β·</span>;
421
+ }
app/globals.css CHANGED
@@ -3,176 +3,151 @@
3
  @tailwind utilities;
4
 
5
  /* ───────────────────────────────────────────────────────────────
6
- Risograph Folio β€” base
7
  ─────────────────────────────────────────────────────────────── */
8
  @layer base {
9
  :root {
10
- color-scheme: light;
11
- --paper: #f4ede0;
12
- --paper-deep: #ebe2d0;
13
- --ink: #191713;
14
- --rust: #c84d2c;
15
  }
16
 
17
  html,
18
  body {
19
- background: var(--paper);
20
- color: var(--ink);
21
- font-family: var(--font-plex), ui-sans-serif, system-ui, sans-serif;
22
  font-size: 15px;
23
- line-height: 1.65;
24
  font-weight: 400;
25
- font-feature-settings: "ss01", "ss03", "calt";
26
  -webkit-font-smoothing: antialiased;
27
  -moz-osx-font-smoothing: grayscale;
28
  text-rendering: optimizeLegibility;
29
  }
30
 
31
  body {
32
- /* Layered paper:
33
- 1. base parchment color
34
- 2. halftone dot pattern (very low opacity)
35
- 3. warm sun-bleached gradient at top-right
36
- 4. inked-edge vignette toward bottom-left
37
- 5. fine paper grain (svg noise) */
38
  background:
39
- radial-gradient(900px 600px at 92% 0%, rgba(200, 77, 44, 0.07), transparent 60%),
40
- radial-gradient(800px 700px at 0% 100%, rgba(25, 23, 19, 0.05), transparent 60%),
41
- var(--paper);
 
42
  background-attachment: fixed;
43
  min-height: 100vh;
44
  overflow-x: hidden;
45
- position: relative;
46
  }
47
 
48
- /* Halftone dot field (decorative; below all content) */
49
  body::before {
50
  content: "";
51
  position: fixed;
52
  inset: 0;
53
  pointer-events: none;
54
  z-index: 0;
55
- background-image: radial-gradient(
56
- circle at center,
57
- rgba(25, 23, 19, 0.08) 0.7px,
58
- transparent 1.4px
59
- );
60
- background-size: 6px 6px;
61
- background-position: 0 0;
62
- opacity: 0.55;
63
- mask-image: radial-gradient(
64
- ellipse 100% 70% at 50% 30%,
65
- transparent 0%,
66
- black 70%
67
- );
68
- -webkit-mask-image: radial-gradient(
69
- ellipse 100% 70% at 50% 30%,
70
- transparent 0%,
71
- black 70%
72
- );
73
- }
74
-
75
- /* Paper grain β€” very subtle */
76
- body::after {
77
- content: "";
78
- position: fixed;
79
- inset: 0;
80
- pointer-events: none;
81
- z-index: 0;
82
- background-image: url("data:image/svg+xml;utf8,<svg viewBox='0 0 240 240' xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch' seed='3'/><feColorMatrix values='0 0 0 0 0.10, 0 0 0 0 0.09, 0 0 0 0 0.08, 0 0 0 0.10 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
83
- opacity: 0.5;
84
- mix-blend-mode: multiply;
85
  }
86
 
87
  ::selection {
88
- background: var(--rust);
89
- color: var(--paper);
90
  }
91
 
92
- /* Hand-set scrollbar (warm, paper-tinted) */
93
  ::-webkit-scrollbar {
94
- width: 12px;
95
- height: 12px;
96
  }
97
  ::-webkit-scrollbar-thumb {
98
- background-color: rgba(25, 23, 19, 0.18);
99
  border-radius: 9999px;
100
- border: 3px solid var(--paper);
101
  background-clip: content-box;
102
  }
103
  ::-webkit-scrollbar-thumb:hover {
104
- background-color: rgba(25, 23, 19, 0.30);
105
  }
106
  ::-webkit-scrollbar-track {
107
  background: transparent;
108
  }
109
 
 
110
  :focus-visible {
111
- outline: 2px solid var(--rust);
 
 
 
 
 
 
 
112
  outline-offset: 3px;
113
  }
114
 
115
- /* Headlines β€” Fraunces by default for h1/h2/h3 */
116
  h1,
117
  h2,
118
  h3,
119
  h4 {
120
- font-family: var(--font-fraunces), ui-serif, Georgia, serif;
121
- font-weight: 500;
122
  letter-spacing: -0.02em;
123
- color: var(--ink);
124
- }
125
-
126
- /* Italic links: slip into italic on hover */
127
- a {
128
- transition: color 0.2s ease, font-style 0s;
129
  }
130
  }
131
 
132
  /* ───────────────────────────────────────────────────────────────
133
- Components
134
  ─────────────────────────────────────────────────────────────── */
135
  @layer components {
136
- /* ── Italic display moments ─────────────────────────────────── */
137
- .italic-display {
138
- font-family: var(--font-fraunces), ui-serif, Georgia, serif;
139
- font-style: italic;
140
- font-weight: 400;
141
- font-variation-settings: "SOFT" 30, "WONK" 1;
 
 
142
  }
143
 
144
- /* ── Buttons β€” flat printed ─────────────────────────────────── */
145
- .btn-primary {
146
- @apply inline-flex items-center justify-center
147
- px-5 py-2.5 rounded-pill
148
- text-body-strong text-paper
149
- bg-ink
150
- transition-all duration-200 ease-atelier
151
- active:scale-[0.97]
152
- disabled:opacity-30 disabled:cursor-not-allowed
153
- hover:bg-rust;
 
 
 
 
 
 
 
 
154
  }
155
 
156
- .btn-rust {
 
157
  @apply inline-flex items-center justify-center
158
  px-5 py-2.5 rounded-pill
159
- text-body-strong text-paper
160
- bg-rust
161
  transition-all duration-200 ease-atelier
162
  active:scale-[0.97]
163
- hover:bg-rust-deep
164
- disabled:opacity-30 disabled:cursor-not-allowed;
165
  }
166
 
167
  .btn-secondary {
168
  @apply inline-flex items-center justify-center
169
  px-5 py-2.5 rounded-pill
170
  text-body text-ink
171
- bg-transparent
172
  transition-all duration-200 ease-atelier
173
  active:scale-[0.97]
174
- hover:bg-ink-08;
175
- box-shadow: 0 0 0 1px rgba(25, 23, 19, 0.20);
176
  }
177
 
178
  .btn-ghost {
@@ -180,96 +155,50 @@
180
  px-3 py-1.5 rounded-md
181
  text-caption text-ink-70
182
  transition-colors duration-200
183
- hover:text-ink hover:bg-ink-08;
184
  }
185
 
186
- /* ── Inputs β€” flat printed input fields ────────────────────── */
187
- .input-print {
188
- @apply w-full bg-paper-deep text-ink
 
 
 
 
 
 
 
 
 
189
  rounded-md px-4 py-3
190
  text-body
191
  outline-none
192
  transition-all duration-200;
193
- box-shadow: 0 0 0 1px rgba(25, 23, 19, 0.18),
194
- inset 0 1px 0 rgba(25, 23, 19, 0.04);
195
  }
196
- .input-print:focus {
197
- box-shadow: 0 0 0 2px var(--rust),
198
- inset 0 1px 0 rgba(25, 23, 19, 0.04);
 
199
  }
200
- .input-print::placeholder {
201
- color: rgba(25, 23, 19, 0.36);
202
  }
203
 
204
- /* ── Cards β€” flat printed paper, hairline ruled ────────────── */
205
  .card {
206
- @apply rounded-md p-6;
207
- background: var(--paper-deep);
208
- box-shadow: 0 0 0 1px rgba(25, 23, 19, 0.12),
209
- inset 0 1px 0 rgba(255, 255, 255, 0.45);
210
  }
211
 
212
- .card-rust {
213
- @apply rounded-md p-6;
214
- background: linear-gradient(
215
- 180deg,
216
- rgba(245, 216, 200, 0.6) 0%,
217
- var(--paper-deep) 100%
218
- );
219
- box-shadow: 0 0 0 1px rgba(200, 77, 44, 0.35),
220
- inset 0 1px 0 rgba(255, 255, 255, 0.45);
221
- }
222
-
223
- /* ── Folio chrome (the magazine masthead vibe) ─────────────── */
224
- .folio-chrome {
225
- @apply font-mono uppercase tracking-[0.22em] text-ink-50;
226
- font-size: 10px;
227
- line-height: 1.4;
228
- font-weight: 600;
229
- }
230
- .folio-chrome--ink {
231
- @apply text-ink;
232
- }
233
- .folio-chrome--rust {
234
- @apply text-rust;
235
- }
236
-
237
- /* Hairline rule β€” animates in (use animate-rule) */
238
- .hairline {
239
- height: 1px;
240
- background: rgba(25, 23, 19, 0.16);
241
- }
242
- .hairline--rust {
243
- background: var(--rust);
244
- }
245
- .hairline--double {
246
- height: 4px;
247
- background: linear-gradient(
248
- to bottom,
249
- rgba(25, 23, 19, 0.22),
250
- rgba(25, 23, 19, 0.22) 1px,
251
- transparent 1px,
252
- transparent 3px,
253
- rgba(25, 23, 19, 0.22) 3px,
254
- rgba(25, 23, 19, 0.22) 4px
255
- );
256
- }
257
-
258
- /* Drop-cap for assistant answers (the editorial feature) */
259
- .drop-cap::first-letter {
260
- font-family: var(--font-fraunces), ui-serif, Georgia, serif;
261
  font-style: italic;
262
  font-weight: 400;
263
- font-variation-settings: "SOFT" 30, "WONK" 1;
264
- color: var(--rust);
265
- float: left;
266
- font-size: 4.4em;
267
- line-height: 0.85;
268
- margin: 0.08em 0.12em -0.05em -0.04em;
269
- padding: 0;
270
  }
271
 
272
- /* Containers */
273
  .container-app {
274
  @apply max-w-[1480px] mx-auto px-6 md:px-10;
275
  }
@@ -280,35 +209,49 @@
280
  @apply max-w-[760px] mx-auto px-6 md:px-10;
281
  }
282
 
283
- /* Italic-on-hover (editorial micro-interaction) */
284
- .hover-italic {
285
- transition: color 0.2s ease, font-style 0.001s;
286
- }
287
- .hover-italic:hover {
288
- font-style: italic;
289
- color: var(--rust);
 
 
 
290
  }
291
 
292
- /* Underline-from-left link */
293
- .link-rule {
294
- position: relative;
295
- color: inherit;
 
 
 
 
 
 
 
296
  }
297
- .link-rule::after {
298
- content: "";
299
- position: absolute;
300
- bottom: -2px;
301
- left: 0;
302
- width: 100%;
303
  height: 1px;
304
- background: currentColor;
305
- transform: scaleX(0);
306
- transform-origin: right;
307
- transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
 
 
308
  }
309
- .link-rule:hover::after {
310
- transform-origin: left;
311
- transform: scaleX(1);
 
 
 
 
 
312
  }
313
  }
314
 
@@ -316,54 +259,65 @@
316
  Utilities
317
  ─────────────────────────────────────────────────────────────── */
318
  @layer utilities {
319
- .text-balance {
320
- text-wrap: balance;
 
 
 
 
 
 
 
 
 
 
321
  }
322
- .text-pretty {
323
- text-wrap: pretty;
 
 
 
 
 
 
324
  }
325
 
326
- /* Halftone backdrop β€” for hero/feature panels */
327
- .halftone-bg {
328
- background-image: radial-gradient(
329
- circle at center,
330
- rgba(25, 23, 19, 0.18) 0.8px,
331
- transparent 1.6px
 
 
332
  );
333
- background-size: 5px 5px;
 
334
  }
335
- .halftone-rust {
336
- background-image: radial-gradient(
337
- circle at center,
338
- rgba(200, 77, 44, 0.4) 0.8px,
339
- transparent 1.6px
 
 
340
  );
341
- background-size: 5px 5px;
342
- }
343
-
344
- /* Register marks (corner crosshairs) */
345
- .register-corners {
346
- position: relative;
347
  }
348
- .register-corners::before,
349
- .register-corners::after {
350
- content: "✚";
351
  position: absolute;
352
- font-family: var(--font-mono);
353
- font-size: 11px;
354
- color: rgba(25, 23, 19, 0.30);
355
- line-height: 1;
356
- }
357
- .register-corners::before {
358
- top: 8px;
359
- left: 8px;
360
- }
361
- .register-corners::after {
362
- bottom: 8px;
363
- right: 8px;
364
  }
365
 
366
- /* No-scrollbar */
367
  .scrollbar-none {
368
  scrollbar-width: none;
369
  }
@@ -371,8 +325,7 @@
371
  display: none;
372
  }
373
 
374
- /* Riso-style "ink offset" effect β€” used for emphasis text */
375
- .ink-offset {
376
- text-shadow: 1px 0 0 rgba(200, 77, 44, 0.5);
377
  }
378
  }
 
3
  @tailwind utilities;
4
 
5
  /* ───────────────────────────────────────────────────────────────
6
+ Twilight Atelier β€” base canvas
7
  ─────────────────────────────────────────────────────────────── */
8
  @layer base {
9
  :root {
10
+ color-scheme: dark;
 
 
 
 
11
  }
12
 
13
  html,
14
  body {
15
+ background: #06070a;
16
+ color: #f5f3ec;
17
+ font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
18
  font-size: 15px;
19
+ line-height: 1.6;
20
  font-weight: 400;
21
+ font-feature-settings: "ss01", "cv11", "calt";
22
  -webkit-font-smoothing: antialiased;
23
  -moz-osx-font-smoothing: grayscale;
24
  text-rendering: optimizeLegibility;
25
  }
26
 
27
  body {
28
+ /* Layered: deep canvas + warm aurora + subtle grain */
 
 
 
 
 
29
  background:
30
+ radial-gradient(1100px 700px at 78% -10%, rgba(255, 181, 69, 0.12), transparent 65%),
31
+ radial-gradient(900px 800px at 8% 110%, rgba(98, 70, 255, 0.10), transparent 60%),
32
+ radial-gradient(1400px 900px at 50% 50%, rgba(20, 24, 40, 0.6), transparent 70%),
33
+ #06070a;
34
  background-attachment: fixed;
35
  min-height: 100vh;
36
  overflow-x: hidden;
 
37
  }
38
 
39
+ /* Subtle grain overlay (doesn't capture pointer events) */
40
  body::before {
41
  content: "";
42
  position: fixed;
43
  inset: 0;
44
  pointer-events: none;
45
  z-index: 0;
46
+ background-image: url("data:image/svg+xml;utf8,<svg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1, 0 0 0 0 1, 0 0 0 0 1, 0 0 0 0.06 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
47
+ opacity: 0.65;
48
+ mix-blend-mode: overlay;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
  ::selection {
52
+ background: rgba(255, 181, 69, 0.32);
53
+ color: #fff8e7;
54
  }
55
 
56
+ /* Custom scrollbar β€” barely there */
57
  ::-webkit-scrollbar {
58
+ width: 10px;
59
+ height: 10px;
60
  }
61
  ::-webkit-scrollbar-thumb {
62
+ background-color: rgba(255, 255, 255, 0.08);
63
  border-radius: 9999px;
64
+ border: 2px solid transparent;
65
  background-clip: content-box;
66
  }
67
  ::-webkit-scrollbar-thumb:hover {
68
+ background-color: rgba(255, 255, 255, 0.16);
69
  }
70
  ::-webkit-scrollbar-track {
71
  background: transparent;
72
  }
73
 
74
+ /* Focus ring β€” luminous amber */
75
  :focus-visible {
76
+ outline: 2px solid rgba(255, 181, 69, 0.75);
77
+ outline-offset: 2px;
78
+ border-radius: 4px;
79
+ }
80
+ button:focus-visible,
81
+ a:focus-visible,
82
+ input:focus-visible,
83
+ textarea:focus-visible {
84
  outline-offset: 3px;
85
  }
86
 
87
+ /* Default kerning for headlines */
88
  h1,
89
  h2,
90
  h3,
91
  h4 {
 
 
92
  letter-spacing: -0.02em;
 
 
 
 
 
 
93
  }
94
  }
95
 
96
  /* ───────────────────────────────────────────────────────────────
97
+ Component grammars
98
  ─────────────────────────────────────────────────────────────── */
99
  @layer components {
100
+ /* ── Glass surfaces ─────────────────────────────────────────── */
101
+ .glass {
102
+ background: rgba(255, 255, 255, 0.04);
103
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset,
104
+ 0 0 0 1px rgba(255, 255, 255, 0.08),
105
+ 0 16px 40px -16px rgba(0, 0, 0, 0.6);
106
+ backdrop-filter: blur(16px) saturate(140%);
107
+ -webkit-backdrop-filter: blur(16px) saturate(140%);
108
  }
109
 
110
+ .glass-strong {
111
+ background: rgba(255, 255, 255, 0.06);
112
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08) inset,
113
+ 0 0 0 1px rgba(255, 255, 255, 0.14),
114
+ 0 28px 80px -24px rgba(0, 0, 0, 0.7);
115
+ backdrop-filter: blur(20px) saturate(160%);
116
+ -webkit-backdrop-filter: blur(20px) saturate(160%);
117
+ }
118
+
119
+ .glass-elev {
120
+ background: linear-gradient(
121
+ 180deg,
122
+ rgba(255, 255, 255, 0.06) 0%,
123
+ rgba(255, 255, 255, 0.02) 100%
124
+ );
125
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08) inset,
126
+ 0 0 0 1px rgba(255, 255, 255, 0.10),
127
+ 0 32px 80px -24px rgba(0, 0, 0, 0.7);
128
  }
129
 
130
+ /* ── Buttons ────────────────────────────────────────────────── */
131
+ .btn-primary {
132
  @apply inline-flex items-center justify-center
133
  px-5 py-2.5 rounded-pill
134
+ text-body-strong text-canvas
135
+ bg-amber
136
  transition-all duration-200 ease-atelier
137
  active:scale-[0.97]
138
+ disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100
139
+ hover:bg-amber-soft hover:shadow-amber;
140
  }
141
 
142
  .btn-secondary {
143
  @apply inline-flex items-center justify-center
144
  px-5 py-2.5 rounded-pill
145
  text-body text-ink
146
+ bg-glass
147
  transition-all duration-200 ease-atelier
148
  active:scale-[0.97]
149
+ hover:bg-glass-stronger;
150
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.10);
151
  }
152
 
153
  .btn-ghost {
 
155
  px-3 py-1.5 rounded-md
156
  text-caption text-ink-70
157
  transition-colors duration-200
158
+ hover:text-ink hover:bg-glass;
159
  }
160
 
161
+ .btn-danger {
162
+ @apply inline-flex items-center justify-center
163
+ px-5 py-2.5 rounded-pill
164
+ text-body-strong text-status-err
165
+ transition-colors duration-200
166
+ hover:bg-status-err-glow;
167
+ box-shadow: 0 0 0 1px rgba(255, 122, 122, 0.4);
168
+ }
169
+
170
+ /* ── Inputs ─────────────────────────────────────────────────── */
171
+ .input-glass {
172
+ @apply w-full bg-glass text-ink
173
  rounded-md px-4 py-3
174
  text-body
175
  outline-none
176
  transition-all duration-200;
177
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.10);
 
178
  }
179
+ .input-glass:focus {
180
+ background: rgba(255, 255, 255, 0.07);
181
+ box-shadow: 0 0 0 1px rgba(255, 181, 69, 0.55),
182
+ 0 0 24px -6px rgba(255, 181, 69, 0.35);
183
  }
184
+ .input-glass::placeholder {
185
+ color: rgba(245, 243, 236, 0.36);
186
  }
187
 
188
+ /* ── Cards ──────────────────────────────────────────────────── */
189
  .card {
190
+ @apply rounded-lg p-6 glass;
 
 
 
191
  }
192
 
193
+ /* ── Editorial italic moments ───────────────────────────────── */
194
+ .serif-italic {
195
+ font-family: var(--font-instrument), ui-serif, Georgia, serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  font-style: italic;
197
  font-weight: 400;
198
+ letter-spacing: -0.01em;
 
 
 
 
 
 
199
  }
200
 
201
+ /* ── Containers ─────────────────────────────────────────────── */
202
  .container-app {
203
  @apply max-w-[1480px] mx-auto px-6 md:px-10;
204
  }
 
209
  @apply max-w-[760px] mx-auto px-6 md:px-10;
210
  }
211
 
212
+ /* ── Chat-specific helpers ──────────────────────────────────── */
213
+ .chat-bubble-user {
214
+ @apply rounded-2xl rounded-tr-sm px-5 py-3 text-body text-ink;
215
+ background: linear-gradient(
216
+ 135deg,
217
+ rgba(255, 181, 69, 0.16) 0%,
218
+ rgba(255, 181, 69, 0.08) 100%
219
+ );
220
+ box-shadow: 0 0 0 1px rgba(255, 181, 69, 0.22),
221
+ 0 8px 24px -8px rgba(255, 181, 69, 0.18);
222
  }
223
 
224
+ .chat-bubble-assistant {
225
+ @apply rounded-2xl rounded-tl-sm px-5 py-4 text-body text-ink;
226
+ background: linear-gradient(
227
+ 180deg,
228
+ rgba(255, 255, 255, 0.05) 0%,
229
+ rgba(255, 255, 255, 0.025) 100%
230
+ );
231
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.10),
232
+ 0 16px 48px -16px rgba(0, 0, 0, 0.5);
233
+ backdrop-filter: blur(16px);
234
+ -webkit-backdrop-filter: blur(16px);
235
  }
236
+
237
+ /* Hairline divider β€” for sectioning inside cards */
238
+ .hairline {
 
 
 
239
  height: 1px;
240
+ background: linear-gradient(
241
+ 90deg,
242
+ transparent,
243
+ rgba(255, 255, 255, 0.12),
244
+ transparent
245
+ );
246
  }
247
+ .hairline-vert {
248
+ width: 1px;
249
+ background: linear-gradient(
250
+ 180deg,
251
+ transparent,
252
+ rgba(255, 255, 255, 0.10),
253
+ transparent
254
+ );
255
  }
256
  }
257
 
 
259
  Utilities
260
  ─────────────────────────────────────────────────────────────── */
261
  @layer utilities {
262
+ .text-shimmer {
263
+ background: linear-gradient(
264
+ 110deg,
265
+ rgba(245, 243, 236, 0.4) 30%,
266
+ rgba(245, 243, 236, 1) 50%,
267
+ rgba(245, 243, 236, 0.4) 70%
268
+ );
269
+ background-size: 200% 100%;
270
+ -webkit-background-clip: text;
271
+ background-clip: text;
272
+ color: transparent;
273
+ animation: shimmerBg 2.4s linear infinite;
274
  }
275
+
276
+ @keyframes shimmerBg {
277
+ 0% {
278
+ background-position: 200% 0;
279
+ }
280
+ 100% {
281
+ background-position: -200% 0;
282
+ }
283
  }
284
 
285
+ /* Aurora gradient orbs (used decoratively behind hero) */
286
+ .aurora-amber {
287
+ position: absolute;
288
+ border-radius: 9999px;
289
+ background: radial-gradient(
290
+ circle,
291
+ rgba(255, 181, 69, 0.32) 0%,
292
+ rgba(255, 181, 69, 0) 65%
293
  );
294
+ filter: blur(80px);
295
+ pointer-events: none;
296
  }
297
+ .aurora-violet {
298
+ position: absolute;
299
+ border-radius: 9999px;
300
+ background: radial-gradient(
301
+ circle,
302
+ rgba(120, 90, 240, 0.22) 0%,
303
+ rgba(120, 90, 240, 0) 65%
304
  );
305
+ filter: blur(90px);
306
+ pointer-events: none;
 
 
 
 
307
  }
308
+ .aurora-teal {
 
 
309
  position: absolute;
310
+ border-radius: 9999px;
311
+ background: radial-gradient(
312
+ circle,
313
+ rgba(80, 200, 200, 0.18) 0%,
314
+ rgba(80, 200, 200, 0) 65%
315
+ );
316
+ filter: blur(80px);
317
+ pointer-events: none;
 
 
 
 
318
  }
319
 
320
+ /* No-scrollbar utility */
321
  .scrollbar-none {
322
  scrollbar-width: none;
323
  }
 
325
  display: none;
326
  }
327
 
328
+ .text-balance {
329
+ text-wrap: balance;
 
330
  }
331
  }
app/layout.tsx CHANGED
@@ -1,35 +1,35 @@
1
  import type { Metadata } from "next";
2
- import { Fraunces, IBM_Plex_Sans, JetBrains_Mono } from "next/font/google";
3
  import "./globals.css";
4
  import { Providers } from "./providers";
5
  import { AppShell } from "@/components/chat/AppShell";
6
 
7
- const fraunces = Fraunces({
8
  subsets: ["latin"],
9
- variable: "--font-fraunces",
10
  display: "swap",
11
- style: ["normal", "italic"],
12
- axes: ["SOFT", "WONK", "opsz"],
13
  });
14
 
15
- const plex = IBM_Plex_Sans({
16
  subsets: ["latin"],
17
- variable: "--font-plex",
18
  display: "swap",
19
- weight: ["300", "400", "500", "600", "700"],
 
20
  });
21
 
22
  const mono = JetBrains_Mono({
23
  subsets: ["latin"],
24
  variable: "--font-mono",
25
  display: "swap",
26
- weight: ["400", "500", "600"],
27
  });
28
 
29
  export const metadata: Metadata = {
30
- title: "docΒ·toΒ·lora β€” Etiya BSS Folio",
31
  description:
32
- "A printed-publication interrogation surface for the Etiya BSS corpus, built on the doc-to-lora hypernetwork.",
33
  };
34
 
35
  export default function RootLayout({
@@ -40,7 +40,7 @@ export default function RootLayout({
40
  return (
41
  <html
42
  lang="en"
43
- className={`${fraunces.variable} ${plex.variable} ${mono.variable}`}
44
  >
45
  <body className="min-h-screen">
46
  <Providers>
 
1
  import type { Metadata } from "next";
2
+ import { Instrument_Serif, Manrope, JetBrains_Mono } from "next/font/google";
3
  import "./globals.css";
4
  import { Providers } from "./providers";
5
  import { AppShell } from "@/components/chat/AppShell";
6
 
7
+ const manrope = Manrope({
8
  subsets: ["latin"],
9
+ variable: "--font-manrope",
10
  display: "swap",
11
+ weight: ["300", "400", "500", "600", "700"],
 
12
  });
13
 
14
+ const instrument = Instrument_Serif({
15
  subsets: ["latin"],
16
+ variable: "--font-instrument",
17
  display: "swap",
18
+ weight: ["400"],
19
+ style: ["normal", "italic"],
20
  });
21
 
22
  const mono = JetBrains_Mono({
23
  subsets: ["latin"],
24
  variable: "--font-mono",
25
  display: "swap",
26
+ weight: ["400", "500"],
27
  });
28
 
29
  export const metadata: Metadata = {
30
+ title: "doc-to-lora Β· Etiya BSS Atelier",
31
  description:
32
+ "An atelier for stateless retrieval-augmented inference over Etiya BSS documents β€” built on the doc-to-lora hypernetwork.",
33
  };
34
 
35
  export default function RootLayout({
 
40
  return (
41
  <html
42
  lang="en"
43
+ className={`${manrope.variable} ${instrument.variable} ${mono.variable}`}
44
  >
45
  <body className="min-h-screen">
46
  <Providers>
app/system/page.tsx CHANGED
@@ -6,10 +6,6 @@ import { useMutation, useQuery } from "@tanstack/react-query";
6
  import { useEffect, useState } from "react";
7
  import clsx from "clsx";
8
 
9
- /**
10
- * Colophon. The publication's printer's marks: typesetters, presses,
11
- * paper, ink. Here: model, hardware, latencies, eval suite, indexer.
12
- */
13
  export default function SystemPage() {
14
  const { data, error, refetch, isFetching } = useQuery({
15
  queryKey: ["health"],
@@ -30,195 +26,125 @@ export default function SystemPage() {
30
 
31
  return (
32
  <div className="flex-1 overflow-y-auto">
33
- <div className="container-app py-10 space-y-12">
34
- {/* Editorial masthead */}
35
- <header className="animate-fade-in">
36
- <div className="flex items-baseline justify-between mb-3">
37
- <span className="folio-chrome">Folio III Β· Colophon</span>
38
- <button
39
- onClick={() => refetch()}
40
- disabled={isFetching}
41
- className="folio-chrome hover:text-rust transition-colors"
42
- >
43
- {isFetching ? "Refreshing…" : "Refresh ↻"}
44
- </button>
45
  </div>
46
- <h1
47
- className="text-ink"
48
- style={{
49
- fontSize: "clamp(56px, 9vw, 110px)",
50
- lineHeight: 0.92,
51
- fontWeight: 300,
52
- letterSpacing: "-0.04em",
53
- fontFamily: "var(--font-fraunces)",
54
- }}
55
  >
56
- The <span className="italic-display text-rust">colophon.</span>
57
- </h1>
58
- <div className="hairline--double mt-6 animate-rule" />
59
- <p
60
- className="text-ink mt-6 max-w-[64ch] animate-stagger-1"
61
- style={{
62
- fontFamily: "var(--font-fraunces)",
63
- fontSize: "20px",
64
- lineHeight: 1.45,
65
- fontWeight: 300,
66
- }}
67
- >
68
- A printer's mark for the atelier β€” the presses, the type, the paper.
69
- Live readouts auto-refresh every thirty seconds.
70
- </p>
71
  </header>
72
 
73
  {error && (
74
- <div className="card-rust animate-fade-in">
75
- <div className="folio-chrome--rust folio-chrome mb-1">Errata</div>
76
- <p className="text-body-strong text-ink">Failed to read /health</p>
 
 
 
 
 
 
 
77
  <p className="text-caption text-ink-70 mt-1">
78
  {error instanceof Error ? error.message : String(error)}
79
  </p>
80
  </div>
81
  )}
82
 
83
- {/* Hero metric panel β€” magazine pull-quote of the GPU */}
84
- <section className="animate-fade-up">
85
- <div
86
- className="rounded-lg p-8 md:p-12 register-corners halftone-bg"
87
- style={{
88
- background:
89
- "linear-gradient(180deg, var(--paper-deep) 0%, var(--paper) 100%)",
90
- boxShadow: "0 0 0 1.5px rgba(25,23,19,0.30), 0 1px 0 rgba(255,255,255,0.5) inset",
91
- }}
92
- >
93
- <div className="folio-chrome mb-3">Press status</div>
94
- <div className="flex items-baseline gap-4 flex-wrap">
95
- <div
96
- className="italic-display text-ink"
97
- style={{ fontSize: "clamp(64px, 10vw, 120px)", lineHeight: 0.95, fontWeight: 300 }}
98
- >
99
- {data === undefined ? "β€”" : data.model_loaded ? "Running" : "Loading"}
100
- </div>
101
- <span
102
- className={clsx(
103
- "inline-block w-3 h-3 rounded-full mb-3 ml-2 transition-colors",
104
- data === undefined
105
- ? "bg-ink-30"
106
- : data.model_loaded
107
- ? "bg-status-ok animate-ink-pulse"
108
- : "bg-status-warn animate-ink-pulse"
109
- )}
110
- />
111
- </div>
112
-
113
- <div className="hairline mt-8 mb-6" />
114
-
115
- <div className="grid sm:grid-cols-3 gap-x-8 gap-y-6">
116
- <Pull
117
- label="Documents indexed"
118
- value={data?.doc_count?.toLocaleString() ?? "β€”"}
119
- />
120
- <Pull
121
- label="GPU memory"
122
- value={data ? data.gpu_memory_gb.toFixed(2) : "β€”"}
123
- unit="GB"
124
- />
125
- <Pull label="Hardware" value="A100" unit="80GB" />
126
- </div>
127
- </div>
128
  </section>
129
 
130
  {/* Latencies + corpus */}
131
- <section className="grid lg:grid-cols-2 gap-6 animate-fade-up-slow">
132
  <Latencies />
133
  <CorpusBreakdown data={data} />
134
  </section>
135
 
136
- {/* Eval β€” pull-quote panel with halftone */}
137
- <section className="animate-fade-up">
138
- <div
139
- className="rounded-lg p-8 md:p-10 relative overflow-hidden"
140
- style={{
141
- background: "var(--ink)",
142
- color: "var(--paper)",
143
- }}
144
- >
145
- <div
146
- aria-hidden
147
- className="absolute inset-0 opacity-25"
148
- style={{
149
- backgroundImage:
150
- "radial-gradient(circle at center, rgba(244, 237, 224, 0.5) 0.8px, transparent 1.6px)",
151
- backgroundSize: "5px 5px",
152
- }}
153
- />
154
- <div className="relative">
155
- <div className="folio-chrome" style={{ color: "rgba(244,237,224,0.5)" }}>
156
- Issue review Β· Eval suite Β· 100 questions
157
- </div>
158
- <h2
159
- className="mt-3"
160
- style={{
161
- fontFamily: "var(--font-fraunces)",
162
- fontSize: "44px",
163
- lineHeight: 1.05,
164
- fontWeight: 400,
165
- }}
166
- >
167
- The <span className="italic-display" style={{ color: "var(--rust)" }}>numbers.</span>
168
- </h2>
169
- <div className="hairline--double mt-6 mb-8" style={{ filter: "invert(1)", opacity: 0.6 }} />
170
- <div className="grid grid-cols-2 md:grid-cols-4 gap-x-8 gap-y-6">
171
- <Bench label="Hallucination defense" value="87.5" unit="%" />
172
- <Bench label="Concept queries" value="84" unit="%" />
173
- <Bench label="Cross-domain" value="55" unit="%" />
174
- <Bench label="Fact recall" value="59" unit="%" />
175
- </div>
176
- <p
177
- className="mt-8 max-w-[60ch]"
178
- style={{
179
- fontFamily: "var(--font-fraunces)",
180
- fontSize: "16px",
181
- lineHeight: 1.55,
182
- fontWeight: 300,
183
- color: "rgba(244,237,224,0.75)",
184
- }}
185
- >
186
- Pure numerical signals β€” anchor, dense, rerank β€” with zero
187
- hardcoded reference text. The eval set lives at{" "}
188
- <span className="font-mono" style={{ color: "var(--paper)" }}>
189
- eval/eval_set.jsonl
190
- </span>{" "}
191
- and is replayed on demand.
192
- </p>
193
  </div>
 
 
 
 
 
 
194
  </div>
195
  </section>
196
 
197
  {/* Index management */}
198
  <section>
199
  <div className="mb-5">
200
- <span className="folio-chrome">Press Β· Index management</span>
201
- <h2
202
- className="text-ink mt-2"
203
- style={{
204
- fontFamily: "var(--font-fraunces)",
205
- fontSize: "36px",
206
- fontWeight: 500,
207
- letterSpacing: "-0.02em",
208
- }}
209
- >
210
- Run the <span className="italic-display text-rust">indexer.</span>
211
  </h2>
212
- <p className="text-caption text-ink-70 mt-1.5 max-w-[64ch]">
213
- The retrieval index needs syncing after documents change.
214
- Incremental embeds only deltas; full rebuild re-anchors topics.
215
  </p>
216
  </div>
217
 
218
- <div className="grid md:grid-cols-2 gap-6">
219
  <ReindexCard
220
  title="Incremental"
221
- description="Sync new and removed entries only."
222
  cost="β‰ˆ $0.0001"
223
  latency="~350 ms"
224
  loading={
@@ -227,13 +153,13 @@ export default function SystemPage() {
227
  onClick={() =>
228
  reindex.mutate({ force_full: false, rebuild_anchors: false })
229
  }
230
- accent
231
  />
232
  <ReindexCard
233
  title="Full rebuild"
234
- description="Re-embed all entries + rebuild K-means topic anchors."
235
  cost="β‰ˆ $0.16"
236
  latency="~30 s"
 
237
  loading={reindex.isPending && !!reindex.variables?.force_full}
238
  onClick={() =>
239
  reindex.mutate({ force_full: true, rebuild_anchors: true })
@@ -242,22 +168,25 @@ export default function SystemPage() {
242
  </div>
243
 
244
  {reindex.data && (
245
- <div className="mt-6 card animate-fade-up">
246
- <div className="folio-chrome--ink folio-chrome mb-2">
247
- Re-index complete
248
  </div>
249
- <pre
250
- className="font-mono text-caption text-ink whitespace-pre-wrap break-all"
251
- style={{ fontSize: "12px", lineHeight: 1.6 }}
252
- >
253
  {JSON.stringify(reindex.data, null, 2)}
254
  </pre>
255
  </div>
256
  )}
257
  {reindex.error && (
258
- <div className="mt-6 card-rust">
259
- <div className="folio-chrome--rust folio-chrome mb-1">
260
- Re-index failed
 
 
 
 
 
 
261
  </div>
262
  <p className="text-caption text-ink">
263
  {reindex.error instanceof Error
@@ -272,40 +201,44 @@ export default function SystemPage() {
272
  );
273
  }
274
 
275
- function Pull({
276
  label,
277
  value,
278
  unit,
 
279
  }: {
280
  label: string;
281
  value: string | number;
282
  unit?: string;
 
283
  }) {
 
 
 
 
 
 
284
  return (
285
- <div>
286
- <div className="folio-chrome mb-1.5">{label}</div>
287
- <div
288
- className="text-ink"
289
- style={{
290
- fontFamily: "var(--font-fraunces)",
291
- fontSize: "44px",
292
- lineHeight: 1,
293
- fontWeight: 400,
294
- letterSpacing: "-0.02em",
295
- }}
296
- >
297
  {value}
298
- {unit && (
299
- <span className="text-rust ml-1.5" style={{ fontSize: "22px", fontWeight: 400 }}>
300
- {unit}
301
- </span>
302
- )}
303
  </div>
304
  </div>
305
  );
306
  }
307
 
308
- function Bench({
309
  label,
310
  value,
311
  unit,
@@ -316,28 +249,12 @@ function Bench({
316
  }) {
317
  return (
318
  <div>
319
- <div
320
- className="folio-chrome"
321
- style={{ color: "rgba(244,237,224,0.5)" }}
322
- >
323
  {label}
324
  </div>
325
- <div
326
- className="mt-1.5"
327
- style={{
328
- fontFamily: "var(--font-fraunces)",
329
- fontSize: "56px",
330
- lineHeight: 1,
331
- fontWeight: 300,
332
- letterSpacing: "-0.025em",
333
- }}
334
- >
335
  {value}
336
- {unit && (
337
- <span style={{ color: "var(--rust)", fontSize: "26px", marginLeft: "2px" }}>
338
- {unit}
339
- </span>
340
- )}
341
  </div>
342
  </div>
343
  );
@@ -367,23 +284,11 @@ function Latencies() {
367
 
368
  return (
369
  <div className="card">
370
- <div className="flex items-baseline justify-between mb-3">
371
- <span className="folio-chrome">Press timings</span>
372
- </div>
373
- <h3
374
- className="text-ink mb-4"
375
- style={{
376
- fontFamily: "var(--font-fraunces)",
377
- fontSize: "24px",
378
- fontWeight: 500,
379
- }}
380
- >
381
- Round-trip <span className="italic-display text-rust">latencies.</span>
382
- </h3>
383
- <div className="space-y-2.5">
384
  <Row label="browser β†’ ping" value={pingMs ? `${pingMs.toFixed(0)} ms` : "β€”"} />
385
  <Row label="ask_smart (reject)" value="β‰ˆ 400 ms" />
386
- <Row label="ask_smart (inference)" value="β‰ˆ 1.3 – 2.0 s" />
387
  <Row label="reindex (incremental)" value="β‰ˆ 350 ms" />
388
  <Row label="reindex (full)" value="β‰ˆ 30 s" />
389
  </div>
@@ -394,20 +299,8 @@ function Latencies() {
394
  function CorpusBreakdown({ data }: { data: HealthResponse | undefined }) {
395
  return (
396
  <div className="card">
397
- <div className="flex items-baseline justify-between mb-3">
398
- <span className="folio-chrome">Plates &amp; type</span>
399
- </div>
400
- <h3
401
- className="text-ink mb-4"
402
- style={{
403
- fontFamily: "var(--font-fraunces)",
404
- fontSize: "24px",
405
- fontWeight: 500,
406
- }}
407
- >
408
- Corpus &amp; <span className="italic-display text-rust">index.</span>
409
- </h3>
410
- <div className="space-y-2.5">
411
  <Row
412
  label="documents on disk"
413
  value={data ? data.doc_count.toLocaleString() : "β€”"}
@@ -428,7 +321,7 @@ function ReindexCard({
428
  cost,
429
  latency,
430
  loading,
431
- accent,
432
  onClick,
433
  }: {
434
  title: string;
@@ -436,32 +329,30 @@ function ReindexCard({
436
  cost: string;
437
  latency: string;
438
  loading: boolean;
439
- accent?: boolean;
440
  onClick: () => void;
441
  }) {
442
  return (
443
- <div className={accent ? "card-rust flex flex-col gap-4" : "card flex flex-col gap-4"}>
444
  <div>
445
- <div className="flex items-baseline justify-between">
446
- <span className="folio-chrome--ink folio-chrome">{latency}</span>
447
- <span className="folio-chrome">openai cost {cost}</span>
 
 
448
  </div>
449
- <h3
450
- className="text-ink mt-2"
451
- style={{
452
- fontFamily: "var(--font-fraunces)",
453
- fontSize: "26px",
454
- fontWeight: 500,
455
- }}
456
- >
457
- {title}<span className="italic-display text-rust">.</span>
458
- </h3>
459
- <p className="text-caption text-ink-70 mt-1.5">{description}</p>
460
  </div>
461
  <button
462
  onClick={onClick}
463
  disabled={loading}
464
- className={accent ? "btn-rust self-start" : "btn-primary self-start"}
 
 
 
465
  >
466
  {loading ? "Running…" : `Run ${title.toLowerCase()}`}
467
  </button>
@@ -471,9 +362,11 @@ function ReindexCard({
471
 
472
  function Row({ label, value }: { label: string; value: string }) {
473
  return (
474
- <div className="grid grid-cols-[1fr_auto] gap-4 items-baseline border-b border-ink/10 pb-2 last:border-0 last:pb-0">
475
- <span className="folio-chrome">{label}</span>
476
- <span className="font-mono text-caption text-ink truncate">{value}</span>
 
 
477
  </div>
478
  );
479
  }
 
6
  import { useEffect, useState } from "react";
7
  import clsx from "clsx";
8
 
 
 
 
 
9
  export default function SystemPage() {
10
  const { data, error, refetch, isFetching } = useQuery({
11
  queryKey: ["health"],
 
26
 
27
  return (
28
  <div className="flex-1 overflow-y-auto">
29
+ <div className="container-app py-10 space-y-10">
30
+ {/* Header */}
31
+ <header className="flex items-end justify-between flex-wrap gap-4 animate-fade-in">
32
+ <div>
33
+ <h1 className="text-display-lg text-ink">
34
+ System <span className="serif-italic text-amber">readout.</span>
35
+ </h1>
36
+ <p className="text-body text-ink-50 mt-2">
37
+ Live telemetry β€” auto-refresh every 30s.
38
+ </p>
 
 
39
  </div>
40
+ <button
41
+ onClick={() => refetch()}
42
+ disabled={isFetching}
43
+ className="btn-secondary"
 
 
 
 
 
44
  >
45
+ {isFetching ? "Refreshing…" : "Refresh now"}
46
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </header>
48
 
49
  {error && (
50
+ <div
51
+ className="p-4 rounded-md"
52
+ style={{
53
+ background: "rgba(255, 122, 122, 0.06)",
54
+ boxShadow: "0 0 0 1px rgba(255, 122, 122, 0.28)",
55
+ }}
56
+ >
57
+ <p className="text-status-err text-body-strong">
58
+ Failed to read /health
59
+ </p>
60
  <p className="text-caption text-ink-70 mt-1">
61
  {error instanceof Error ? error.message : String(error)}
62
  </p>
63
  </div>
64
  )}
65
 
66
+ {/* Hero metrics */}
67
+ <section className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 animate-fade-up">
68
+ <BigMetric
69
+ label="Stage"
70
+ value={
71
+ data === undefined
72
+ ? "β€”"
73
+ : data.model_loaded
74
+ ? "Running"
75
+ : "Loading"
76
+ }
77
+ tone={
78
+ data === undefined ? "neutral" : data.model_loaded ? "ok" : "warn"
79
+ }
80
+ />
81
+ <BigMetric
82
+ label="Documents indexed"
83
+ value={data?.doc_count?.toLocaleString() ?? "β€”"}
84
+ />
85
+ <BigMetric
86
+ label="GPU memory"
87
+ value={data ? data.gpu_memory_gb.toFixed(2) : "β€”"}
88
+ unit="GB"
89
+ />
90
+ <BigMetric label="Hardware" value="A100" unit="80GB" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  </section>
92
 
93
  {/* Latencies + corpus */}
94
+ <section className="grid lg:grid-cols-2 gap-4 animate-fade-up-slow">
95
  <Latencies />
96
  <CorpusBreakdown data={data} />
97
  </section>
98
 
99
+ {/* Eval suite β€” featured panel */}
100
+ <section
101
+ className="relative rounded-xl p-8 md:p-10 overflow-hidden"
102
+ style={{
103
+ background:
104
+ "linear-gradient(135deg, rgba(255,181,69,0.08) 0%, rgba(255,255,255,0.02) 100%)",
105
+ boxShadow:
106
+ "0 0 0 1px rgba(255,181,69,0.20), 0 32px 80px -32px rgba(0,0,0,0.6)",
107
+ }}
108
+ >
109
+ <div className="aurora-amber" style={{ width: 480, height: 480, top: -200, right: -200 }} />
110
+ <div className="relative">
111
+ <div className="text-micro uppercase tracking-[0.18em] text-amber font-mono">
112
+ Eval suite
113
+ </div>
114
+ <h2 className="text-display-md text-ink mt-3">
115
+ 100-question <span className="serif-italic">benchmark.</span>
116
+ </h2>
117
+ <div className="mt-8 grid grid-cols-2 md:grid-cols-4 gap-6">
118
+ <BenchMetric label="Hallucination defense" value="87.5" unit="%" />
119
+ <BenchMetric label="Concept queries" value="84" unit="%" />
120
+ <BenchMetric label="Cross-domain" value="55" unit="%" />
121
+ <BenchMetric label="Fact recall" value="59" unit="%" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </div>
123
+ <p className="text-caption text-ink-70 mt-8 max-w-[640px]">
124
+ Pure numerical signals (anchor / dense / rerank) with zero hardcoded
125
+ reference text. The eval set lives at{" "}
126
+ <span className="font-mono text-ink">eval/eval_set.jsonl</span>{" "}
127
+ and can be replayed any time.
128
+ </p>
129
  </div>
130
  </section>
131
 
132
  {/* Index management */}
133
  <section>
134
  <div className="mb-5">
135
+ <h2 className="text-display-md text-ink">
136
+ Index <span className="serif-italic text-amber">management.</span>
 
 
 
 
 
 
 
 
 
137
  </h2>
138
+ <p className="text-caption text-ink-50 mt-1.5 max-w-[680px]">
139
+ The RAG index needs to be synchronized after document changes.
140
+ Incremental is the default β€” it only embeds new/removed docs.
141
  </p>
142
  </div>
143
 
144
+ <div className="grid md:grid-cols-2 gap-4">
145
  <ReindexCard
146
  title="Incremental"
147
+ description="Sync new and removed docs only."
148
  cost="β‰ˆ $0.0001"
149
  latency="~350 ms"
150
  loading={
 
153
  onClick={() =>
154
  reindex.mutate({ force_full: false, rebuild_anchors: false })
155
  }
 
156
  />
157
  <ReindexCard
158
  title="Full rebuild"
159
+ description="Re-embed all docs + rebuild K-means anchors."
160
  cost="β‰ˆ $0.16"
161
  latency="~30 s"
162
+ danger
163
  loading={reindex.isPending && !!reindex.variables?.force_full}
164
  onClick={() =>
165
  reindex.mutate({ force_full: true, rebuild_anchors: true })
 
168
  </div>
169
 
170
  {reindex.data && (
171
+ <div className="mt-4 card animate-fade-up">
172
+ <div className="text-micro uppercase tracking-[0.15em] text-status-ok font-mono mb-2">
173
+ re-index complete
174
  </div>
175
+ <pre className="font-mono text-caption text-ink whitespace-pre-wrap break-all">
 
 
 
176
  {JSON.stringify(reindex.data, null, 2)}
177
  </pre>
178
  </div>
179
  )}
180
  {reindex.error && (
181
+ <div
182
+ className="mt-4 p-4 rounded-md"
183
+ style={{
184
+ background: "rgba(255, 122, 122, 0.06)",
185
+ boxShadow: "0 0 0 1px rgba(255, 122, 122, 0.28)",
186
+ }}
187
+ >
188
+ <div className="text-micro uppercase tracking-[0.15em] text-status-err font-mono mb-1">
189
+ re-index failed
190
  </div>
191
  <p className="text-caption text-ink">
192
  {reindex.error instanceof Error
 
201
  );
202
  }
203
 
204
+ function BigMetric({
205
  label,
206
  value,
207
  unit,
208
+ tone = "neutral",
209
  }: {
210
  label: string;
211
  value: string | number;
212
  unit?: string;
213
+ tone?: "ok" | "warn" | "err" | "neutral";
214
  }) {
215
+ const dotCls = {
216
+ ok: "bg-status-ok shadow-[0_0_8px_rgba(93,214,168,0.7)]",
217
+ warn: "bg-amber shadow-[0_0_8px_rgba(255,181,69,0.7)]",
218
+ err: "bg-status-err",
219
+ neutral: "bg-ink-30",
220
+ }[tone];
221
  return (
222
+ <div className="card">
223
+ <div className="flex items-center gap-2 text-micro uppercase tracking-[0.14em] text-ink-50 font-mono">
224
+ <span
225
+ className={clsx(
226
+ "inline-block w-1.5 h-1.5 rounded-full",
227
+ dotCls,
228
+ tone !== "neutral" && "animate-pulse-soft"
229
+ )}
230
+ />
231
+ {label}
232
+ </div>
233
+ <div className="text-display-md font-mono text-ink mt-2">
234
  {value}
235
+ {unit && <span className="text-ink-50 ml-1.5 text-lead">{unit}</span>}
 
 
 
 
236
  </div>
237
  </div>
238
  );
239
  }
240
 
241
+ function BenchMetric({
242
  label,
243
  value,
244
  unit,
 
249
  }) {
250
  return (
251
  <div>
252
+ <div className="text-micro uppercase tracking-[0.14em] text-ink-50 font-mono">
 
 
 
253
  {label}
254
  </div>
255
+ <div className="text-display-lg font-mono text-ink mt-2">
 
 
 
 
 
 
 
 
 
256
  {value}
257
+ {unit && <span className="text-amber ml-1 text-display-sm">{unit}</span>}
 
 
 
 
258
  </div>
259
  </div>
260
  );
 
284
 
285
  return (
286
  <div className="card">
287
+ <h3 className="text-body-strong text-ink mb-4">Round-trip latencies</h3>
288
+ <div className="space-y-2.5 text-caption">
 
 
 
 
 
 
 
 
 
 
 
 
289
  <Row label="browser β†’ ping" value={pingMs ? `${pingMs.toFixed(0)} ms` : "β€”"} />
290
  <Row label="ask_smart (reject)" value="β‰ˆ 400 ms" />
291
+ <Row label="ask_smart (inference)" value="β‰ˆ 1.3-2.0 s" />
292
  <Row label="reindex (incremental)" value="β‰ˆ 350 ms" />
293
  <Row label="reindex (full)" value="β‰ˆ 30 s" />
294
  </div>
 
299
  function CorpusBreakdown({ data }: { data: HealthResponse | undefined }) {
300
  return (
301
  <div className="card">
302
+ <h3 className="text-body-strong text-ink mb-4">Corpus &amp; index</h3>
303
+ <div className="space-y-2.5 text-caption">
 
 
 
 
 
 
 
 
 
 
 
 
304
  <Row
305
  label="documents on disk"
306
  value={data ? data.doc_count.toLocaleString() : "β€”"}
 
321
  cost,
322
  latency,
323
  loading,
324
+ danger,
325
  onClick,
326
  }: {
327
  title: string;
 
329
  cost: string;
330
  latency: string;
331
  loading: boolean;
332
+ danger?: boolean;
333
  onClick: () => void;
334
  }) {
335
  return (
336
+ <div className="card flex flex-col gap-4">
337
  <div>
338
+ <div className="flex items-center justify-between">
339
+ <h3 className="text-display-sm text-ink">{title}</h3>
340
+ <span className="text-micro font-mono uppercase tracking-[0.12em] text-amber">
341
+ {latency}
342
+ </span>
343
  </div>
344
+ <p className="text-caption text-ink-70 mt-1">{description}</p>
345
+ <p className="text-micro text-ink-50 mt-2 font-mono uppercase tracking-[0.10em]">
346
+ openai cost {cost}
347
+ </p>
 
 
 
 
 
 
 
348
  </div>
349
  <button
350
  onClick={onClick}
351
  disabled={loading}
352
+ className={clsx(
353
+ danger ? "btn-secondary" : "btn-primary",
354
+ "self-start"
355
+ )}
356
  >
357
  {loading ? "Running…" : `Run ${title.toLowerCase()}`}
358
  </button>
 
362
 
363
  function Row({ label, value }: { label: string; value: string }) {
364
  return (
365
+ <div className="flex items-center justify-between gap-4">
366
+ <span className="text-ink-50 font-mono uppercase tracking-[0.10em] text-micro">
367
+ {label}
368
+ </span>
369
+ <span className="font-mono text-ink text-caption truncate">{value}</span>
370
  </div>
371
  );
372
  }
components/chat/AppShell.tsx CHANGED
@@ -4,10 +4,12 @@ import { Sidebar } from "./Sidebar";
4
  import { TopBar } from "./TopBar";
5
  import { Aurora } from "./Aurora";
6
  import { useChatStore } from "@/lib/chatStore";
 
7
  import { usePathname } from "next/navigation";
8
  import { useEffect } from "react";
9
 
10
  export function AppShell({ children }: { children: React.ReactNode }) {
 
11
  const pathname = usePathname();
12
  const conversations = useChatStore((s) => s.conversations);
13
  const activeId = useChatStore((s) => s.activeId);
@@ -23,12 +25,21 @@ export function AppShell({ children }: { children: React.ReactNode }) {
23
 
24
  return (
25
  <div className="relative min-h-screen flex flex-col">
 
26
  <Aurora />
 
27
  <TopBar />
28
 
29
- <div className="relative z-10 flex-1 flex">
30
  <Sidebar />
31
- <main className="flex-1 flex flex-col min-w-0">{children}</main>
 
 
 
 
 
 
 
32
  </div>
33
  </div>
34
  );
 
4
  import { TopBar } from "./TopBar";
5
  import { Aurora } from "./Aurora";
6
  import { useChatStore } from "@/lib/chatStore";
7
+ import clsx from "clsx";
8
  import { usePathname } from "next/navigation";
9
  import { useEffect } from "react";
10
 
11
  export function AppShell({ children }: { children: React.ReactNode }) {
12
+ const sidebarOpen = useChatStore((s) => s.sidebarOpen);
13
  const pathname = usePathname();
14
  const conversations = useChatStore((s) => s.conversations);
15
  const activeId = useChatStore((s) => s.activeId);
 
25
 
26
  return (
27
  <div className="relative min-h-screen flex flex-col">
28
+ {/* Atmospheric background β€” fixed, behind everything */}
29
  <Aurora />
30
+
31
  <TopBar />
32
 
33
+ <div className="relative z-10 flex-1 flex overflow-hidden">
34
  <Sidebar />
35
+ <main
36
+ className={clsx(
37
+ "flex-1 flex flex-col min-w-0 transition-[margin] duration-300 ease-atelier",
38
+ sidebarOpen ? "md:ml-0" : "md:ml-0"
39
+ )}
40
+ >
41
+ {children}
42
+ </main>
43
  </div>
44
  </div>
45
  );
components/chat/Aurora.tsx CHANGED
@@ -1,34 +1,44 @@
1
  "use client";
2
 
3
  /**
4
- * Halftone Folio backdrop.
5
- * A printer's plate: corner register marks + bleed lines + halftone field.
6
- * Pure decoration. Pinned behind the page.
7
  */
8
  export function Aurora() {
9
  return (
10
- <>
11
- {/* Page-edge register marks (printer's bleed lines) */}
12
- <div
13
- aria-hidden
14
- className="fixed inset-0 z-0 pointer-events-none hidden md:block"
15
- >
16
- <Cross className="top-3 left-3" />
17
- <Cross className="top-3 right-3" />
18
- <Cross className="bottom-3 left-3" />
19
- <Cross className="bottom-3 right-3" />
20
- </div>
21
- </>
22
- );
23
- }
24
-
25
- function Cross({ className }: { className?: string }) {
26
- return (
27
- <span
28
- className={`absolute w-[14px] h-[14px] text-ink-30 font-mono leading-none ${className ?? ""}`}
29
- style={{ fontSize: "13px" }}
30
  >
31
- ✚
32
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  );
34
  }
 
1
  "use client";
2
 
3
  /**
4
+ * Atmospheric backdrop β€” three slow-drifting aurora orbs.
5
+ * Pure CSS, no JS animation. Pinned behind everything via fixed inset.
 
6
  */
7
  export function Aurora() {
8
  return (
9
+ <div
10
+ aria-hidden
11
+ className="fixed inset-0 z-0 overflow-hidden pointer-events-none"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  >
13
+ <div
14
+ className="aurora-amber animate-aurora-1"
15
+ style={{
16
+ width: "70vmax",
17
+ height: "70vmax",
18
+ top: "-25vmax",
19
+ right: "-20vmax",
20
+ }}
21
+ />
22
+ <div
23
+ className="aurora-violet animate-aurora-2"
24
+ style={{
25
+ width: "60vmax",
26
+ height: "60vmax",
27
+ bottom: "-20vmax",
28
+ left: "-15vmax",
29
+ }}
30
+ />
31
+ <div
32
+ className="aurora-teal animate-aurora-1"
33
+ style={{
34
+ width: "40vmax",
35
+ height: "40vmax",
36
+ top: "40vh",
37
+ left: "30vw",
38
+ opacity: 0.6,
39
+ animationDelay: "-8s",
40
+ }}
41
+ />
42
+ </div>
43
  );
44
  }
components/chat/Composer.tsx CHANGED
@@ -4,10 +4,6 @@ import { useEffect, useRef, useState } from "react";
4
  import clsx from "clsx";
5
  import { useChatStore } from "@/lib/chatStore";
6
 
7
- /**
8
- * Composer β€” the printer's submission box.
9
- * Heavy hairline frame, italic placeholder, ink-on-paper buttons.
10
- */
11
  export function Composer({
12
  onSubmit,
13
  disabled,
@@ -23,6 +19,7 @@ export function Composer({
23
  const ref = useRef<HTMLTextAreaElement | null>(null);
24
  const settings = useChatStore((s) => s.settings);
25
 
 
26
  useEffect(() => {
27
  const el = ref.current;
28
  if (!el) return;
@@ -44,99 +41,71 @@ export function Composer({
44
  }
45
  };
46
 
47
- const tunedDot = !isDefaults(settings);
48
-
49
  return (
50
- <div className="sticky bottom-0 z-20 px-4 sm:px-6 pt-3 pb-5 bg-gradient-to-t from-paper via-paper to-transparent">
51
- <div className="max-w-[1040px] mx-auto">
52
- <div className="grid grid-cols-[80px_1fr] gap-6">
53
- {/* Margin folio */}
54
- <div className="text-right pr-2 pt-3 hidden sm:block">
55
- <div className="folio-chrome">Submit</div>
56
- </div>
57
-
58
- {/* Composer body */}
59
- <div
60
- className="rounded-md transition-shadow duration-200"
61
- style={{
62
- background: "var(--paper)",
63
- boxShadow:
64
- "0 0 0 1.5px rgba(25,23,19,0.30), 0 6px 24px -8px rgba(25,23,19,0.18)",
65
- }}
66
- >
67
- <textarea
68
- ref={ref}
69
- value={value}
70
- onChange={(e) => setValue(e.target.value)}
71
- onKeyDown={onKey}
72
- disabled={disabled}
73
- rows={1}
74
- placeholder="Pose your question…"
75
- className={clsx(
76
- "w-full bg-transparent outline-none resize-none px-5 pt-4 pb-2 leading-relaxed scrollbar-none",
77
- "text-ink placeholder:italic placeholder:text-ink-50",
78
- "text-[18px]"
79
- )}
80
- style={{
81
- fontFamily: "var(--font-fraunces), ui-serif, Georgia, serif",
82
- fontWeight: 400,
83
- maxHeight: 200,
84
- }}
85
- aria-label="Question"
86
- />
87
 
88
- <div className="flex items-center justify-between px-3 pb-2.5 pt-1 border-t border-ink/10">
89
- <div className="flex items-center gap-1">
90
- <ToolButton
91
- onClick={onOpenSettings}
92
- label="Tune retrieval & generation"
93
- hasDot={tunedDot}
94
- icon={
95
- <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
96
- <circle
97
- cx="6.5"
98
- cy="6.5"
99
- r="2"
100
- stroke="currentColor"
101
- strokeWidth="1.4"
102
- />
103
- <path
104
- d="M6.5 1v2M6.5 10v2M1 6.5h2M10 6.5h2M2.6 2.6 4 4M9 9l1.4 1.4M2.6 10.4 4 9M9 4l1.4-1.4"
105
- stroke="currentColor"
106
- strokeWidth="1.3"
107
- strokeLinecap="round"
108
- />
109
- </svg>
110
- }
111
- >
112
- Edit settings
113
- </ToolButton>
114
- <ToolButton
115
- onClick={onClear}
116
- label="Clear transcript"
117
- icon={
118
- <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
119
- <path
120
- d="M2.5 3.5h8M5 3.5V2h3v1.5M3.5 3.5l.5 7.5h5l.5-7.5"
121
- stroke="currentColor"
122
- strokeWidth="1.3"
123
- strokeLinecap="round"
124
- strokeLinejoin="round"
125
- />
126
- </svg>
127
- }
128
- >
129
- Clear
130
- </ToolButton>
131
- <span className="folio-chrome ml-3 hidden md:inline">
132
- ⏎ submit Β· β‡§βŽ newline
133
- </span>
134
- </div>
135
- <SendButton
136
- onClick={submit}
137
- disabled={!value.trim() || disabled}
138
- />
139
  </div>
 
140
  </div>
141
  </div>
142
  </div>
@@ -148,29 +117,21 @@ function ToolButton({
148
  onClick,
149
  icon,
150
  label,
151
- hasDot,
152
  children,
153
  }: {
154
  onClick: () => void;
155
  icon: React.ReactNode;
156
  label: string;
157
- hasDot?: boolean;
158
  children?: React.ReactNode;
159
  }) {
160
  return (
161
  <button
162
  onClick={onClick}
163
  title={label}
164
- className="relative inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-ink-70 hover:text-ink hover:bg-ink-08 transition-colors"
165
  >
166
  {icon}
167
- <span className="folio-chrome hidden sm:inline">{children}</span>
168
- {hasDot && (
169
- <span
170
- className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-rust animate-ink-pulse"
171
- aria-hidden
172
- />
173
- )}
174
  </button>
175
  );
176
  }
@@ -186,21 +147,18 @@ function SendButton({
186
  <button
187
  onClick={onClick}
188
  disabled={disabled}
189
- aria-label="Submit"
190
  className={clsx(
191
- "shrink-0 inline-flex items-center gap-2 px-4 py-2 rounded-pill transition-all",
192
- "active:scale-[0.97]",
193
  disabled
194
- ? "bg-paper-deep text-ink-30 cursor-not-allowed"
195
- : "bg-ink text-paper hover:bg-rust hover:shadow-rust"
196
  )}
197
  >
198
- <span className="folio-chrome--ink folio-chrome" style={{ color: "inherit" }}>
199
- Submit
200
- </span>
201
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
202
  <path
203
- d="M2 5.5h7M5.5 2 9 5.5 5.5 9"
204
  stroke="currentColor"
205
  strokeWidth="1.6"
206
  strokeLinecap="round"
 
4
  import clsx from "clsx";
5
  import { useChatStore } from "@/lib/chatStore";
6
 
 
 
 
 
7
  export function Composer({
8
  onSubmit,
9
  disabled,
 
19
  const ref = useRef<HTMLTextAreaElement | null>(null);
20
  const settings = useChatStore((s) => s.settings);
21
 
22
+ // Autosize textarea
23
  useEffect(() => {
24
  const el = ref.current;
25
  if (!el) return;
 
41
  }
42
  };
43
 
 
 
44
  return (
45
+ <div className="sticky bottom-0 z-20 px-4 sm:px-6 pt-4 pb-5 bg-gradient-to-t from-canvas-deep via-canvas-deep/85 to-transparent">
46
+ <div className="max-w-[920px] mx-auto">
47
+ <div
48
+ className="relative rounded-2xl glass-strong overflow-hidden transition-all duration-200"
49
+ style={{ padding: "10px 12px" }}
50
+ >
51
+ <textarea
52
+ ref={ref}
53
+ value={value}
54
+ onChange={(e) => setValue(e.target.value)}
55
+ onKeyDown={onKey}
56
+ disabled={disabled}
57
+ rows={1}
58
+ placeholder="Ask anything about Etiya BSS…"
59
+ className="w-full bg-transparent outline-none resize-none px-3 py-2.5 text-body text-ink placeholder:text-ink-30 leading-relaxed scrollbar-none"
60
+ style={{ maxHeight: 200 }}
61
+ aria-label="Question"
62
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ {/* Action row */}
65
+ <div className="flex items-center justify-between mt-1.5 px-1">
66
+ <div className="flex items-center gap-1">
67
+ <ToolButton
68
+ onClick={onOpenSettings}
69
+ label="Tune retrieval & inference"
70
+ icon={
71
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
72
+ <circle cx="7" cy="7" r="2" stroke="currentColor" strokeWidth="1.4" />
73
+ <path
74
+ d="M7 1v2M7 11v2M1 7h2M11 7h2M2.8 2.8 4.2 4.2M9.8 9.8l1.4 1.4M2.8 11.2 4.2 9.8M9.8 4.2l1.4-1.4"
75
+ stroke="currentColor"
76
+ strokeWidth="1.3"
77
+ strokeLinecap="round"
78
+ />
79
+ </svg>
80
+ }
81
+ >
82
+ <span className="text-caption hidden sm:inline">Settings</span>
83
+ {!isDefaults(settings) && (
84
+ <span className="ml-1 inline-block w-1.5 h-1.5 rounded-full bg-amber animate-pulse-soft" />
85
+ )}
86
+ </ToolButton>
87
+ <ToolButton
88
+ onClick={onClear}
89
+ label="Clear conversation"
90
+ icon={
91
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
92
+ <path
93
+ d="M3 4h8M5.5 4V2.5h3V4M4 4l.5 8h5L10 4M6 6.5v3M8 6.5v3"
94
+ stroke="currentColor"
95
+ strokeWidth="1.3"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ />
99
+ </svg>
100
+ }
101
+ >
102
+ <span className="text-caption hidden sm:inline">Clear</span>
103
+ </ToolButton>
104
+ <span className="text-micro font-mono text-ink-30 ml-2 hidden md:inline">
105
+ ⏎ to send Β· β‡§βŽ for newline
106
+ </span>
 
 
 
 
 
 
 
 
107
  </div>
108
+ <SendButton onClick={submit} disabled={!value.trim() || disabled} />
109
  </div>
110
  </div>
111
  </div>
 
117
  onClick,
118
  icon,
119
  label,
 
120
  children,
121
  }: {
122
  onClick: () => void;
123
  icon: React.ReactNode;
124
  label: string;
 
125
  children?: React.ReactNode;
126
  }) {
127
  return (
128
  <button
129
  onClick={onClick}
130
  title={label}
131
+ className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-ink-50 hover:text-ink hover:bg-glass transition-colors"
132
  >
133
  {icon}
134
+ {children}
 
 
 
 
 
 
135
  </button>
136
  );
137
  }
 
147
  <button
148
  onClick={onClick}
149
  disabled={disabled}
150
+ aria-label="Send"
151
  className={clsx(
152
+ "shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-full transition-all",
153
+ "active:scale-[0.95]",
154
  disabled
155
+ ? "bg-glass text-ink-30 cursor-not-allowed"
156
+ : "bg-amber text-canvas hover:bg-amber-soft hover:shadow-amber"
157
  )}
158
  >
159
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
 
 
 
160
  <path
161
+ d="M2.5 7h9M7 2.5l4.5 4.5L7 11.5"
162
  stroke="currentColor"
163
  strokeWidth="1.6"
164
  strokeLinecap="round"
components/chat/Empty.tsx CHANGED
@@ -7,115 +7,85 @@ const SAMPLES = [
7
  "Can a category be localized in multiple languages?",
8
  ];
9
 
10
- /**
11
- * Empty state β€” magazine cover for a new transcript.
12
- */
13
  export function ChatEmpty({ onAsk }: { onAsk: (q: string) => void }) {
14
  return (
15
  <div className="relative flex-1 flex items-center justify-center px-6 py-12">
16
- <div className="max-w-[920px] w-full">
17
- {/* Top folio bar */}
18
- <div className="flex items-baseline justify-between mb-10 animate-fade-in">
19
- <span className="folio-chrome">Folio I Β· Transcript</span>
20
- <span className="folio-chrome">β„– 01</span>
21
- </div>
22
-
23
- {/* Headline β€” heavy editorial */}
24
- <h1 className="animate-stagger-1">
25
- <span
26
- className="block font-display text-ink"
27
- style={{
28
- fontSize: "clamp(56px, 11vw, 132px)",
29
- lineHeight: 0.92,
30
- fontWeight: 300,
31
- letterSpacing: "-0.04em",
32
- }}
33
- >
34
- Pose a
35
- </span>
36
- <span
37
- className="italic-display block text-rust"
38
  style={{
39
- fontSize: "clamp(64px, 13vw, 156px)",
40
- lineHeight: 0.88,
41
- fontWeight: 300,
42
- letterSpacing: "-0.045em",
43
- fontVariationSettings: '"SOFT" 100, "WONK" 1, "opsz" 144',
44
  }}
45
  >
46
- question.
47
- </span>
48
- </h1>
 
 
 
 
 
 
 
 
49
 
50
- <div className="hairline mt-10 mb-6 animate-rule" />
 
 
 
 
 
 
 
 
51
 
52
- {/* Lede paragraph (magazine deck style) */}
53
- <div className="grid md:grid-cols-[2fr_1fr] gap-10 items-start animate-stagger-2">
54
- <p
55
- className="text-ink"
56
- style={{
57
- fontFamily: "var(--font-fraunces)",
58
- fontSize: "22px",
59
- lineHeight: 1.45,
60
- fontWeight: 300,
61
- letterSpacing: "-0.012em",
62
- }}
63
- >
64
- A stateless retrieval-augmented atelier over{" "}
65
- <span className="italic-display text-rust">1,166</span>{" "}
66
- Etiya BSS documents. The hypernetwork re-tokenizes its source on
67
- every turn β€” no LoRA cache, no drift, only fresh thinking.
68
- </p>
69
- <aside className="folio-chrome--ink folio-chrome border-l border-ink/30 pl-4">
70
- <div className="mb-2">Methods</div>
71
- <ul className="space-y-1.5 list-none normal-case tracking-normal text-caption text-ink-70">
72
- <li>Β· K-means topic anchors</li>
73
- <li>Β· BM25 + dense (text-3-large)</li>
74
- <li>Β· BGE rerank Β· RRF fusion</li>
75
- <li>Β· Local refusal classifier</li>
76
- </ul>
77
- </aside>
78
  </div>
79
 
80
- <div className="hairline--double mt-10 mb-8 animate-rule-2" />
81
-
82
- {/* Sample interrogations */}
83
- <div className="animate-stagger-3">
84
- <div className="folio-chrome mb-4">Suggested entries</div>
85
- <ul className="grid sm:grid-cols-2 gap-x-8 gap-y-1">
86
- {SAMPLES.map((q, i) => (
87
- <li key={q}>
88
- <button
89
- onClick={() => onAsk(q)}
90
- className="group w-full text-left py-3 flex items-baseline gap-3
91
- border-b border-ink/12 hover:border-rust transition-colors"
92
- >
93
- <span className="folio-chrome shrink-0 w-7 group-hover:text-rust transition-colors">
94
- {String(i + 1).padStart(2, "0")}
95
- </span>
96
- <span
97
- className="flex-1 text-ink group-hover:italic transition-all"
98
- style={{
99
- fontFamily: "var(--font-fraunces)",
100
- fontSize: "17px",
101
- lineHeight: 1.4,
102
- fontWeight: 400,
103
- }}
104
- >
105
- {q}
106
- </span>
107
- <span
108
- className="text-rust opacity-0 group-hover:opacity-100 -translate-x-1 group-hover:translate-x-0 transition-all"
109
- aria-hidden
110
- >
111
- ↳
112
- </span>
113
- </button>
114
- </li>
115
- ))}
116
- </ul>
117
  </div>
118
  </div>
119
  </div>
120
  );
121
  }
 
 
 
 
 
 
 
 
 
 
 
7
  "Can a category be localized in multiple languages?",
8
  ];
9
 
 
 
 
10
  export function ChatEmpty({ onAsk }: { onAsk: (q: string) => void }) {
11
  return (
12
  <div className="relative flex-1 flex items-center justify-center px-6 py-12">
13
+ <div className="max-w-[760px] w-full text-center">
14
+ {/* Decorative orbital mark */}
15
+ <div className="flex justify-center mb-8 animate-fade-in">
16
+ <div
17
+ className="relative w-16 h-16 rounded-2xl flex items-center justify-center"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  style={{
19
+ background:
20
+ "linear-gradient(135deg, rgba(255,181,69,0.28), rgba(255,181,69,0.04))",
21
+ boxShadow:
22
+ "0 0 0 1px rgba(255,181,69,0.40), 0 0 64px -16px rgba(255,181,69,0.55)",
 
23
  }}
24
  >
25
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
26
+ <path
27
+ d="M14 3v4M14 21v4M3 14h4M21 14h4M6 6l3 3M19 19l3 3M6 22l3-3M19 9l3-3"
28
+ stroke="#ffb545"
29
+ strokeWidth="1.6"
30
+ strokeLinecap="round"
31
+ />
32
+ <circle cx="14" cy="14" r="3" fill="#ffb545" />
33
+ </svg>
34
+ </div>
35
+ </div>
36
 
37
+ {/* Editorial headline β€” sans + serif italic mix */}
38
+ <h1 className="text-display-xl text-ink animate-stagger-1 text-balance">
39
+ Ask <span className="serif-italic text-amber">anything.</span>
40
+ </h1>
41
+ <p className="text-lead text-ink-70 mt-5 animate-stagger-2 text-balance">
42
+ A stateless retrieval-augmented atelier over{" "}
43
+ <span className="font-mono text-ink">1,166</span> Etiya BSS documents,
44
+ re-tokenizing every turn β€” no cache, no drift.
45
+ </p>
46
 
47
+ {/* Sample chips */}
48
+ <div className="mt-12 grid sm:grid-cols-2 gap-2.5 animate-stagger-3">
49
+ {SAMPLES.map((q) => (
50
+ <button
51
+ key={q}
52
+ onClick={() => onAsk(q)}
53
+ className="group text-left rounded-md p-4 transition-all
54
+ bg-glass hover:bg-glass-stronger
55
+ hover:shadow-amber/30"
56
+ style={{ boxShadow: "0 0 0 1px rgba(255,255,255,0.08)" }}
57
+ >
58
+ <div className="flex items-start gap-3">
59
+ <span className="shrink-0 mt-1 w-1 h-1 rounded-full bg-amber" />
60
+ <span className="text-body text-ink-70 group-hover:text-ink transition-colors">
61
+ {q}
62
+ </span>
63
+ </div>
64
+ </button>
65
+ ))}
 
 
 
 
 
 
 
66
  </div>
67
 
68
+ {/* Capability strip */}
69
+ <div className="mt-14 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-micro uppercase tracking-[0.18em] text-ink-50 font-mono animate-stagger-4">
70
+ <Cap label="triple-gate retrieval" />
71
+ <Dot />
72
+ <Cap label="bge rerank" />
73
+ <Dot />
74
+ <Cap label="kmeans anchors" />
75
+ <Dot />
76
+ <Cap label="zero hardcoded text" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  </div>
78
  </div>
79
  </div>
80
  );
81
  }
82
+
83
+ function Cap({ label }: { label: string }) {
84
+ return <span>{label}</span>;
85
+ }
86
+
87
+ function Dot() {
88
+ return (
89
+ <span className="inline-block w-1 h-1 rounded-full bg-ink-30" aria-hidden />
90
+ );
91
+ }
components/chat/GroundingPill.tsx CHANGED
@@ -20,7 +20,7 @@ const META: Record<
20
  ungrounded: {
21
  label: "Ungrounded",
22
  tone: "warn",
23
- hint: "Answer was generated but doesn't strongly match the source.",
24
  },
25
  rejected_low_similarity: {
26
  label: "No source",
@@ -40,21 +40,27 @@ export function GroundingPill({ status }: { status: GroundingStatus }) {
40
  tone: "neutral",
41
  hint: "",
42
  };
43
- const cls = {
44
- ok: "text-status-ok ring-status-ok/40 bg-status-ok-glow",
45
- warn: "text-status-warn ring-status-warn/40 bg-status-warn-glow",
46
- err: "text-status-err ring-status-err/40 bg-status-err-glow",
47
- neutral: "text-ink-70 ring-ink/24 bg-paper-deep",
 
 
 
 
 
 
 
48
  }[meta.tone];
49
 
50
  return (
51
  <span
52
  title={meta.hint}
53
  className={clsx(
54
- "inline-flex items-center gap-1.5 rounded-pill px-2.5 py-0.5",
55
- "folio-chrome",
56
- "ring-1",
57
- cls
58
  )}
59
  >
60
  <Dot tone={meta.tone} />
@@ -66,8 +72,8 @@ export function GroundingPill({ status }: { status: GroundingStatus }) {
66
  function Dot({ tone }: { tone: "ok" | "warn" | "err" | "neutral" }) {
67
  const cls = {
68
  ok: "bg-status-ok",
69
- warn: "bg-status-warn",
70
- err: "bg-rust",
71
  neutral: "bg-ink-50",
72
  }[tone];
73
  return <span className={clsx("inline-block w-1.5 h-1.5 rounded-full", cls)} />;
 
20
  ungrounded: {
21
  label: "Ungrounded",
22
  tone: "warn",
23
+ hint: "Answer was generated but doesn't strongly match the source. Treat with caution.",
24
  },
25
  rejected_low_similarity: {
26
  label: "No source",
 
40
  tone: "neutral",
41
  hint: "",
42
  };
43
+ const toneCls = {
44
+ ok: "bg-status-ok-glow text-status-ok",
45
+ warn: "bg-status-warn-glow text-amber",
46
+ err: "bg-status-err-glow text-status-err",
47
+ neutral: "bg-glass text-ink-70",
48
+ }[meta.tone];
49
+
50
+ const ringCls = {
51
+ ok: "ring-1 ring-status-ok/30",
52
+ warn: "ring-1 ring-amber/35",
53
+ err: "ring-1 ring-status-err/30",
54
+ neutral: "ring-1 ring-glass-border",
55
  }[meta.tone];
56
 
57
  return (
58
  <span
59
  title={meta.hint}
60
  className={clsx(
61
+ "inline-flex items-center gap-1.5 rounded-pill px-2.5 py-1 text-micro uppercase tracking-[0.12em] font-mono",
62
+ toneCls,
63
+ ringCls
 
64
  )}
65
  >
66
  <Dot tone={meta.tone} />
 
72
  function Dot({ tone }: { tone: "ok" | "warn" | "err" | "neutral" }) {
73
  const cls = {
74
  ok: "bg-status-ok",
75
+ warn: "bg-amber",
76
+ err: "bg-status-err",
77
  neutral: "bg-ink-50",
78
  }[tone];
79
  return <span className={clsx("inline-block w-1.5 h-1.5 rounded-full", cls)} />;
components/chat/Message.tsx CHANGED
@@ -6,50 +6,25 @@ import { SourceChips } from "./SourceChips";
6
  import { useState } from "react";
7
  import clsx from "clsx";
8
 
9
- /**
10
- * Editorial transcript layout:
11
- * [margin folio] | [content column]
12
- *
13
- * User questions: italic Fraunces ledes, hairline rule beneath.
14
- * Assistant answers: drop-cap first letter, IBM Plex body, bibliography below,
15
- * spec sheet typeset like a magazine colophon.
16
- */
17
-
18
- export function UserMessage({ text, index }: { text: string; index: number }) {
19
  return (
20
- <article className="grid grid-cols-[80px_1fr] gap-6 animate-fade-up">
21
- <div className="text-right pr-2 pt-1">
22
- <div className="folio-chrome">β„– {String(index + 1).padStart(2, "0")} Β· Q</div>
23
  </div>
24
- <div className="border-t border-ink/16 pt-3">
25
- <span className="folio-chrome--ink folio-chrome">Question</span>
26
- <p
27
- className="italic-display text-ink mt-1.5"
28
- style={{
29
- fontSize: "clamp(22px, 2.6vw, 30px)",
30
- lineHeight: 1.25,
31
- letterSpacing: "-0.018em",
32
- fontWeight: 400,
33
- fontVariationSettings: '"SOFT" 30, "WONK" 1',
34
- }}
35
- >
36
- {text}
37
- </p>
38
  </div>
39
- </article>
40
  );
41
  }
42
 
43
- export function AssistantMessage({
44
- turn,
45
- index,
46
- }: {
47
- turn: ChatTurn;
48
- index: number;
49
- }) {
50
  const r = turn.response;
51
- const [copied, setCopied] = useState(false);
52
  if (!r) return null;
 
53
 
54
  const copy = async () => {
55
  try {
@@ -60,82 +35,40 @@ export function AssistantMessage({
60
  };
61
 
62
  return (
63
- <article className="grid grid-cols-[80px_1fr] gap-6 animate-fade-up">
64
- {/* Margin folio */}
65
- <div className="text-right pr-2 pt-1">
66
- <div className="folio-chrome">
67
- β„– {String(index + 1).padStart(2, "0")} Β· A
68
- </div>
69
- <div className="mt-3 folio-chrome text-rust">
70
- {r.total_seconds.toFixed(2)}s
71
- </div>
72
- </div>
73
-
74
- {/* Body column */}
75
- <div className="border-t border-ink/16 pt-3">
76
- {/* Banner */}
77
- <div className="flex items-baseline justify-between mb-3 flex-wrap gap-2">
78
- <div className="flex items-baseline gap-3">
79
- <span className="folio-chrome--ink folio-chrome">Answer</span>
80
- <GroundingPill status={r._grounding_status} />
 
 
 
 
 
 
 
 
 
 
 
81
  </div>
82
- <button
83
- onClick={copy}
84
- className="folio-chrome hover:text-rust transition-colors flex items-center gap-1.5"
85
- title="Copy answer"
86
- >
87
- {copied ? (
88
- <>
89
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
90
- <path
91
- d="M2 6 4.5 8.5 9 3"
92
- stroke="currentColor"
93
- strokeWidth="1.6"
94
- strokeLinecap="round"
95
- />
96
- </svg>
97
- Copied
98
- </>
99
- ) : (
100
- <>
101
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
102
- <rect
103
- x="2"
104
- y="2"
105
- width="6"
106
- height="6"
107
- rx="1"
108
- stroke="currentColor"
109
- strokeWidth="1.2"
110
- />
111
- <rect
112
- x="3.5"
113
- y="3.5"
114
- width="6"
115
- height="6"
116
- rx="1"
117
- stroke="currentColor"
118
- strokeWidth="1.2"
119
- />
120
- </svg>
121
- Copy
122
- </>
123
- )}
124
- </button>
125
- </div>
126
 
127
- {/* Answer body β€” drop-cap, generous reading width */}
128
- <div
129
- className="drop-cap text-ink"
130
- style={{
131
- fontFamily: "var(--font-fraunces)",
132
- fontSize: "20px",
133
- lineHeight: 1.55,
134
- fontWeight: 400,
135
- maxWidth: "70ch",
136
- whiteSpace: "pre-wrap",
137
- }}
138
- >
139
  {r.answer}
140
  </div>
141
 
@@ -143,85 +76,98 @@ export function AssistantMessage({
143
  {r.source_docs?.length > 0 && <SourceChips docs={r.source_docs} />}
144
 
145
  {/* Spec sheet */}
146
- <footer className="mt-6 pt-3 border-t border-ink/16">
147
- <div className="folio-chrome mb-2">Colophon</div>
148
- <div className="grid grid-cols-3 sm:grid-cols-6 gap-x-6 gap-y-3">
149
- <Spec label="topΒ·sim" value={r._top_similarity?.toFixed(3) ?? "β€”"} />
150
- <Spec label="rerank" value={r._top_rerank_score?.toFixed(2) ?? "β€”"} />
151
- <Spec label="anchor" value={r._anchor_score?.toFixed(3) ?? "β€”"} />
152
- <Spec
153
- label="retrieve"
154
- value={`${r.retrieve_seconds.toFixed(2)}s`}
155
- />
156
- <Spec
157
- label="inference"
158
- value={`${r.inference_seconds.toFixed(2)}s`}
159
- />
160
- <Spec label="total" value={`${r.total_seconds.toFixed(2)}s`} accent />
161
- </div>
 
162
  </footer>
163
- </div>
164
- </article>
165
  );
166
  }
167
 
168
- export function ErrorMessage({
169
- turn,
170
- index,
171
- }: {
172
- turn: ChatTurn;
173
- index: number;
174
- }) {
175
  const e = turn.error;
176
  if (!e) return null;
177
  return (
178
- <article className="grid grid-cols-[80px_1fr] gap-6 animate-fade-up">
179
- <div className="text-right pr-2 pt-1">
180
- <div className="folio-chrome text-rust">Errata</div>
181
- <div className="mt-2 folio-chrome">β„– {String(index + 1).padStart(2, "0")}</div>
182
- </div>
183
- <div className="border-t border-rust pt-3">
184
- <span className="folio-chrome--rust folio-chrome">
185
- HTTP {e.status || "β€”"}
186
- </span>
187
- <p
188
- className="italic-display text-ink mt-1.5"
189
- style={{ fontSize: "20px", lineHeight: 1.3 }}
190
- >
191
- {e.message}
192
- </p>
193
  {e.status === 502 && (
194
- <p className="text-caption text-ink-70 mt-3 max-w-prose">
195
- The upstream Space appears to be sleeping or building. Try again in
196
- a moment.
197
- </p>
198
  )}
199
- </div>
200
- </article>
201
  );
202
  }
203
 
204
  function Spec({
205
  label,
206
  value,
207
- accent,
208
  }: {
209
  label: string;
210
  value: string;
211
- accent?: boolean;
212
  }) {
213
  return (
214
- <div>
215
- <div className="folio-chrome">{label}</div>
216
- <div
217
- className={clsx(
218
- "font-mono mt-0.5",
219
- accent ? "text-rust" : "text-ink"
220
- )}
221
- style={{ fontSize: "15px", fontWeight: 500 }}
222
- >
223
  {value}
224
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  </div>
226
  );
227
  }
 
6
  import { useState } from "react";
7
  import clsx from "clsx";
8
 
9
+ export function UserMessage({ text }: { text: string }) {
 
 
 
 
 
 
 
 
 
10
  return (
11
+ <div className="flex items-start gap-3 justify-end animate-fade-up">
12
+ <div className="max-w-[80%] chat-bubble-user">
13
+ <span className="whitespace-pre-wrap">{text}</span>
14
  </div>
15
+ <div className="shrink-0 w-7 h-7 rounded-md bg-glass flex items-center justify-center text-micro font-mono text-ink-70 mt-1"
16
+ style={{ boxShadow: "0 0 0 1px rgba(255,255,255,0.10)" }}
17
+ >
18
+ U
 
 
 
 
 
 
 
 
 
 
19
  </div>
20
+ </div>
21
  );
22
  }
23
 
24
+ export function AssistantMessage({ turn }: { turn: ChatTurn }) {
 
 
 
 
 
 
25
  const r = turn.response;
 
26
  if (!r) return null;
27
+ const [copied, setCopied] = useState(false);
28
 
29
  const copy = async () => {
30
  try {
 
35
  };
36
 
37
  return (
38
+ <div className="flex items-start gap-3 animate-fade-up">
39
+ <Avatar />
40
+ <article className="chat-bubble-assistant flex-1 min-w-0 max-w-[88%]">
41
+ {/* Header: grounding + actions */}
42
+ <header className="flex items-center justify-between gap-3 mb-3">
43
+ <GroundingPill status={r._grounding_status} />
44
+ <div className="flex items-center gap-1">
45
+ <button
46
+ onClick={copy}
47
+ className="text-micro text-ink-50 hover:text-ink hover:bg-glass rounded-md px-2 py-1 transition-colors flex items-center gap-1"
48
+ title="Copy answer"
49
+ >
50
+ {copied ? (
51
+ <>
52
+ <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
53
+ <path d="M2 6 4.5 8.5 9 3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
54
+ </svg>
55
+ copied
56
+ </>
57
+ ) : (
58
+ <>
59
+ <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
60
+ <rect x="2" y="2" width="6" height="6" rx="1" stroke="currentColor" strokeWidth="1.2"/>
61
+ <rect x="3.5" y="3.5" width="6" height="6" rx="1" stroke="currentColor" strokeWidth="1.2"/>
62
+ </svg>
63
+ copy
64
+ </>
65
+ )}
66
+ </button>
67
  </div>
68
+ </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ {/* Answer */}
71
+ <div className="text-ink leading-[1.65] whitespace-pre-wrap">
 
 
 
 
 
 
 
 
 
 
72
  {r.answer}
73
  </div>
74
 
 
76
  {r.source_docs?.length > 0 && <SourceChips docs={r.source_docs} />}
77
 
78
  {/* Spec sheet */}
79
+ <footer className="mt-4 pt-3 border-t border-glass-border flex flex-wrap items-center gap-x-5 gap-y-1.5 text-micro text-ink-50 font-mono uppercase tracking-[0.10em]">
80
+ <Spec label="topΒ·sim" value={r._top_similarity?.toFixed(3) ?? "β€”"} />
81
+ <Spec label="rerank" value={r._top_rerank_score?.toFixed(2) ?? "β€”"} />
82
+ <Spec label="anchor" value={r._anchor_score?.toFixed(3) ?? "β€”"} />
83
+ <Spec
84
+ label="retrieve"
85
+ value={`${r.retrieve_seconds.toFixed(2)}s`}
86
+ />
87
+ <Spec
88
+ label="inference"
89
+ value={`${r.inference_seconds.toFixed(2)}s`}
90
+ />
91
+ <Spec
92
+ label="total"
93
+ value={`${r.total_seconds.toFixed(2)}s`}
94
+ highlight
95
+ />
96
  </footer>
97
+ </article>
98
+ </div>
99
  );
100
  }
101
 
102
+ export function ErrorMessage({ turn }: { turn: ChatTurn }) {
 
 
 
 
 
 
103
  const e = turn.error;
104
  if (!e) return null;
105
  return (
106
+ <div className="flex items-start gap-3 animate-fade-up">
107
+ <Avatar tone="err" />
108
+ <article
109
+ className="flex-1 min-w-0 max-w-[88%] rounded-2xl rounded-tl-sm px-5 py-4"
110
+ style={{
111
+ background: "rgba(255, 122, 122, 0.06)",
112
+ boxShadow: "0 0 0 1px rgba(255, 122, 122, 0.28)",
113
+ }}
114
+ >
115
+ <div className="text-micro uppercase tracking-[0.12em] text-status-err font-mono">
116
+ error Β· http {e.status || "β€”"}
117
+ </div>
118
+ <div className="text-body-strong text-ink mt-1.5">{e.message}</div>
 
 
119
  {e.status === 502 && (
120
+ <div className="text-caption text-ink-70 mt-2">
121
+ Backend not reachable. Check the HF Space stage.
122
+ </div>
 
123
  )}
124
+ </article>
125
+ </div>
126
  );
127
  }
128
 
129
  function Spec({
130
  label,
131
  value,
132
+ highlight,
133
  }: {
134
  label: string;
135
  value: string;
136
+ highlight?: boolean;
137
  }) {
138
  return (
139
+ <span className="inline-flex items-baseline gap-1">
140
+ <span>{label}</span>
141
+ <span className={clsx(highlight ? "text-amber" : "text-ink-70")}>
 
 
 
 
 
 
142
  {value}
143
+ </span>
144
+ </span>
145
+ );
146
+ }
147
+
148
+ function Avatar({ tone = "amber" }: { tone?: "amber" | "err" }) {
149
+ const bg =
150
+ tone === "err"
151
+ ? "linear-gradient(135deg, rgba(255,122,122,0.95), rgba(195,70,70,1))"
152
+ : "linear-gradient(135deg, rgba(255,181,69,0.95), rgba(214,138,31,1))";
153
+ const ring =
154
+ tone === "err"
155
+ ? "0 0 0 1px rgba(255,122,122,0.45), 0 0 14px -2px rgba(255,122,122,0.4)"
156
+ : "0 0 0 1px rgba(255,181,69,0.45), 0 0 14px -2px rgba(255,181,69,0.45)";
157
+ return (
158
+ <div
159
+ className="shrink-0 w-7 h-7 rounded-md flex items-center justify-center mt-1"
160
+ style={{ background: bg, boxShadow: ring }}
161
+ >
162
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
163
+ <path
164
+ d="M2.5 7.5 4.8 9.8l6-6.6"
165
+ stroke="#0a0b10"
166
+ strokeWidth="1.8"
167
+ strokeLinecap="round"
168
+ strokeLinejoin="round"
169
+ />
170
+ </svg>
171
  </div>
172
  );
173
  }
components/chat/SettingsDrawer.tsx CHANGED
@@ -14,15 +14,78 @@ type ParamMeta = {
14
  };
15
 
16
  const PARAMS: ParamMeta[] = [
17
- { key: "top_k", label: "Top-K retrieval", desc: "How many documents surface as context. 1 = single best; 3 = blend.", min: 1, max: 3, step: 1 },
18
- { key: "max_new_tokens", label: "Max new tokens", desc: "Generation cap. >200 risks repetition under greedy decoding.", min: 32, max: 512, step: 8 },
19
- { key: "similarity_threshold", label: "Dense similarity gate", desc: "Reject if dense cosine top-1 below this. Lower = more permissive.", min: 0, max: 1, step: 0.01 },
20
- { key: "rerank_threshold", label: "BGE rerank gate", desc: "Reject if cross-encoder score below this. Higher = stricter.", min: -2, max: 5, step: 0.05 },
21
- { key: "anchor_threshold", label: "Anchor (topic) gate", desc: "Reject if question is far from every K-means topic centroid.", min: 0, max: 1, step: 0.01 },
22
- { key: "scaler", label: "doc-to-lora scaler", desc: "LoRA intensity. 0=ignore doc Β· 1=normal Β· >1 amplify Β· <0 invert.", min: -2, max: 2, step: 0.05 },
23
- { key: "bias_scaler", label: "Bias scaler", desc: "Bias-side LoRA intensity. Usually 1.0.", min: -2, max: 2, step: 0.05 },
24
- { key: "repetition_penalty", label: "Repetition penalty", desc: "1.0 = off. 1.1–1.2 cuts loops without hurting fluency.", min: 1, max: 2, step: 0.05 },
25
- { key: "no_repeat_ngram_size", label: "No-repeat n-gram", desc: "Forbid same n-gram twice. 0=off. 4=cuts most loops.", min: 0, max: 8, step: 1 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  ];
27
 
28
  export function SettingsDrawer({
@@ -36,6 +99,7 @@ export function SettingsDrawer({
36
  const setSettings = useChatStore((s) => s.setSettings);
37
  const reset = useChatStore((s) => s.resetSettings);
38
 
 
39
  useEffect(() => {
40
  if (!open) return;
41
  const onKey = (e: KeyboardEvent) => {
@@ -47,65 +111,52 @@ export function SettingsDrawer({
47
 
48
  return (
49
  <>
 
50
  <div
51
  className={clsx(
52
- "fixed inset-0 z-40 transition-opacity duration-300",
53
  open ? "opacity-100" : "opacity-0 pointer-events-none"
54
  )}
55
- style={{ background: "rgba(25,23,19,0.20)" }}
56
  onClick={onClose}
57
  aria-hidden
58
  />
 
59
  <aside
60
  className={clsx(
61
- "fixed top-0 right-0 z-50 h-screen w-full sm:w-[480px]",
62
  "flex flex-col transition-transform duration-300 ease-atelier",
63
  open ? "translate-x-0" : "translate-x-full"
64
  )}
65
- style={{
66
- background: "var(--paper-deep)",
67
- boxShadow: "-12px 0 48px -12px rgba(25,23,19,0.20), -1px 0 0 rgba(25,23,19,0.20)",
68
- }}
69
  role="dialog"
70
  aria-modal="true"
71
  aria-label="Inference settings"
72
  >
73
- <header className="px-7 pt-7 pb-5 border-b border-ink/16">
74
- <div className="flex items-baseline justify-between mb-2">
75
- <span className="folio-chrome">Workshop Β· Settings</span>
76
- <button
77
- onClick={onClose}
78
- className="p-1.5 -mr-1.5 rounded-md text-ink-50 hover:text-ink hover:bg-ink-08 transition-colors"
79
- aria-label="Close"
80
- >
81
- <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
82
- <path
83
- d="m3 3 7 7M10 3l-7 7"
84
- stroke="currentColor"
85
- strokeWidth="1.5"
86
- strokeLinecap="round"
87
- />
88
- </svg>
89
- </button>
90
  </div>
91
- <h2
92
- className="italic-display text-ink"
93
- style={{ fontSize: "32px", lineHeight: 1.15 }}
 
94
  >
95
- Tune the <span className="text-rust">inference.</span>
96
- </h2>
97
- <p className="text-caption text-ink-70 mt-2 max-w-[36ch]">
98
- Adjust retrieval gates and generation behaviour. Changes take
99
- effect on the next submitted question.
100
- </p>
 
 
 
101
  </header>
102
 
103
- <div className="flex-1 overflow-y-auto px-7 py-6 space-y-6">
104
- {/* Grounding toggle */}
105
- <label
106
- className="flex items-start gap-3 p-3 -mx-1 rounded-md cursor-pointer hover:bg-ink-08 transition-colors"
107
- style={{ boxShadow: "0 0 0 1px rgba(25,23,19,0.12)" }}
108
- >
109
  <Toggle
110
  checked={settings.use_grounding}
111
  onChange={(v) => setSettings({ ...settings, use_grounding: v })}
@@ -114,7 +165,7 @@ export function SettingsDrawer({
114
  <div className="text-body-strong text-ink">
115
  Wrap with grounding instruction
116
  </div>
117
- <div className="text-caption text-ink-70 mt-0.5">
118
  Forces the model to refuse if context is insufficient.
119
  </div>
120
  </div>
@@ -132,10 +183,10 @@ export function SettingsDrawer({
132
  ))}
133
  </div>
134
 
135
- <footer className="px-7 py-4 border-t border-ink/16 flex items-center justify-between">
136
  <button
137
  onClick={reset}
138
- className="folio-chrome flex items-center gap-1.5 hover:text-rust transition-colors"
139
  >
140
  <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
141
  <path
@@ -148,7 +199,7 @@ export function SettingsDrawer({
148
  </svg>
149
  Reset to defaults
150
  </button>
151
- <button onClick={onClose} className="btn-primary">
152
  Done
153
  </button>
154
  </footer>
@@ -166,21 +217,22 @@ function ParamSlider({
166
  value: number;
167
  onChange: (v: number) => void;
168
  }) {
169
- const isDefault = (DEFAULT_SETTINGS[meta.key] as number) === value;
170
- const display =
 
171
  meta.step < 1 ? value.toFixed(2) : Math.round(value).toString();
172
 
173
  return (
174
  <div>
175
- <div className="flex items-baseline justify-between mb-1.5">
176
  <label className="text-body-strong text-ink">{meta.label}</label>
177
  <span
178
  className={clsx(
179
- "font-mono text-caption",
180
- isDefault ? "text-ink-50" : "text-rust"
181
  )}
182
  >
183
- {display}
184
  </span>
185
  </div>
186
  <input
@@ -190,10 +242,10 @@ function ParamSlider({
190
  step={meta.step}
191
  value={value}
192
  onChange={(e) => onChange(parseFloat(e.target.value))}
193
- className="w-full accent-rust"
194
  aria-label={meta.label}
195
  />
196
- <p className="text-caption text-ink-70 mt-1.5">{meta.desc}</p>
197
  </div>
198
  );
199
  }
@@ -213,16 +265,15 @@ function Toggle({
213
  onClick={() => onChange(!checked)}
214
  className={clsx(
215
  "relative w-9 h-5 rounded-pill transition-colors duration-200 shrink-0 mt-0.5",
216
- checked ? "bg-rust" : "bg-paper-edge"
217
  )}
218
- style={{ boxShadow: "inset 0 0 0 1px rgba(25,23,19,0.18)" }}
219
  >
220
  <span
221
  className={clsx(
222
- "absolute top-0.5 w-4 h-4 rounded-full bg-paper transition-transform duration-200",
223
  checked ? "translate-x-[18px]" : "translate-x-0.5"
224
  )}
225
- style={{ boxShadow: "0 1px 2px rgba(25,23,19,0.25)" }}
226
  />
227
  </button>
228
  );
 
14
  };
15
 
16
  const PARAMS: ParamMeta[] = [
17
+ {
18
+ key: "top_k",
19
+ label: "Top-K retrieval",
20
+ desc: "How many docs surface as context. 1 = single best; 3 = blend.",
21
+ min: 1,
22
+ max: 3,
23
+ step: 1,
24
+ },
25
+ {
26
+ key: "max_new_tokens",
27
+ label: "Max new tokens",
28
+ desc: "Generation cap. >200 risks repetition under greedy decoding.",
29
+ min: 32,
30
+ max: 512,
31
+ step: 8,
32
+ },
33
+ {
34
+ key: "similarity_threshold",
35
+ label: "Dense similarity gate",
36
+ desc: "Reject if dense cosine top-1 below this. Lower = more permissive.",
37
+ min: 0,
38
+ max: 1,
39
+ step: 0.01,
40
+ },
41
+ {
42
+ key: "rerank_threshold",
43
+ label: "BGE rerank gate",
44
+ desc: "Reject if BGE cross-encoder score below this. Higher = stricter.",
45
+ min: -2,
46
+ max: 5,
47
+ step: 0.05,
48
+ },
49
+ {
50
+ key: "anchor_threshold",
51
+ label: "Anchor (topic) gate",
52
+ desc: "Reject if question is far from every K-means topic centroid.",
53
+ min: 0,
54
+ max: 1,
55
+ step: 0.01,
56
+ },
57
+ {
58
+ key: "scaler",
59
+ label: "doc-to-lora scaler",
60
+ desc: "LoRA intensity. 0=ignore doc Β· 1=normal Β· >1 amplify Β· <0 invert.",
61
+ min: -2,
62
+ max: 2,
63
+ step: 0.05,
64
+ },
65
+ {
66
+ key: "bias_scaler",
67
+ label: "Bias scaler",
68
+ desc: "Bias-side LoRA intensity. Usually 1.0.",
69
+ min: -2,
70
+ max: 2,
71
+ step: 0.05,
72
+ },
73
+ {
74
+ key: "repetition_penalty",
75
+ label: "Repetition penalty",
76
+ desc: "1.0 = off. 1.1–1.2 cuts loops without hurting fluency.",
77
+ min: 1,
78
+ max: 2,
79
+ step: 0.05,
80
+ },
81
+ {
82
+ key: "no_repeat_ngram_size",
83
+ label: "No-repeat n-gram",
84
+ desc: "Forbid same n-gram twice. 0=off. 4=cuts most loops.",
85
+ min: 0,
86
+ max: 8,
87
+ step: 1,
88
+ },
89
  ];
90
 
91
  export function SettingsDrawer({
 
99
  const setSettings = useChatStore((s) => s.setSettings);
100
  const reset = useChatStore((s) => s.resetSettings);
101
 
102
+ // ESC to close
103
  useEffect(() => {
104
  if (!open) return;
105
  const onKey = (e: KeyboardEvent) => {
 
111
 
112
  return (
113
  <>
114
+ {/* Scrim */}
115
  <div
116
  className={clsx(
117
+ "fixed inset-0 z-40 bg-canvas-deep/60 backdrop-blur-sm transition-opacity duration-300",
118
  open ? "opacity-100" : "opacity-0 pointer-events-none"
119
  )}
 
120
  onClick={onClose}
121
  aria-hidden
122
  />
123
+ {/* Panel */}
124
  <aside
125
  className={clsx(
126
+ "fixed top-0 right-0 z-50 h-screen w-full sm:w-[460px] glass-strong",
127
  "flex flex-col transition-transform duration-300 ease-atelier",
128
  open ? "translate-x-0" : "translate-x-full"
129
  )}
 
 
 
 
130
  role="dialog"
131
  aria-modal="true"
132
  aria-label="Inference settings"
133
  >
134
+ <header className="px-6 py-5 flex items-center justify-between border-b border-glass-border">
135
+ <div>
136
+ <div className="text-display-sm">Inference settings</div>
137
+ <div className="text-caption text-ink-50 mt-0.5">
138
+ Tune retrieval gates and generation in real time.
139
+ </div>
 
 
 
 
 
 
 
 
 
 
 
140
  </div>
141
+ <button
142
+ onClick={onClose}
143
+ className="p-2 rounded-md text-ink-50 hover:text-ink hover:bg-glass transition-colors"
144
+ aria-label="Close settings"
145
  >
146
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
147
+ <path
148
+ d="m3 3 8 8M11 3l-8 8"
149
+ stroke="currentColor"
150
+ strokeWidth="1.5"
151
+ strokeLinecap="round"
152
+ />
153
+ </svg>
154
+ </button>
155
  </header>
156
 
157
+ <div className="flex-1 overflow-y-auto px-6 py-5 space-y-5">
158
+ {/* use_grounding toggle */}
159
+ <label className="flex items-start gap-3 p-3 rounded-md bg-glass cursor-pointer hover:bg-glass-stronger transition-colors">
 
 
 
160
  <Toggle
161
  checked={settings.use_grounding}
162
  onChange={(v) => setSettings({ ...settings, use_grounding: v })}
 
165
  <div className="text-body-strong text-ink">
166
  Wrap with grounding instruction
167
  </div>
168
+ <div className="text-caption text-ink-50 mt-0.5">
169
  Forces the model to refuse if context is insufficient.
170
  </div>
171
  </div>
 
183
  ))}
184
  </div>
185
 
186
+ <footer className="px-6 py-4 border-t border-glass-border flex items-center justify-between">
187
  <button
188
  onClick={reset}
189
+ className="text-caption text-ink-70 hover:text-ink transition-colors flex items-center gap-1.5"
190
  >
191
  <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
192
  <path
 
199
  </svg>
200
  Reset to defaults
201
  </button>
202
+ <button onClick={onClose} className="btn-primary text-caption">
203
  Done
204
  </button>
205
  </footer>
 
217
  value: number;
218
  onChange: (v: number) => void;
219
  }) {
220
+ const isDefault =
221
+ (DEFAULT_SETTINGS[meta.key] as number) === value;
222
+ const displayValue =
223
  meta.step < 1 ? value.toFixed(2) : Math.round(value).toString();
224
 
225
  return (
226
  <div>
227
+ <div className="flex items-center justify-between mb-1.5">
228
  <label className="text-body-strong text-ink">{meta.label}</label>
229
  <span
230
  className={clsx(
231
+ "text-caption font-mono",
232
+ isDefault ? "text-ink-50" : "text-amber"
233
  )}
234
  >
235
+ {displayValue}
236
  </span>
237
  </div>
238
  <input
 
242
  step={meta.step}
243
  value={value}
244
  onChange={(e) => onChange(parseFloat(e.target.value))}
245
+ className="w-full accent-amber"
246
  aria-label={meta.label}
247
  />
248
+ <p className="text-micro text-ink-50 mt-1.5">{meta.desc}</p>
249
  </div>
250
  );
251
  }
 
265
  onClick={() => onChange(!checked)}
266
  className={clsx(
267
  "relative w-9 h-5 rounded-pill transition-colors duration-200 shrink-0 mt-0.5",
268
+ checked ? "bg-amber" : "bg-glass-stronger"
269
  )}
270
+ style={{ boxShadow: "0 0 0 1px rgba(255,255,255,0.10)" }}
271
  >
272
  <span
273
  className={clsx(
274
+ "absolute top-0.5 w-4 h-4 rounded-full bg-canvas-deep transition-transform duration-200",
275
  checked ? "translate-x-[18px]" : "translate-x-0.5"
276
  )}
 
277
  />
278
  </button>
279
  );
components/chat/Sidebar.tsx CHANGED
@@ -5,10 +5,6 @@ import { usePathname } from "next/navigation";
5
  import { useChatStore } from "@/lib/chatStore";
6
  import clsx from "clsx";
7
 
8
- /**
9
- * Catalog sidebar β€” magazine table-of-contents vibe.
10
- * Each conversation is a folio entry, numbered, with a hairline rule below.
11
- */
12
  export function Sidebar() {
13
  const pathname = usePathname();
14
  const sidebarOpen = useChatStore((s) => s.sidebarOpen);
@@ -22,39 +18,16 @@ export function Sidebar() {
22
  return (
23
  <aside
24
  className={clsx(
25
- "shrink-0 sticky z-20 self-start",
26
  "transition-[width,opacity] duration-300 ease-atelier",
27
  sidebarOpen
28
- ? "w-[300px] opacity-100"
29
  : "w-0 opacity-0 pointer-events-none"
30
  )}
31
- style={{
32
- top: "calc(var(--topbar-h, 88px))",
33
- height: "calc(100vh - var(--topbar-h, 88px))",
34
- }}
35
  >
36
- <div
37
- className="h-full overflow-hidden flex flex-col"
38
- style={{
39
- background: "var(--paper-deep)",
40
- boxShadow: "1px 0 0 rgba(25,23,19,0.12)",
41
- }}
42
- >
43
- {/* Header: Catalog */}
44
- <div className="px-5 pt-5 pb-3">
45
- <div className="folio-chrome">Catalog</div>
46
- <h2
47
- className="italic-display text-ink mt-1"
48
- style={{ fontSize: "26px", lineHeight: 1.05 }}
49
- >
50
- Recent <span className="not-italic font-display font-medium not-italic-fix">interrogations.</span>
51
- </h2>
52
- </div>
53
-
54
- <div className="hairline mx-5" />
55
-
56
- {/* New conversation */}
57
- <div className="px-5 py-4">
58
  <button
59
  onClick={() => {
60
  newConversation();
@@ -63,161 +36,129 @@ export function Sidebar() {
63
  window.dispatchEvent(new PopStateEvent("popstate"));
64
  }
65
  }}
66
- className="group w-full flex items-center justify-between
67
- py-2.5 px-3 rounded-md
68
- text-ink hover:text-paper hover:bg-ink
69
- transition-all"
70
- style={{ boxShadow: "0 0 0 1px rgba(25,23,19,0.18)" }}
71
  >
72
- <span className="flex items-center gap-2.5">
73
- <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
74
- <path
75
- d="M6.5 1.5v10M1.5 6.5h10"
76
- stroke="currentColor"
77
- strokeWidth="1.4"
78
- strokeLinecap="round"
79
- />
80
- </svg>
81
- <span className="text-body-strong">Begin new transcript</span>
82
- </span>
83
- <span className="folio-chrome group-hover:text-paper transition-colors">⌘N</span>
84
  </button>
85
  </div>
86
 
87
- {/* Sections (folio nav) */}
88
- <nav className="px-3 pb-2 flex flex-col">
89
- <NavLink href="/" active={pathname === "/"} folio="I" label="Transcript" />
 
 
 
 
 
90
  <NavLink
91
  href="/documents"
92
  active={pathname.startsWith("/documents")}
93
- folio="II"
94
- label="Library"
95
  />
96
  <NavLink
97
  href="/system"
98
  active={pathname.startsWith("/system")}
99
- folio="III"
100
- label="Colophon"
101
  />
102
  </nav>
103
 
104
- <div className="hairline mx-5 my-2" />
105
 
106
- {/* Recent transcripts */}
107
- <div className="flex-1 overflow-y-auto px-3 pt-2 pb-4">
108
- <div className="px-2 pb-2 folio-chrome">Index of transcripts</div>
109
- {list.length === 0 ? (
110
- <div className="px-3 py-3 text-caption text-ink-50 italic">
111
- <span className="italic-display">No interrogations yet.</span>
 
 
112
  </div>
113
- ) : (
114
- <ul>
115
- {list.map((c, idx) => {
116
- const folio = String(idx + 1).padStart(2, "0");
117
- const active = activeId === c.id && pathname === "/";
118
- return (
119
- <li key={c.id} className="relative group">
120
- <button
121
- onClick={() => {
122
- openConversation(c.id);
123
- if (pathname !== "/") {
124
- window.history.pushState(null, "", "/");
125
- window.dispatchEvent(new PopStateEvent("popstate"));
126
- }
127
- if (window.innerWidth < 768) setSidebarOpen(false);
128
- }}
129
- className={clsx(
130
- "w-full text-left px-2 py-3 transition-colors flex items-start gap-3 rounded-sm",
131
- active
132
- ? "bg-ink text-paper"
133
- : "text-ink hover:bg-ink-08"
134
- )}
135
- >
136
- <span
137
- className={clsx(
138
- "shrink-0 mt-0.5 font-mono",
139
- active ? "text-paper/70" : "text-rust"
140
- )}
141
- style={{
142
- fontSize: "10px",
143
- letterSpacing: "0.18em",
144
- fontWeight: 600,
145
- }}
146
- >
147
- β„– {folio}
148
- </span>
149
- <span className="min-w-0 flex-1">
150
- <span
151
- className={clsx(
152
- "block truncate text-body",
153
- active ? "text-paper" : "text-ink"
154
- )}
155
- style={{ fontWeight: active ? 500 : 400 }}
156
- >
157
- {c.title}
158
- </span>
159
- <span
160
- className={clsx(
161
- "block mt-0.5 folio-chrome",
162
- active && "text-paper/60"
163
- )}
164
- >
165
- {formatRelative(c.updatedAt)} Β· {c.turns.length}{" "}
166
- turn{c.turns.length === 1 ? "" : "s"}
167
- </span>
168
- </span>
169
- </button>
170
- <button
171
- onClick={(e) => {
172
- e.stopPropagation();
173
- if (window.confirm(`Delete "${c.title}"?`)) {
174
- deleteConversation(c.id);
175
- }
176
- }}
177
- className={clsx(
178
- "absolute right-2 top-3 opacity-0 group-hover:opacity-100",
179
- "p-1 rounded text-ink-50 hover:text-rust hover:bg-rust-glow",
180
- "transition-all",
181
- active && "text-paper/60 hover:text-paper hover:bg-rust"
182
- )}
183
- aria-label="Delete transcript"
184
- title="Delete"
185
- >
186
- <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
187
- <path
188
- d="M2.5 2.5l6 6M8.5 2.5l-6 6"
189
- stroke="currentColor"
190
- strokeWidth="1.4"
191
- strokeLinecap="round"
192
- />
193
- </svg>
194
- </button>
195
- <div
196
- className={clsx(
197
- "h-px mx-2",
198
- active ? "bg-transparent" : "bg-ink/12"
199
- )}
200
- />
201
- </li>
202
- );
203
- })}
204
- </ul>
205
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  </div>
207
 
208
- {/* Footer colophon */}
209
- <div
210
- className="px-5 py-4 border-t border-ink/12"
211
- style={{ background: "rgba(0,0,0,0.02)" }}
212
- >
213
  <a
214
  href="https://huggingface.co/spaces/Etiya/d2l-api"
215
  target="_blank"
216
  rel="noopener noreferrer"
217
- className="folio-chrome flex items-center justify-between hover:text-rust transition-colors"
218
  >
219
- <span>Imprint Β· HF Space</span>
220
- <span aria-hidden>β†—</span>
 
 
 
 
 
 
 
221
  </a>
222
  </div>
223
  </div>
@@ -228,52 +169,26 @@ export function Sidebar() {
228
  function NavLink({
229
  href,
230
  active,
231
- folio,
232
  label,
 
233
  }: {
234
  href: string;
235
  active: boolean;
236
- folio: string;
237
  label: string;
 
238
  }) {
239
  return (
240
  <Link
241
  href={href}
242
  className={clsx(
243
- "group flex items-baseline gap-3 px-3 py-2 rounded-sm transition-colors",
244
- active ? "text-rust" : "text-ink hover:text-rust"
 
 
245
  )}
246
  >
247
- <span
248
- className="folio-chrome shrink-0 w-7"
249
- style={{ color: active ? "var(--rust)" : undefined }}
250
- >
251
- {folio}
252
- </span>
253
- <span
254
- className={clsx(
255
- "flex-1 transition-all",
256
- active ? "italic" : "group-hover:italic"
257
- )}
258
- style={{
259
- fontFamily: active ? "var(--font-fraunces)" : "var(--font-plex)",
260
- fontSize: active ? "20px" : "16px",
261
- fontWeight: active ? 400 : 500,
262
- lineHeight: 1.1,
263
- fontVariationSettings: active ? '"SOFT" 30, "WONK" 1' : undefined,
264
- }}
265
- >
266
- {label}{active && "."}
267
- </span>
268
- <span
269
- className={clsx(
270
- "transition-transform shrink-0 text-rust",
271
- active ? "translate-x-0 opacity-100" : "-translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100"
272
- )}
273
- aria-hidden
274
- >
275
- β†’
276
- </span>
277
  </Link>
278
  );
279
  }
@@ -281,14 +196,51 @@ function NavLink({
281
  function formatRelative(ts: number): string {
282
  const diff = Date.now() - ts;
283
  const min = Math.floor(diff / 60_000);
284
- if (min < 1) return "just now";
285
- if (min < 60) return `${min}m ago`;
286
  const hr = Math.floor(min / 60);
287
- if (hr < 24) return `${hr}h ago`;
288
  const d = Math.floor(hr / 24);
289
- if (d < 7) return `${d}d ago`;
290
- return new Date(ts).toLocaleDateString(undefined, {
291
- month: "short",
292
- day: "numeric",
293
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
 
5
  import { useChatStore } from "@/lib/chatStore";
6
  import clsx from "clsx";
7
 
 
 
 
 
8
  export function Sidebar() {
9
  const pathname = usePathname();
10
  const sidebarOpen = useChatStore((s) => s.sidebarOpen);
 
18
  return (
19
  <aside
20
  className={clsx(
21
+ "shrink-0 h-[calc(100vh-3.5rem)] sticky top-14 z-20",
22
  "transition-[width,opacity] duration-300 ease-atelier",
23
  sidebarOpen
24
+ ? "w-[280px] opacity-100"
25
  : "w-0 opacity-0 pointer-events-none"
26
  )}
 
 
 
 
27
  >
28
+ <div className="h-full overflow-hidden border-r border-glass-border bg-canvas-deep/40 backdrop-blur-glass flex flex-col">
29
+ {/* New chat */}
30
+ <div className="p-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  <button
32
  onClick={() => {
33
  newConversation();
 
36
  window.dispatchEvent(new PopStateEvent("popstate"));
37
  }
38
  }}
39
+ className="w-full flex items-center gap-2.5 px-4 py-3 rounded-md
40
+ bg-amber/10 text-amber
41
+ hover:bg-amber/15 transition-colors
42
+ text-body-strong"
43
+ style={{ boxShadow: "0 0 0 1px rgba(255,181,69,0.28)" }}
44
  >
45
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
46
+ <path
47
+ d="M7 2v10M2 7h10"
48
+ stroke="currentColor"
49
+ strokeWidth="1.6"
50
+ strokeLinecap="round"
51
+ />
52
+ </svg>
53
+ <span>New conversation</span>
 
 
 
54
  </button>
55
  </div>
56
 
57
+ {/* Sections */}
58
+ <nav className="px-3 pb-3 flex flex-col gap-0.5">
59
+ <NavLink
60
+ href="/"
61
+ active={pathname === "/"}
62
+ label="Chat"
63
+ icon={<IconSpark />}
64
+ />
65
  <NavLink
66
  href="/documents"
67
  active={pathname.startsWith("/documents")}
68
+ label="Documents"
69
+ icon={<IconDoc />}
70
  />
71
  <NavLink
72
  href="/system"
73
  active={pathname.startsWith("/system")}
74
+ label="System"
75
+ icon={<IconPulse />}
76
  />
77
  </nav>
78
 
79
+ <div className="hairline mx-4 my-2" />
80
 
81
+ {/* Conversation history */}
82
+ <div className="px-3 flex-1 overflow-y-auto">
83
+ <div className="px-3 py-2 text-micro uppercase tracking-[0.15em] text-ink-50">
84
+ Recent
85
+ </div>
86
+ {list.length === 0 && (
87
+ <div className="px-3 py-4 text-caption text-ink-50">
88
+ No conversations yet.
89
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  )}
91
+ <ul className="flex flex-col gap-0.5">
92
+ {list.map((c) => (
93
+ <li key={c.id} className="group relative">
94
+ <button
95
+ onClick={() => {
96
+ openConversation(c.id);
97
+ if (pathname !== "/") {
98
+ window.history.pushState(null, "", "/");
99
+ window.dispatchEvent(new PopStateEvent("popstate"));
100
+ }
101
+ if (window.innerWidth < 768) setSidebarOpen(false);
102
+ }}
103
+ className={clsx(
104
+ "w-full text-left px-3 py-2 rounded-md transition-colors",
105
+ "flex items-start gap-2 min-w-0",
106
+ activeId === c.id && pathname === "/"
107
+ ? "bg-glass-stronger text-ink"
108
+ : "text-ink-70 hover:text-ink hover:bg-glass"
109
+ )}
110
+ >
111
+ <span className="text-caption truncate flex-1 min-w-0">
112
+ {c.title}
113
+ </span>
114
+ <span className="text-micro text-ink-30 font-mono shrink-0 mt-0.5">
115
+ {formatRelative(c.updatedAt)}
116
+ </span>
117
+ </button>
118
+ <button
119
+ onClick={(e) => {
120
+ e.stopPropagation();
121
+ if (window.confirm(`Delete "${c.title}"?`)) {
122
+ deleteConversation(c.id);
123
+ }
124
+ }}
125
+ className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100
126
+ p-1 rounded text-ink-50 hover:text-status-err hover:bg-status-err-glow
127
+ transition-all"
128
+ aria-label="Delete conversation"
129
+ title="Delete"
130
+ >
131
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
132
+ <path
133
+ d="M3 3l6 6M9 3l-6 6"
134
+ stroke="currentColor"
135
+ strokeWidth="1.4"
136
+ strokeLinecap="round"
137
+ />
138
+ </svg>
139
+ </button>
140
+ </li>
141
+ ))}
142
+ </ul>
143
  </div>
144
 
145
+ {/* Footer */}
146
+ <div className="px-4 py-3 border-t border-glass-border">
 
 
 
147
  <a
148
  href="https://huggingface.co/spaces/Etiya/d2l-api"
149
  target="_blank"
150
  rel="noopener noreferrer"
151
+ className="flex items-center justify-between text-micro uppercase tracking-[0.12em] text-ink-50 hover:text-ink-70 transition-colors"
152
  >
153
+ <span>HF Space Β· backend</span>
154
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
155
+ <path
156
+ d="M3 3h4v4M7 3 3 7"
157
+ stroke="currentColor"
158
+ strokeWidth="1.3"
159
+ strokeLinecap="round"
160
+ />
161
+ </svg>
162
  </a>
163
  </div>
164
  </div>
 
169
  function NavLink({
170
  href,
171
  active,
 
172
  label,
173
+ icon,
174
  }: {
175
  href: string;
176
  active: boolean;
 
177
  label: string;
178
+ icon: React.ReactNode;
179
  }) {
180
  return (
181
  <Link
182
  href={href}
183
  className={clsx(
184
+ "flex items-center gap-3 px-3 py-2 rounded-md transition-all",
185
+ active
186
+ ? "bg-glass-stronger text-ink"
187
+ : "text-ink-70 hover:text-ink hover:bg-glass"
188
  )}
189
  >
190
+ <span className={clsx("w-4 h-4", active && "text-amber")}>{icon}</span>
191
+ <span className="text-body">{label}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  </Link>
193
  );
194
  }
 
196
  function formatRelative(ts: number): string {
197
  const diff = Date.now() - ts;
198
  const min = Math.floor(diff / 60_000);
199
+ if (min < 1) return "now";
200
+ if (min < 60) return `${min}m`;
201
  const hr = Math.floor(min / 60);
202
+ if (hr < 24) return `${hr}h`;
203
  const d = Math.floor(hr / 24);
204
+ if (d < 7) return `${d}d`;
205
+ return new Date(ts).toLocaleDateString(undefined, { month: "short", day: "numeric" });
206
+ }
207
+
208
+ function IconSpark() {
209
+ return (
210
+ <svg viewBox="0 0 16 16" fill="none" className="w-full h-full">
211
+ <path
212
+ d="M8 1.5v3M8 11.5v3M1.5 8h3M11.5 8h3M3.5 3.5l2 2M10.5 10.5l2 2M3.5 12.5l2-2M10.5 5.5l2-2"
213
+ stroke="currentColor"
214
+ strokeWidth="1.4"
215
+ strokeLinecap="round"
216
+ />
217
+ </svg>
218
+ );
219
+ }
220
+
221
+ function IconDoc() {
222
+ return (
223
+ <svg viewBox="0 0 16 16" fill="none" className="w-full h-full">
224
+ <path
225
+ d="M3.5 1.5h6l3.5 3.5v9a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-12a.5.5 0 0 1 .5-.5z"
226
+ stroke="currentColor"
227
+ strokeWidth="1.4"
228
+ />
229
+ <path d="M9.5 1.5V5h3.5" stroke="currentColor" strokeWidth="1.4" />
230
+ </svg>
231
+ );
232
+ }
233
+
234
+ function IconPulse() {
235
+ return (
236
+ <svg viewBox="0 0 16 16" fill="none" className="w-full h-full">
237
+ <path
238
+ d="M1.5 8h3l1.5-4 3 8L10.5 8h4"
239
+ stroke="currentColor"
240
+ strokeWidth="1.4"
241
+ strokeLinecap="round"
242
+ strokeLinejoin="round"
243
+ />
244
+ </svg>
245
+ );
246
  }
components/chat/SourceChips.tsx CHANGED
@@ -4,51 +4,46 @@ import type { SourceDoc } from "@/lib/types";
4
  import { useState } from "react";
5
  import clsx from "clsx";
6
 
7
- /**
8
- * Source list β€” bibliography style.
9
- * Numbered entries, italic title, hairline below, expandable scores.
10
- */
11
  export function SourceChips({ docs }: { docs: SourceDoc[] }) {
12
  const [expanded, setExpanded] = useState<string | null>(null);
13
  if (!docs?.length) return null;
14
 
15
  return (
16
- <section className="mt-6">
17
- <div className="flex items-baseline justify-between mb-2">
18
- <span className="folio-chrome">Bibliography</span>
19
- <span className="folio-chrome">{docs.length} entr{docs.length === 1 ? "y" : "ies"}</span>
20
  </div>
21
- <ul className="border-t border-ink/16">
22
- {docs.map((d, i) => {
23
  const isOpen = expanded === d.doc_id;
24
  const sim = d.similarity ?? d.dense_similarity ?? 0;
25
  return (
26
- <li key={d.doc_id} className="border-b border-ink/12">
27
  <button
28
  onClick={() => setExpanded(isOpen ? null : d.doc_id)}
29
- className="w-full text-left py-3 flex items-baseline gap-4 group"
 
 
 
 
 
 
 
30
  >
31
- <span className="folio-chrome--ink folio-chrome shrink-0 w-7">
32
- {String(i + 1).padStart(2, "0")}
33
- </span>
34
- <span className="min-w-0 flex-1">
35
- <span
36
- className="block italic-display text-ink truncate group-hover:text-rust transition-colors"
37
- style={{ fontSize: "16px", lineHeight: 1.3 }}
38
- >
39
  {d.name}
40
- </span>
41
- <span className="block mt-0.5 folio-chrome">
42
  {d.doc_id}
43
- </span>
44
- </span>
45
- <SimilarityRing value={sim} />
46
  <span
47
  className={clsx(
48
  "text-ink-50 transition-transform shrink-0",
49
  isOpen && "rotate-180"
50
  )}
51
- aria-hidden
52
  >
53
  <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
54
  <path
@@ -61,7 +56,10 @@ export function SourceChips({ docs }: { docs: SourceDoc[] }) {
61
  </span>
62
  </button>
63
  {isOpen && (
64
- <div className="pb-4 grid grid-cols-3 gap-6 animate-fade-in">
 
 
 
65
  <Metric label="Cosine" value={fmt(d.similarity)} />
66
  <Metric label="Dense" value={fmt(d.dense_similarity)} />
67
  <Metric label="Rerank" value={fmt(d.rerank_score, 2)} />
@@ -71,20 +69,17 @@ export function SourceChips({ docs }: { docs: SourceDoc[] }) {
71
  );
72
  })}
73
  </ul>
74
- </section>
75
  );
76
  }
77
 
78
  function Metric({ label, value }: { label: string; value: string }) {
79
  return (
80
  <div>
81
- <div className="folio-chrome">{label}</div>
82
- <div
83
- className="font-mono text-ink mt-0.5"
84
- style={{ fontSize: "16px", fontWeight: 500 }}
85
- >
86
- {value}
87
  </div>
 
88
  </div>
89
  );
90
  }
@@ -98,41 +93,41 @@ function SimilarityRing({ value }: { value: number }) {
98
  const v = Math.max(0, Math.min(1, value || 0));
99
  const tone =
100
  v >= 0.7
101
- ? "var(--rust)"
102
  : v >= 0.45
103
- ? "rgba(25, 23, 19, 0.85)"
104
- : "rgba(25, 23, 19, 0.35)";
105
- const r = 11;
106
  const C = 2 * Math.PI * r;
107
  const dash = C * v;
108
  return (
109
- <svg width="28" height="28" viewBox="0 0 28 28" className="shrink-0">
110
  <circle
111
- cx="14"
112
- cy="14"
113
  r={r}
114
- stroke="rgba(25,23,19,0.18)"
115
- strokeWidth="1.5"
116
  fill="none"
117
  />
118
  <circle
119
- cx="14"
120
- cy="14"
121
  r={r}
122
  stroke={tone}
123
- strokeWidth="1.8"
124
  fill="none"
125
  strokeLinecap="round"
126
  strokeDasharray={`${dash} ${C - dash}`}
127
- transform="rotate(-90 14 14)"
128
  style={{ transition: "stroke-dasharray 0.4s cubic-bezier(0.16,1,0.3,1)" }}
129
  />
130
  <text
131
- x="14"
132
- y="16.5"
133
  textAnchor="middle"
134
- fontSize="8"
135
- fill="rgba(25, 23, 19, 0.85)"
136
  fontFamily="var(--font-mono), monospace"
137
  fontWeight="600"
138
  >
 
4
  import { useState } from "react";
5
  import clsx from "clsx";
6
 
 
 
 
 
7
  export function SourceChips({ docs }: { docs: SourceDoc[] }) {
8
  const [expanded, setExpanded] = useState<string | null>(null);
9
  if (!docs?.length) return null;
10
 
11
  return (
12
+ <div className="mt-4 flex flex-col gap-2">
13
+ <div className="text-micro uppercase tracking-[0.12em] text-ink-50 font-mono">
14
+ Sources Β· {docs.length}
 
15
  </div>
16
+ <ul className="flex flex-col gap-1.5">
17
+ {docs.map((d) => {
18
  const isOpen = expanded === d.doc_id;
19
  const sim = d.similarity ?? d.dense_similarity ?? 0;
20
  return (
21
+ <li key={d.doc_id}>
22
  <button
23
  onClick={() => setExpanded(isOpen ? null : d.doc_id)}
24
+ className={clsx(
25
+ "w-full text-left rounded-md px-3 py-2 transition-all",
26
+ "flex items-center gap-3",
27
+ isOpen
28
+ ? "bg-glass-stronger"
29
+ : "bg-glass hover:bg-glass-stronger"
30
+ )}
31
+ style={{ boxShadow: "0 0 0 1px rgba(255,255,255,0.07)" }}
32
  >
33
+ <SimilarityRing value={sim} />
34
+ <div className="min-w-0 flex-1">
35
+ <div className="text-caption-strong text-ink truncate">
 
 
 
 
 
36
  {d.name}
37
+ </div>
38
+ <div className="text-micro font-mono text-ink-50 truncate">
39
  {d.doc_id}
40
+ </div>
41
+ </div>
 
42
  <span
43
  className={clsx(
44
  "text-ink-50 transition-transform shrink-0",
45
  isOpen && "rotate-180"
46
  )}
 
47
  >
48
  <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
49
  <path
 
56
  </span>
57
  </button>
58
  {isOpen && (
59
+ <div
60
+ className="mt-1.5 px-3 py-3 rounded-md bg-canvas-deep/70 grid grid-cols-3 gap-3 animate-fade-in"
61
+ style={{ boxShadow: "0 0 0 1px rgba(255,255,255,0.06)" }}
62
+ >
63
  <Metric label="Cosine" value={fmt(d.similarity)} />
64
  <Metric label="Dense" value={fmt(d.dense_similarity)} />
65
  <Metric label="Rerank" value={fmt(d.rerank_score, 2)} />
 
69
  );
70
  })}
71
  </ul>
72
+ </div>
73
  );
74
  }
75
 
76
  function Metric({ label, value }: { label: string; value: string }) {
77
  return (
78
  <div>
79
+ <div className="text-micro uppercase tracking-[0.12em] text-ink-50">
80
+ {label}
 
 
 
 
81
  </div>
82
+ <div className="text-body-strong font-mono text-ink mt-0.5">{value}</div>
83
  </div>
84
  );
85
  }
 
93
  const v = Math.max(0, Math.min(1, value || 0));
94
  const tone =
95
  v >= 0.7
96
+ ? "rgb(93, 214, 168)"
97
  : v >= 0.45
98
+ ? "rgb(255, 181, 69)"
99
+ : "rgba(245, 243, 236, 0.4)";
100
+ const r = 9;
101
  const C = 2 * Math.PI * r;
102
  const dash = C * v;
103
  return (
104
+ <svg width="22" height="22" viewBox="0 0 22 22" className="shrink-0">
105
  <circle
106
+ cx="11"
107
+ cy="11"
108
  r={r}
109
+ stroke="rgba(255,255,255,0.10)"
110
+ strokeWidth="2"
111
  fill="none"
112
  />
113
  <circle
114
+ cx="11"
115
+ cy="11"
116
  r={r}
117
  stroke={tone}
118
+ strokeWidth="2"
119
  fill="none"
120
  strokeLinecap="round"
121
  strokeDasharray={`${dash} ${C - dash}`}
122
+ transform="rotate(-90 11 11)"
123
  style={{ transition: "stroke-dasharray 0.4s cubic-bezier(0.16,1,0.3,1)" }}
124
  />
125
  <text
126
+ x="11"
127
+ y="13.5"
128
  textAnchor="middle"
129
+ fontSize="7"
130
+ fill="rgba(245, 243, 236, 0.85)"
131
  fontFamily="var(--font-mono), monospace"
132
  fontWeight="600"
133
  >
components/chat/Thinking.tsx CHANGED
@@ -1,30 +1,17 @@
1
  "use client";
2
 
3
- const STAGES = ["Retrieving", "Reranking", "Grounding", "Generating"];
4
 
5
- /**
6
- * Thinking placeholder. Editorial: a folio mark, italic running header,
7
- * three pulsing dots in mono.
8
- */
9
  export function Thinking() {
10
  return (
11
- <div className="grid grid-cols-[80px_1fr] gap-6 animate-fade-in">
12
- <div className="text-right pr-2 pt-1">
13
- <div className="folio-chrome">Pending</div>
14
- </div>
15
- <div className="border-t border-ink/16 pt-3">
16
- <div className="flex items-baseline gap-3">
17
- <span
18
- className="italic-display text-ink-50"
19
- style={{
20
- fontSize: "20px",
21
- lineHeight: 1.2,
22
- fontVariationSettings: '"SOFT" 30, "WONK" 1',
23
- }}
24
- >
25
  {STAGES.join(" Β· ")}
26
  </span>
27
- <Dots />
28
  </div>
29
  </div>
30
  </div>
@@ -35,17 +22,41 @@ function Dots() {
35
  return (
36
  <span className="inline-flex items-center gap-1">
37
  <span
38
- className="w-1.5 h-1.5 rounded-full bg-rust animate-thinking-dot"
39
  style={{ animationDelay: "0s" }}
40
  />
41
  <span
42
- className="w-1.5 h-1.5 rounded-full bg-rust animate-thinking-dot"
43
- style={{ animationDelay: "0.18s" }}
44
  />
45
  <span
46
- className="w-1.5 h-1.5 rounded-full bg-rust animate-thinking-dot"
47
- style={{ animationDelay: "0.36s" }}
48
  />
49
  </span>
50
  );
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  "use client";
2
 
3
+ const STAGES = ["retrieving", "reranking", "grounding", "generating"];
4
 
 
 
 
 
5
  export function Thinking() {
6
  return (
7
+ <div className="flex items-start gap-3 animate-fade-in">
8
+ <Avatar />
9
+ <div className="chat-bubble-assistant flex-1 min-w-0">
10
+ <div className="flex items-center gap-2.5">
11
+ <Dots />
12
+ <span className="text-caption text-ink-70 text-shimmer">
 
 
 
 
 
 
 
 
13
  {STAGES.join(" Β· ")}
14
  </span>
 
15
  </div>
16
  </div>
17
  </div>
 
22
  return (
23
  <span className="inline-flex items-center gap-1">
24
  <span
25
+ className="w-1.5 h-1.5 rounded-full bg-amber animate-thinking-dot"
26
  style={{ animationDelay: "0s" }}
27
  />
28
  <span
29
+ className="w-1.5 h-1.5 rounded-full bg-amber animate-thinking-dot"
30
+ style={{ animationDelay: "0.16s" }}
31
  />
32
  <span
33
+ className="w-1.5 h-1.5 rounded-full bg-amber animate-thinking-dot"
34
+ style={{ animationDelay: "0.32s" }}
35
  />
36
  </span>
37
  );
38
  }
39
+
40
+ function Avatar() {
41
+ return (
42
+ <div
43
+ className="shrink-0 w-7 h-7 rounded-md flex items-center justify-center"
44
+ style={{
45
+ background:
46
+ "linear-gradient(135deg, rgba(255,181,69,0.95), rgba(214,138,31,1))",
47
+ boxShadow:
48
+ "0 0 0 1px rgba(255,181,69,0.45), 0 0 14px -2px rgba(255,181,69,0.45)",
49
+ }}
50
+ >
51
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
52
+ <path
53
+ d="M2.5 7.5 4.8 9.8l6-6.6"
54
+ stroke="#0a0b10"
55
+ strokeWidth="1.8"
56
+ strokeLinecap="round"
57
+ strokeLinejoin="round"
58
+ />
59
+ </svg>
60
+ </div>
61
+ );
62
+ }
components/chat/Thread.tsx CHANGED
@@ -6,9 +6,7 @@ import { AssistantMessage, ErrorMessage, UserMessage } from "./Message";
6
  import { Thinking } from "./Thinking";
7
 
8
  export function Thread() {
9
- const conv = useChatStore((s) =>
10
- s.activeId ? s.conversations[s.activeId] : null
11
- );
12
  const ref = useRef<HTMLDivElement | null>(null);
13
 
14
  // Auto-scroll to bottom on new turns
@@ -22,36 +20,15 @@ export function Thread() {
22
 
23
  return (
24
  <div ref={ref} className="flex-1 overflow-y-auto">
25
- <div className="max-w-[1040px] mx-auto px-6 md:px-10 py-10">
26
- {/* Transcript header */}
27
- <header className="grid grid-cols-[80px_1fr] gap-6 mb-8 animate-fade-in">
28
- <div className="text-right pr-2">
29
- <div className="folio-chrome">Transcript</div>
30
- </div>
31
- <div className="border-b-2 border-ink/30 pb-3 flex items-baseline justify-between flex-wrap gap-2">
32
- <h1
33
- className="italic-display text-ink"
34
- style={{ fontSize: "32px", lineHeight: 1.1 }}
35
- >
36
- {conv.title}
37
- </h1>
38
- <span className="folio-chrome">
39
- {conv.turns.length} turn{conv.turns.length === 1 ? "" : "s"} Β·{" "}
40
- {new Date(conv.createdAt).toLocaleDateString()}
41
- </span>
42
  </div>
43
- </header>
44
-
45
- <div className="space-y-10">
46
- {conv.turns.map((t, i) => (
47
- <div key={t.id} className="space-y-8">
48
- <UserMessage text={t.question} index={i} />
49
- {t.pending && <Thinking />}
50
- {t.error && <ErrorMessage turn={t} index={i} />}
51
- {t.response && <AssistantMessage turn={t} index={i} />}
52
- </div>
53
- ))}
54
- </div>
55
  </div>
56
  </div>
57
  );
 
6
  import { Thinking } from "./Thinking";
7
 
8
  export function Thread() {
9
+ const conv = useChatStore((s) => (s.activeId ? s.conversations[s.activeId] : null));
 
 
10
  const ref = useRef<HTMLDivElement | null>(null);
11
 
12
  // Auto-scroll to bottom on new turns
 
20
 
21
  return (
22
  <div ref={ref} className="flex-1 overflow-y-auto">
23
+ <div className="max-w-[920px] mx-auto px-4 sm:px-6 py-8 space-y-6">
24
+ {conv.turns.map((t) => (
25
+ <div key={t.id} className="space-y-4">
26
+ <UserMessage text={t.question} />
27
+ {t.pending && <Thinking />}
28
+ {t.error && <ErrorMessage turn={t} />}
29
+ {t.response && <AssistantMessage turn={t} />}
 
 
 
 
 
 
 
 
 
 
30
  </div>
31
+ ))}
 
 
 
 
 
 
 
 
 
 
 
32
  </div>
33
  </div>
34
  );
components/chat/TopBar.tsx CHANGED
@@ -6,10 +6,6 @@ import { api } from "@/lib/api";
6
  import { useChatStore } from "@/lib/chatStore";
7
  import clsx from "clsx";
8
 
9
- /**
10
- * Magazine masthead. Vol/Issue/folio chrome on the left, brand/title centered,
11
- * status pulse on the right.
12
- */
13
  export function TopBar() {
14
  const [pulse, setPulse] = useState<"alive" | "down" | "loading">("loading");
15
  const [pingMs, setPingMs] = useState<number | null>(null);
@@ -41,84 +37,64 @@ export function TopBar() {
41
  };
42
  }, []);
43
 
44
- const today = new Date().toLocaleDateString("en-US", {
45
- year: "numeric",
46
- month: "short",
47
- day: "2-digit",
48
- });
49
-
50
  return (
51
- <header className="relative z-30 sticky top-0 bg-paper/85 backdrop-blur-sm">
52
- {/* Top thin issue-strip */}
53
- <div className="border-b border-ink/12">
54
- <div className="container-app py-1.5 flex items-center justify-between text-[10px] folio-chrome">
55
- <span>Vol. I Β· Issue 02 Β· Etiya BSS Atelier Β· {today}</span>
56
- <span className="hidden md:inline">EN Β· UTC+3 Β· ISTANBUL</span>
57
- </div>
58
- </div>
59
-
60
- {/* Masthead */}
61
- <div className="border-b border-ink/16">
62
- <div className="container-app py-3 flex items-center justify-between gap-4">
63
- {/* Left: sidebar toggle + brand */}
64
- <div className="flex items-center gap-3">
65
- <button
66
- onClick={toggleSidebar}
67
- className="hidden md:inline-flex p-2 rounded-md text-ink-50 hover:text-ink hover:bg-ink-08 transition-colors"
68
- aria-label="Toggle catalog"
69
- >
70
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
71
- <path
72
- d="M2 4h12M2 8h12M2 12h12"
73
- stroke="currentColor"
74
- strokeWidth="1.5"
75
- strokeLinecap="round"
76
- />
77
- </svg>
78
- </button>
79
- <Link href="/" className="flex items-center gap-2.5 group">
80
- <Mark />
81
- <div className="leading-tight">
82
- <div
83
- className="italic-display text-ink"
84
- style={{ fontSize: "26px", lineHeight: 1, letterSpacing: "-0.02em" }}
85
- >
86
- doc<span className="text-rust">Β·</span>to
87
- <span className="text-rust">Β·</span>lora
88
- </div>
89
- <div className="folio-chrome mt-1">
90
- An interrogation atelier
91
- </div>
92
  </div>
93
- </Link>
94
- </div>
 
95
 
96
- {/* Right: status */}
97
- <div className="flex items-center gap-3">
 
 
 
 
 
98
  <span
99
- className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-pill bg-paper-deep"
100
- style={{ boxShadow: "0 0 0 1px rgba(25,23,19,0.18)" }}
101
- aria-live="polite"
102
- >
103
- <span
104
- className={clsx(
105
- "inline-block w-1.5 h-1.5 rounded-full transition-colors",
106
- pulse === "alive"
107
- ? "bg-status-ok animate-ink-pulse"
108
- : pulse === "down"
109
- ? "bg-rust"
110
- : "bg-ink-50"
111
- )}
112
- />
113
- <span className="folio-chrome--ink folio-chrome">
114
- {pulse === "alive"
115
- ? `Live Β· ${pingMs?.toFixed(0) ?? "β€”"}ms`
116
  : pulse === "down"
117
- ? "Down"
118
- : "Β·Β·Β·"}
119
- </span>
 
 
 
 
 
 
 
120
  </span>
121
- </div>
122
  </div>
123
  </div>
124
  </header>
@@ -128,23 +104,23 @@ export function TopBar() {
128
  function Mark() {
129
  return (
130
  <span
131
- className="relative inline-flex items-center justify-center w-9 h-9 rounded-md"
132
  style={{
133
- background: "var(--rust)",
134
- boxShadow: "0 0 0 1px rgba(200,77,44,0.35), 0 1px 0 rgba(255,255,255,0.4) inset",
 
 
135
  }}
136
  >
137
- <span
138
- className="italic-display"
139
- style={{
140
- fontSize: "22px",
141
- lineHeight: 1,
142
- color: "#f4ede0",
143
- fontWeight: 400,
144
- }}
145
- >
146
- d
147
- </span>
148
  </span>
149
  );
150
  }
 
6
  import { useChatStore } from "@/lib/chatStore";
7
  import clsx from "clsx";
8
 
 
 
 
 
9
  export function TopBar() {
10
  const [pulse, setPulse] = useState<"alive" | "down" | "loading">("loading");
11
  const [pingMs, setPingMs] = useState<number | null>(null);
 
37
  };
38
  }, []);
39
 
 
 
 
 
 
 
40
  return (
41
+ <header className="sticky top-0 z-30 h-14 flex items-center backdrop-blur-nav bg-canvas-deep/60 border-b border-glass-border">
42
+ <div className="container-app flex items-center justify-between w-full">
43
+ {/* Left: brand + sidebar toggle */}
44
+ <div className="flex items-center gap-3">
45
+ <button
46
+ onClick={toggleSidebar}
47
+ className="md:flex hidden p-2 rounded-md text-ink-50 hover:text-ink hover:bg-glass transition-colors"
48
+ aria-label="Toggle sidebar"
49
+ >
50
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
51
+ <path
52
+ d="M3 5h12M3 9h12M3 13h12"
53
+ stroke="currentColor"
54
+ strokeWidth="1.5"
55
+ strokeLinecap="round"
56
+ />
57
+ </svg>
58
+ </button>
59
+ <Link href="/" className="flex items-center gap-2.5 group">
60
+ <Mark />
61
+ <div className="leading-none">
62
+ <div className="text-body-strong text-ink">
63
+ doc<span className="text-amber">Β·</span>to
64
+ <span className="text-amber">Β·</span>lora
65
+ </div>
66
+ <div className="text-micro text-ink-50 mt-0.5 uppercase tracking-[0.18em]">
67
+ Etiya BSS Atelier
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  </div>
69
+ </div>
70
+ </Link>
71
+ </div>
72
 
73
+ {/* Right: status pulse */}
74
+ <div className="flex items-center gap-3">
75
+ <span
76
+ className="hidden sm:flex items-center gap-2 rounded-pill px-3 py-1.5 bg-glass"
77
+ style={{ boxShadow: "0 0 0 1px rgba(255,255,255,0.08)" }}
78
+ aria-live="polite"
79
+ >
80
  <span
81
+ className={clsx(
82
+ "inline-block w-1.5 h-1.5 rounded-full transition-colors",
83
+ pulse === "alive"
84
+ ? "bg-status-ok shadow-[0_0_8px_rgba(93,214,168,0.8)] animate-pulse-soft"
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  : pulse === "down"
86
+ ? "bg-status-err"
87
+ : "bg-ink-50"
88
+ )}
89
+ />
90
+ <span className="text-micro text-ink-70 font-mono uppercase tracking-[0.15em]">
91
+ {pulse === "alive"
92
+ ? `live Β· ${pingMs?.toFixed(0) ?? "β€”"}ms`
93
+ : pulse === "down"
94
+ ? "down"
95
+ : "Β·Β·Β·"}
96
  </span>
97
+ </span>
98
  </div>
99
  </div>
100
  </header>
 
104
  function Mark() {
105
  return (
106
  <span
107
+ className="relative inline-flex items-center justify-center w-8 h-8 rounded-md"
108
  style={{
109
+ background:
110
+ "linear-gradient(135deg, rgba(255,181,69,0.95) 0%, rgba(214,138,31,1) 100%)",
111
+ boxShadow:
112
+ "0 0 0 1px rgba(255,181,69,0.4), 0 0 20px -2px rgba(255,181,69,0.5)",
113
  }}
114
  >
115
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
116
+ <path
117
+ d="M2.5 8.5 5 11l6-7"
118
+ stroke="#0a0b10"
119
+ strokeWidth="2"
120
+ strokeLinecap="round"
121
+ strokeLinejoin="round"
122
+ />
123
+ </svg>
 
 
124
  </span>
125
  );
126
  }
tailwind.config.ts CHANGED
@@ -1,13 +1,12 @@
1
  import type { Config } from "tailwindcss";
2
 
3
  /**
4
- * Etiya doc-to-lora β€” Risograph Folio.
5
  *
6
- * A printed-publication aesthetic for an AI corpus. Two-color riso ink on
7
- * warm parchment, halftone textures instead of gradients, Fraunces italic
8
- * carrying display moments, magazine-folio chrome (volume, issue, folio
9
- * numbers, register marks). The interrogation is a transcript; the doc list
10
- * is a catalog; the system page is a colophon.
11
  */
12
  const config: Config = {
13
  content: [
@@ -18,97 +17,105 @@ const config: Config = {
18
  theme: {
19
  extend: {
20
  colors: {
21
- // ── Paper (warm parchment, never pure white)
22
- paper: {
23
- DEFAULT: "#f4ede0",
24
- deep: "#ebe2d0",
25
- shade: "#e2d6bd",
26
- edge: "#d3c6a8",
 
 
27
  },
28
- // ── Ink (warm near-black, espresso)
29
- ink: {
30
- DEFAULT: "#191713",
31
- 90: "rgba(25, 23, 19, 0.92)",
32
- 70: "rgba(25, 23, 19, 0.68)",
33
- 50: "rgba(25, 23, 19, 0.46)",
34
- 30: "rgba(25, 23, 19, 0.26)",
35
- 15: "rgba(25, 23, 19, 0.14)",
36
- "08": "rgba(25, 23, 19, 0.08)",
37
  },
38
- // ── Single saturated accent (Riso warm red / rust)
39
- rust: {
40
- DEFAULT: "#c84d2c",
41
- deep: "#a43d20",
42
- soft: "#e89878",
43
- glow: "rgba(200, 77, 44, 0.14)",
44
- ring: "rgba(200, 77, 44, 0.45)",
45
- paper: "#f5d8c8",
46
  },
47
- // ── Cool counterpoint (used sparingly: status-ok, technical chips)
48
- slate: {
49
- DEFAULT: "#3e5260",
50
- soft: "#7a8c98",
51
- paper: "#d6dde2",
 
 
52
  },
53
- // ── Status (riso-tuned, not generic)
54
  status: {
55
- ok: "#496c2a",
56
- "ok-glow": "rgba(73, 108, 42, 0.12)",
57
- warn: "#c2873a",
58
- "warn-glow": "rgba(194, 135, 58, 0.14)",
59
- err: "#c84d2c",
60
- "err-glow": "rgba(200, 77, 44, 0.14)",
61
  },
62
  },
63
  fontFamily: {
64
- // Display: Fraunces (variable italic, optical sizes β€” wonderful)
65
- display: ["var(--font-fraunces)", "ui-serif", "Georgia", "serif"],
66
- // UI / body: IBM Plex Sans β€” humanist, characterful, never Inter
67
- sans: ["var(--font-plex)", "ui-sans-serif", "system-ui", "sans-serif"],
68
- // Folio numerals & technical readouts
69
  mono: ["var(--font-mono)", "ui-monospace", "SF Mono", "Menlo", "monospace"],
70
- // Editorial italic shortcut
71
- serif: ["var(--font-fraunces)", "ui-serif", "Georgia", "serif"],
72
  },
73
  fontSize: {
74
- // Folio display ladder β€” Fraunces with wide tracking range
75
- folio: ["120px", { lineHeight: "0.92", letterSpacing: "-0.04em", fontWeight: "300" }],
76
- "display-2xl": ["88px", { lineHeight: "0.96", letterSpacing: "-0.035em", fontWeight: "300" }],
77
- "display-xl": ["64px", { lineHeight: "1.0", letterSpacing: "-0.03em", fontWeight: "400" }],
78
- "display-lg": ["44px", { lineHeight: "1.05", letterSpacing: "-0.02em", fontWeight: "500" }],
79
- "display-md": ["32px", { lineHeight: "1.15", letterSpacing: "-0.018em", fontWeight: "500" }],
80
- "display-sm": ["22px", { lineHeight: "1.25", letterSpacing: "-0.012em", fontWeight: "500" }],
81
- // Body (Plex)
82
- lead: ["19px", { lineHeight: "1.55", letterSpacing: "0", fontWeight: "400" }],
83
- body: ["15px", { lineHeight: "1.65", letterSpacing: "0", fontWeight: "400" }],
84
  "body-strong": ["15px", { lineHeight: "1.6", letterSpacing: "0", fontWeight: "600" }],
85
  caption: ["13px", { lineHeight: "1.5", letterSpacing: "0", fontWeight: "400" }],
86
  "caption-strong": ["13px", { lineHeight: "1.4", letterSpacing: "0.005em", fontWeight: "600" }],
87
- // Folio chrome β€” heavy uppercase tracking
88
- folio_chrome: ["10px", { lineHeight: "1.4", letterSpacing: "0.22em", fontWeight: "600" }],
89
- micro: ["11px", { lineHeight: "1.4", letterSpacing: "0.16em", fontWeight: "500" }],
 
90
  },
91
  borderRadius: {
92
  none: "0",
93
- xs: "2px",
94
- sm: "4px",
95
- DEFAULT: "6px",
96
- md: "8px",
97
- lg: "12px",
 
 
98
  pill: "9999px",
99
  },
100
  boxShadow: {
101
- // No glassy floats β€” flat printed surfaces with hairline rules
102
- rule: "0 1px 0 rgba(25, 23, 19, 0.14)",
103
- "rule-strong": "0 1px 0 rgba(25, 23, 19, 0.26)",
104
- "rule-up": "0 -1px 0 rgba(25, 23, 19, 0.14)",
105
- // A faint press shadow for cards (almost imperceptible)
106
- press: "0 0 0 1px rgba(25,23,19,0.08), 0 1px 0 rgba(25,23,19,0.06)",
107
- // Rust ring for accent buttons
108
- rust:
109
- "0 0 0 1px rgba(200,77,44,0.45), 0 6px 18px -6px rgba(200,77,44,0.35)",
 
 
 
 
 
 
110
  },
111
  transitionTimingFunction: {
 
112
  atelier: "cubic-bezier(0.32, 0.72, 0, 1)",
113
  "out-expo": "cubic-bezier(0.16, 1, 0.3, 1)",
114
  },
@@ -120,10 +127,10 @@ const config: Config = {
120
  "stagger-2": "fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.16s both",
121
  "stagger-3": "fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.24s both",
122
  "stagger-4": "fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.32s both",
123
- rule: "rule 0.5s cubic-bezier(0.16, 1, 0.3, 1) both",
124
- "rule-2": "rule 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.1s both",
125
- "rule-3": "rule 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.2s both",
126
- "ink-pulse": "inkPulse 2.4s ease-in-out infinite",
127
  "thinking-dot": "thinkingDot 1.4s ease-in-out infinite",
128
  },
129
  keyframes: {
@@ -132,20 +139,28 @@ const config: Config = {
132
  "100%": { opacity: "1" },
133
  },
134
  fadeUp: {
135
- "0%": { opacity: "0", transform: "translateY(10px)" },
136
  "100%": { opacity: "1", transform: "translateY(0)" },
137
  },
138
- rule: {
139
- "0%": { transform: "scaleX(0)", transformOrigin: "left" },
140
- "100%": { transform: "scaleX(1)", transformOrigin: "left" },
 
 
 
 
 
 
 
 
141
  },
142
- inkPulse: {
143
- "0%, 100%": { opacity: "0.65" },
144
- "50%": { opacity: "1" },
145
  },
146
  thinkingDot: {
147
- "0%, 80%, 100%": { opacity: "0.18" },
148
- "40%": { opacity: "1" },
149
  },
150
  },
151
  },
 
1
  import type { Config } from "tailwindcss";
2
 
3
  /**
4
+ * Etiya doc-to-lora β€” Twilight Atelier design tokens.
5
  *
6
+ * Aesthetic: A research notebook at dusk. Deep indigo canvas warmed by a
7
+ * single luminous amber, atmospheric aurora behind glass surfaces, editorial
8
+ * serif italics carrying display moments. Sober enough for enterprise BSS,
9
+ * soulful enough to belong in an AI atelier.
 
10
  */
11
  const config: Config = {
12
  content: [
 
17
  theme: {
18
  extend: {
19
  colors: {
20
+ // ── Canvas (deep indigo-charcoal with warmth)
21
+ canvas: {
22
+ DEFAULT: "#0a0b10",
23
+ deep: "#06070a",
24
+ 1: "#0e0f17",
25
+ 2: "#14151f",
26
+ 3: "#1c1d29",
27
+ 4: "#262735",
28
  },
29
+ // ── Glass / overlay
30
+ glass: {
31
+ DEFAULT: "rgba(255, 255, 255, 0.04)",
32
+ stronger: "rgba(255, 255, 255, 0.07)",
33
+ border: "rgba(255, 255, 255, 0.10)",
34
+ "border-strong": "rgba(255, 255, 255, 0.16)",
 
 
 
35
  },
36
+ // ── Ink (warm off-white text β€” never pure white)
37
+ ink: {
38
+ DEFAULT: "#f5f3ec",
39
+ 90: "rgba(245, 243, 236, 0.90)",
40
+ 70: "rgba(245, 243, 236, 0.65)",
41
+ 50: "rgba(245, 243, 236, 0.46)",
42
+ 30: "rgba(245, 243, 236, 0.28)",
43
+ 15: "rgba(245, 243, 236, 0.14)",
44
  },
45
+ // ── Single luminous accent (warm amber)
46
+ amber: {
47
+ DEFAULT: "#ffb545",
48
+ soft: "#ffd599",
49
+ deep: "#d68a1f",
50
+ glow: "rgba(255, 181, 69, 0.18)",
51
+ ring: "rgba(255, 181, 69, 0.45)",
52
  },
53
+ // ── Status (warm-tuned, never primary brand colors)
54
  status: {
55
+ ok: "#5dd6a8",
56
+ "ok-glow": "rgba(93, 214, 168, 0.16)",
57
+ warn: "#ffb545",
58
+ "warn-glow": "rgba(255, 181, 69, 0.16)",
59
+ err: "#ff7a7a",
60
+ "err-glow": "rgba(255, 122, 122, 0.16)",
61
  },
62
  },
63
  fontFamily: {
64
+ // Editorial italic display (Google Font: Instrument Serif)
65
+ serif: ["var(--font-instrument)", "ui-serif", "Georgia", "serif"],
66
+ // UI / body (Manrope β€” modern, geometric, warmer than Inter)
67
+ sans: ["var(--font-manrope)", "system-ui", "-apple-system", "sans-serif"],
68
+ // Spec-sheet readouts
69
  mono: ["var(--font-mono)", "ui-monospace", "SF Mono", "Menlo", "monospace"],
 
 
70
  },
71
  fontSize: {
72
+ // Atelier display ladder β€” generous, with -0.02em tracking
73
+ "display-2xl": ["88px", { lineHeight: "0.95", letterSpacing: "-0.04em", fontWeight: "500" }],
74
+ "display-xl": ["64px", { lineHeight: "1.0", letterSpacing: "-0.035em", fontWeight: "500" }],
75
+ "display-lg": ["44px", { lineHeight: "1.05", letterSpacing: "-0.025em", fontWeight: "600" }],
76
+ "display-md": ["32px", { lineHeight: "1.12", letterSpacing: "-0.02em", fontWeight: "600" }],
77
+ "display-sm": ["24px", { lineHeight: "1.2", letterSpacing: "-0.015em", fontWeight: "600" }],
78
+ // Body
79
+ lead: ["20px", { lineHeight: "1.5", letterSpacing: "-0.005em", fontWeight: "400" }],
80
+ body: ["15px", { lineHeight: "1.6", letterSpacing: "0", fontWeight: "400" }],
 
81
  "body-strong": ["15px", { lineHeight: "1.6", letterSpacing: "0", fontWeight: "600" }],
82
  caption: ["13px", { lineHeight: "1.5", letterSpacing: "0", fontWeight: "400" }],
83
  "caption-strong": ["13px", { lineHeight: "1.4", letterSpacing: "0.005em", fontWeight: "600" }],
84
+ micro: ["11px", { lineHeight: "1.4", letterSpacing: "0.05em", fontWeight: "500" }],
85
+ },
86
+ spacing: {
87
+ section: "96px",
88
  },
89
  borderRadius: {
90
  none: "0",
91
+ xs: "6px",
92
+ sm: "10px",
93
+ DEFAULT: "12px",
94
+ md: "14px",
95
+ lg: "20px",
96
+ xl: "28px",
97
+ "2xl": "36px",
98
  pill: "9999px",
99
  },
100
  boxShadow: {
101
+ // The single sanctioned card glow β€” for elevated surfaces
102
+ glass:
103
+ "0 1px 0 rgba(255,255,255,0.05) inset, 0 0 0 1px rgba(255,255,255,0.08), 0 16px 40px -16px rgba(0,0,0,0.6)",
104
+ "glass-lg":
105
+ "0 1px 0 rgba(255,255,255,0.06) inset, 0 0 0 1px rgba(255,255,255,0.10), 0 28px 80px -24px rgba(0,0,0,0.7)",
106
+ // Amber accent glow for active CTAs
107
+ amber:
108
+ "0 0 0 1px rgba(255,181,69,0.42), 0 0 32px -8px rgba(255,181,69,0.45)",
109
+ // Hairline ring (replaces border-1 for crisp edges on glass)
110
+ hairline: "0 0 0 1px rgba(255,255,255,0.08)",
111
+ "hairline-strong": "0 0 0 1px rgba(255,255,255,0.16)",
112
+ },
113
+ backdropBlur: {
114
+ glass: "16px",
115
+ nav: "24px",
116
  },
117
  transitionTimingFunction: {
118
+ // Smooth deceleration β€” feels like Apple's spring
119
  atelier: "cubic-bezier(0.32, 0.72, 0, 1)",
120
  "out-expo": "cubic-bezier(0.16, 1, 0.3, 1)",
121
  },
 
127
  "stagger-2": "fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.16s both",
128
  "stagger-3": "fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.24s both",
129
  "stagger-4": "fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.32s both",
130
+ shimmer: "shimmer 2.4s ease-in-out infinite",
131
+ "pulse-soft": "pulseSoft 2.4s ease-in-out infinite",
132
+ "aurora-1": "aurora1 22s ease-in-out infinite alternate",
133
+ "aurora-2": "aurora2 28s ease-in-out infinite alternate",
134
  "thinking-dot": "thinkingDot 1.4s ease-in-out infinite",
135
  },
136
  keyframes: {
 
139
  "100%": { opacity: "1" },
140
  },
141
  fadeUp: {
142
+ "0%": { opacity: "0", transform: "translateY(12px)" },
143
  "100%": { opacity: "1", transform: "translateY(0)" },
144
  },
145
+ shimmer: {
146
+ "0%, 100%": { opacity: "0.4" },
147
+ "50%": { opacity: "0.85" },
148
+ },
149
+ pulseSoft: {
150
+ "0%, 100%": { opacity: "0.7", transform: "scale(1)" },
151
+ "50%": { opacity: "1", transform: "scale(1.04)" },
152
+ },
153
+ aurora1: {
154
+ "0%": { transform: "translate(-10%, -8%) scale(1)" },
155
+ "100%": { transform: "translate(8%, 10%) scale(1.15)" },
156
  },
157
+ aurora2: {
158
+ "0%": { transform: "translate(8%, 10%) scale(1.1)" },
159
+ "100%": { transform: "translate(-6%, -4%) scale(1)" },
160
  },
161
  thinkingDot: {
162
+ "0%, 80%, 100%": { opacity: "0.25", transform: "translateY(0)" },
163
+ "40%": { opacity: "1", transform: "translateY(-3px)" },
164
  },
165
  },
166
  },