revert: restore Twilight Atelier (drop Risograph Folio)
Browse files- app/documents/page.tsx +155 -189
- app/globals.css +186 -233
- app/layout.tsx +12 -12
- app/system/page.tsx +155 -262
- components/chat/AppShell.tsx +13 -2
- components/chat/Aurora.tsx +35 -25
- components/chat/Composer.tsx +72 -114
- components/chat/Empty.tsx +67 -97
- components/chat/GroundingPill.tsx +18 -12
- components/chat/Message.tsx +114 -168
- components/chat/SettingsDrawer.tsx +115 -64
- components/chat/Sidebar.tsx +157 -205
- components/chat/SourceChips.tsx +46 -51
- components/chat/Thinking.tsx +36 -25
- components/chat/Thread.tsx +9 -32
- components/chat/TopBar.tsx +66 -90
- tailwind.config.ts +103 -88
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-
|
| 58 |
-
{/*
|
| 59 |
-
<header className="animate-fade-in">
|
| 60 |
-
<div
|
| 61 |
-
<
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</div>
|
| 66 |
-
<
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 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 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
>
|
| 116 |
-
<div>
|
| 117 |
-
<span className="
|
| 118 |
-
|
| 119 |
</span>
|
| 120 |
-
<span className="font-mono text-
|
| 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="
|
| 130 |
>
|
| 131 |
-
|
| 132 |
</button>
|
| 133 |
</div>
|
| 134 |
)}
|
| 135 |
|
| 136 |
-
{/* Add
|
| 137 |
{showAdd && (
|
| 138 |
<AddDocumentForm
|
| 139 |
-
onCreated={() =>
|
|
|
|
|
|
|
| 140 |
/>
|
| 141 |
)}
|
| 142 |
|
| 143 |
-
{/* Search */}
|
| 144 |
-
<section className="animate-fade-up-slow">
|
| 145 |
-
<div className="flex items-
|
| 146 |
-
<
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
{filtered.length.toLocaleString()} match
|
| 149 |
{filtered.length === 1 ? "" : "es"}
|
| 150 |
</span>
|
| 151 |
</div>
|
| 152 |
|
| 153 |
-
|
| 154 |
-
<
|
| 155 |
-
className="
|
| 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 |
-
|
| 180 |
-
|
| 181 |
-
fontWeight: 400,
|
| 182 |
}}
|
| 183 |
-
|
| 184 |
-
|
| 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
|
| 200 |
-
Loading
|
| 201 |
</div>
|
| 202 |
)}
|
| 203 |
|
| 204 |
{visible.length > 0 && (
|
| 205 |
-
<ul className="
|
| 206 |
-
{visible.map((d
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
β {folio}
|
| 215 |
</div>
|
| 216 |
-
<div className="
|
| 217 |
-
<
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
>
|
| 221 |
-
|
| 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 |
-
</
|
| 227 |
</div>
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
</
|
| 235 |
-
|
| 236 |
-
|
| 237 |
</ul>
|
| 238 |
)}
|
| 239 |
|
| 240 |
{totalPages > 1 && (
|
| 241 |
-
<div className="flex items-center justify-between mt-
|
| 242 |
<button
|
| 243 |
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
| 244 |
disabled={page === 0}
|
| 245 |
className="btn-ghost disabled:opacity-30"
|
| 246 |
>
|
| 247 |
-
β
|
| 248 |
</button>
|
| 249 |
-
<span className="
|
| 250 |
-
|
| 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<{
|
|
|
|
|
|
|
|
|
|
| 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: `
|
| 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 |
-
<
|
| 307 |
-
<span className="
|
| 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-
|
| 321 |
-
Documents persist immediately; re-indexing syncs them into
|
| 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-
|
| 332 |
>
|
| 333 |
<div>
|
| 334 |
-
<label className="
|
| 335 |
-
|
| 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-
|
| 343 |
disabled={create.isPending}
|
| 344 |
/>
|
| 345 |
</div>
|
| 346 |
|
| 347 |
<div>
|
| 348 |
-
<label className="
|
| 349 |
-
|
| 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-
|
| 358 |
disabled={create.isPending}
|
| 359 |
-
style={{ fontFamily: "var(--font-mono)", fontSize: "13px" }}
|
| 360 |
/>
|
| 361 |
-
<p className="
|
| 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-
|
| 372 |
/>
|
| 373 |
-
<span
|
| 374 |
-
|
|
|
|
| 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-
|
| 384 |
>
|
| 385 |
-
{create.isPending ? "
|
| 386 |
</button>
|
| 387 |
{feedback && (
|
| 388 |
<span
|
| 389 |
className={clsx(
|
| 390 |
"text-caption",
|
| 391 |
-
feedback.kind === "ok" ? "text-status-ok" : "text-
|
| 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="
|
| 423 |
-
style={{ background: "var(--paper)" }}
|
| 424 |
>
|
| 425 |
-
<
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
</div>
|
| 428 |
-
<
|
| 429 |
-
|
| 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-
|
| 448 |
>
|
| 449 |
-
{loading ? "
|
| 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 |
-
|
| 7 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 8 |
@layer base {
|
| 9 |
:root {
|
| 10 |
-
color-scheme:
|
| 11 |
-
--paper: #f4ede0;
|
| 12 |
-
--paper-deep: #ebe2d0;
|
| 13 |
-
--ink: #191713;
|
| 14 |
-
--rust: #c84d2c;
|
| 15 |
}
|
| 16 |
|
| 17 |
html,
|
| 18 |
body {
|
| 19 |
-
background:
|
| 20 |
-
color:
|
| 21 |
-
font-family: var(--font-
|
| 22 |
font-size: 15px;
|
| 23 |
-
line-height: 1.
|
| 24 |
font-weight: 400;
|
| 25 |
-
font-feature-settings: "ss01", "
|
| 26 |
-webkit-font-smoothing: antialiased;
|
| 27 |
-moz-osx-font-smoothing: grayscale;
|
| 28 |
text-rendering: optimizeLegibility;
|
| 29 |
}
|
| 30 |
|
| 31 |
body {
|
| 32 |
-
/* Layered
|
| 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(
|
| 40 |
-
radial-gradient(800px
|
| 41 |
-
|
|
|
|
| 42 |
background-attachment: fixed;
|
| 43 |
min-height: 100vh;
|
| 44 |
overflow-x: hidden;
|
| 45 |
-
position: relative;
|
| 46 |
}
|
| 47 |
|
| 48 |
-
/*
|
| 49 |
body::before {
|
| 50 |
content: "";
|
| 51 |
position: fixed;
|
| 52 |
inset: 0;
|
| 53 |
pointer-events: none;
|
| 54 |
z-index: 0;
|
| 55 |
-
background-image:
|
| 56 |
-
|
| 57 |
-
|
| 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:
|
| 89 |
-
color:
|
| 90 |
}
|
| 91 |
|
| 92 |
-
/*
|
| 93 |
::-webkit-scrollbar {
|
| 94 |
-
width:
|
| 95 |
-
height:
|
| 96 |
}
|
| 97 |
::-webkit-scrollbar-thumb {
|
| 98 |
-
background-color: rgba(
|
| 99 |
border-radius: 9999px;
|
| 100 |
-
border:
|
| 101 |
background-clip: content-box;
|
| 102 |
}
|
| 103 |
::-webkit-scrollbar-thumb:hover {
|
| 104 |
-
background-color: rgba(
|
| 105 |
}
|
| 106 |
::-webkit-scrollbar-track {
|
| 107 |
background: transparent;
|
| 108 |
}
|
| 109 |
|
|
|
|
| 110 |
:focus-visible {
|
| 111 |
-
outline: 2px solid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
outline-offset: 3px;
|
| 113 |
}
|
| 114 |
|
| 115 |
-
/*
|
| 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 |
-
|
| 134 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 135 |
@layer components {
|
| 136 |
-
/* ββ
|
| 137 |
-
.
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
-
|
|
|
|
| 157 |
@apply inline-flex items-center justify-center
|
| 158 |
px-5 py-2.5 rounded-pill
|
| 159 |
-
text-body-strong text-
|
| 160 |
-
bg-
|
| 161 |
transition-all duration-200 ease-atelier
|
| 162 |
active:scale-[0.97]
|
| 163 |
-
|
| 164 |
-
|
| 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-
|
| 172 |
transition-all duration-200 ease-atelier
|
| 173 |
active:scale-[0.97]
|
| 174 |
-
hover:bg-
|
| 175 |
-
box-shadow: 0 0 0 1px rgba(
|
| 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-
|
| 184 |
}
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 194 |
-
inset 0 1px 0 rgba(25, 23, 19, 0.04);
|
| 195 |
}
|
| 196 |
-
.input-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
| 199 |
}
|
| 200 |
-
.input-
|
| 201 |
-
color: rgba(
|
| 202 |
}
|
| 203 |
|
| 204 |
-
/* ββ Cards
|
| 205 |
.card {
|
| 206 |
-
@apply rounded-
|
| 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 |
-
|
| 213 |
-
|
| 214 |
-
|
| 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 |
-
|
| 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 |
-
/*
|
| 284 |
-
.
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
| 290 |
}
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
}
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
bottom: -2px;
|
| 301 |
-
left: 0;
|
| 302 |
-
width: 100%;
|
| 303 |
height: 1px;
|
| 304 |
-
background:
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
| 308 |
}
|
| 309 |
-
.
|
| 310 |
-
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
}
|
| 313 |
}
|
| 314 |
|
|
@@ -316,54 +259,65 @@
|
|
| 316 |
Utilities
|
| 317 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 318 |
@layer utilities {
|
| 319 |
-
.text-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
}
|
| 322 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
}
|
| 325 |
|
| 326 |
-
/*
|
| 327 |
-
.
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
| 332 |
);
|
| 333 |
-
|
|
|
|
| 334 |
}
|
| 335 |
-
.
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
|
|
|
|
|
|
| 340 |
);
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
/* Register marks (corner crosshairs) */
|
| 345 |
-
.register-corners {
|
| 346 |
-
position: relative;
|
| 347 |
}
|
| 348 |
-
.
|
| 349 |
-
.register-corners::after {
|
| 350 |
-
content: "β";
|
| 351 |
position: absolute;
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 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 |
-
|
| 375 |
-
|
| 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 {
|
| 3 |
import "./globals.css";
|
| 4 |
import { Providers } from "./providers";
|
| 5 |
import { AppShell } from "@/components/chat/AppShell";
|
| 6 |
|
| 7 |
-
const
|
| 8 |
subsets: ["latin"],
|
| 9 |
-
variable: "--font-
|
| 10 |
display: "swap",
|
| 11 |
-
|
| 12 |
-
axes: ["SOFT", "WONK", "opsz"],
|
| 13 |
});
|
| 14 |
|
| 15 |
-
const
|
| 16 |
subsets: ["latin"],
|
| 17 |
-
variable: "--font-
|
| 18 |
display: "swap",
|
| 19 |
-
weight: ["
|
|
|
|
| 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
|
| 31 |
description:
|
| 32 |
-
"
|
| 33 |
};
|
| 34 |
|
| 35 |
export default function RootLayout({
|
|
@@ -40,7 +40,7 @@ export default function RootLayout({
|
|
| 40 |
return (
|
| 41 |
<html
|
| 42 |
lang="en"
|
| 43 |
-
className={`${
|
| 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-
|
| 34 |
-
{/*
|
| 35 |
-
<header className="animate-fade-in">
|
| 36 |
-
<div
|
| 37 |
-
<
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
>
|
| 43 |
-
{isFetching ? "Refreshingβ¦" : "Refresh β»"}
|
| 44 |
-
</button>
|
| 45 |
</div>
|
| 46 |
-
<
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
lineHeight: 0.92,
|
| 51 |
-
fontWeight: 300,
|
| 52 |
-
letterSpacing: "-0.04em",
|
| 53 |
-
fontFamily: "var(--font-fraunces)",
|
| 54 |
-
}}
|
| 55 |
>
|
| 56 |
-
|
| 57 |
-
</
|
| 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
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 84 |
-
<section className="animate-fade-up">
|
| 85 |
-
<
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 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-
|
| 132 |
<Latencies />
|
| 133 |
<CorpusBreakdown data={data} />
|
| 134 |
</section>
|
| 135 |
|
| 136 |
-
{/* Eval β
|
| 137 |
-
<section
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
/>
|
| 154 |
-
<div className="
|
| 155 |
-
<
|
| 156 |
-
|
| 157 |
-
</
|
| 158 |
-
<
|
| 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 |
-
<
|
| 201 |
-
|
| 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-
|
| 213 |
-
The
|
| 214 |
-
Incremental
|
| 215 |
</p>
|
| 216 |
</div>
|
| 217 |
|
| 218 |
-
<div className="grid md:grid-cols-2 gap-
|
| 219 |
<ReindexCard
|
| 220 |
title="Incremental"
|
| 221 |
-
description="Sync new and removed
|
| 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
|
| 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-
|
| 246 |
-
<div className="
|
| 247 |
-
|
| 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
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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="
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 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
|
| 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 |
-
<
|
| 371 |
-
|
| 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
|
| 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 |
-
<
|
| 398 |
-
|
| 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 & <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 |
-
|
| 432 |
onClick,
|
| 433 |
}: {
|
| 434 |
title: string;
|
|
@@ -436,32 +329,30 @@ function ReindexCard({
|
|
| 436 |
cost: string;
|
| 437 |
latency: string;
|
| 438 |
loading: boolean;
|
| 439 |
-
|
| 440 |
onClick: () => void;
|
| 441 |
}) {
|
| 442 |
return (
|
| 443 |
-
<div className=
|
| 444 |
<div>
|
| 445 |
-
<div className="flex items-
|
| 446 |
-
<
|
| 447 |
-
<span className="
|
|
|
|
|
|
|
| 448 |
</div>
|
| 449 |
-
<
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 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={
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 475 |
-
<span className="
|
| 476 |
-
|
|
|
|
|
|
|
| 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 & 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
*
|
| 5 |
-
*
|
| 6 |
-
* Pure decoration. Pinned behind the page.
|
| 7 |
*/
|
| 8 |
export function Aurora() {
|
| 9 |
return (
|
| 10 |
-
<
|
| 11 |
-
|
| 12 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 51 |
-
<div className="max-w-[
|
| 52 |
-
<div
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
<
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
}}
|
| 66 |
-
|
| 67 |
-
|
| 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 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 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="
|
| 165 |
>
|
| 166 |
{icon}
|
| 167 |
-
|
| 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="
|
| 190 |
className={clsx(
|
| 191 |
-
"shrink-0 inline-flex items-center
|
| 192 |
-
"active:scale-[0.
|
| 193 |
disabled
|
| 194 |
-
? "bg-
|
| 195 |
-
: "bg-
|
| 196 |
)}
|
| 197 |
>
|
| 198 |
-
<
|
| 199 |
-
Submit
|
| 200 |
-
</span>
|
| 201 |
-
<svg width="11" height="11" viewBox="0 0 11 11" fill="none">
|
| 202 |
<path
|
| 203 |
-
d="M2
|
| 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-[
|
| 17 |
-
{/*
|
| 18 |
-
<div className="flex
|
| 19 |
-
<
|
| 20 |
-
|
| 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 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
fontVariationSettings: '"SOFT" 100, "WONK" 1, "opsz" 144',
|
| 44 |
}}
|
| 45 |
>
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
{/*
|
| 53 |
-
<div className="grid
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 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 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
<
|
| 85 |
-
<
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 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
|
| 44 |
-
ok: "
|
| 45 |
-
warn: "
|
| 46 |
-
err: "
|
| 47 |
-
neutral: "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 55 |
-
|
| 56 |
-
|
| 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-
|
| 70 |
-
err: "bg-
|
| 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 |
-
<
|
| 21 |
-
<div className="
|
| 22 |
-
<
|
| 23 |
</div>
|
| 24 |
-
<div className="
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 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 |
-
</
|
| 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 |
-
<
|
| 64 |
-
|
| 65 |
-
<
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</div>
|
| 82 |
-
|
| 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
|
| 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-
|
| 147 |
-
<
|
| 148 |
-
<
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
| 162 |
</footer>
|
| 163 |
-
</
|
| 164 |
-
</
|
| 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 |
-
<
|
| 179 |
-
<
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
<
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
>
|
| 191 |
-
{e.message}
|
| 192 |
-
</p>
|
| 193 |
{e.status === 502 && (
|
| 194 |
-
<
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
</p>
|
| 198 |
)}
|
| 199 |
-
</
|
| 200 |
-
</
|
| 201 |
);
|
| 202 |
}
|
| 203 |
|
| 204 |
function Spec({
|
| 205 |
label,
|
| 206 |
value,
|
| 207 |
-
|
| 208 |
}: {
|
| 209 |
label: string;
|
| 210 |
value: string;
|
| 211 |
-
|
| 212 |
}) {
|
| 213 |
return (
|
| 214 |
-
<
|
| 215 |
-
<
|
| 216 |
-
<
|
| 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 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
{
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-[
|
| 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-
|
| 74 |
-
<div
|
| 75 |
-
<
|
| 76 |
-
<
|
| 77 |
-
|
| 78 |
-
|
| 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 |
-
<
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
>
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
| 101 |
</header>
|
| 102 |
|
| 103 |
-
<div className="flex-1 overflow-y-auto px-
|
| 104 |
-
{/*
|
| 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-
|
| 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-
|
| 136 |
<button
|
| 137 |
onClick={reset}
|
| 138 |
-
className="
|
| 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 =
|
| 170 |
-
|
|
|
|
| 171 |
meta.step < 1 ? value.toFixed(2) : Math.round(value).toString();
|
| 172 |
|
| 173 |
return (
|
| 174 |
<div>
|
| 175 |
-
<div className="flex items-
|
| 176 |
<label className="text-body-strong text-ink">{meta.label}</label>
|
| 177 |
<span
|
| 178 |
className={clsx(
|
| 179 |
-
"
|
| 180 |
-
isDefault ? "text-ink-50" : "text-
|
| 181 |
)}
|
| 182 |
>
|
| 183 |
-
{
|
| 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-
|
| 194 |
aria-label={meta.label}
|
| 195 |
/>
|
| 196 |
-
<p className="text-
|
| 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-
|
| 217 |
)}
|
| 218 |
-
style={{ boxShadow: "
|
| 219 |
>
|
| 220 |
<span
|
| 221 |
className={clsx(
|
| 222 |
-
"absolute top-0.5 w-4 h-4 rounded-full bg-
|
| 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
|
| 26 |
"transition-[width,opacity] duration-300 ease-atelier",
|
| 27 |
sidebarOpen
|
| 28 |
-
? "w-[
|
| 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 |
-
|
| 38 |
-
|
| 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="
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
style={{ boxShadow: "0 0 0 1px rgba(
|
| 71 |
>
|
| 72 |
-
<
|
| 73 |
-
<
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 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
|
| 88 |
-
<nav className="px-3 pb-
|
| 89 |
-
<NavLink
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
<NavLink
|
| 91 |
href="/documents"
|
| 92 |
active={pathname.startsWith("/documents")}
|
| 93 |
-
|
| 94 |
-
|
| 95 |
/>
|
| 96 |
<NavLink
|
| 97 |
href="/system"
|
| 98 |
active={pathname.startsWith("/system")}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
/>
|
| 102 |
</nav>
|
| 103 |
|
| 104 |
-
<div className="hairline mx-
|
| 105 |
|
| 106 |
-
{/*
|
| 107 |
-
<div className="flex-1 overflow-y-auto
|
| 108 |
-
<div className="px-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
| 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
|
| 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="
|
| 218 |
>
|
| 219 |
-
<span>
|
| 220 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 244 |
-
active
|
|
|
|
|
|
|
| 245 |
)}
|
| 246 |
>
|
| 247 |
-
<span
|
| 248 |
-
|
| 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 "
|
| 285 |
-
if (min < 60) return `${min}m
|
| 286 |
const hr = Math.floor(min / 60);
|
| 287 |
-
if (hr < 24) return `${hr}h
|
| 288 |
const d = Math.floor(hr / 24);
|
| 289 |
-
if (d < 7) return `${d}d
|
| 290 |
-
return new Date(ts).toLocaleDateString(undefined, {
|
| 291 |
-
|
| 292 |
-
|
| 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 |
-
<
|
| 17 |
-
<div className="
|
| 18 |
-
|
| 19 |
-
<span className="folio-chrome">{docs.length} entr{docs.length === 1 ? "y" : "ies"}</span>
|
| 20 |
</div>
|
| 21 |
-
<ul className="
|
| 22 |
-
{docs.map((d
|
| 23 |
const isOpen = expanded === d.doc_id;
|
| 24 |
const sim = d.similarity ?? d.dense_similarity ?? 0;
|
| 25 |
return (
|
| 26 |
-
<li key={d.doc_id}
|
| 27 |
<button
|
| 28 |
onClick={() => setExpanded(isOpen ? null : d.doc_id)}
|
| 29 |
-
className=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
>
|
| 31 |
-
<
|
| 32 |
-
|
| 33 |
-
|
| 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 |
-
</
|
| 41 |
-
<
|
| 42 |
{d.doc_id}
|
| 43 |
-
</
|
| 44 |
-
</
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
</
|
| 75 |
);
|
| 76 |
}
|
| 77 |
|
| 78 |
function Metric({ label, value }: { label: string; value: string }) {
|
| 79 |
return (
|
| 80 |
<div>
|
| 81 |
-
<div className="
|
| 82 |
-
|
| 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 |
-
? "
|
| 102 |
: v >= 0.45
|
| 103 |
-
? "
|
| 104 |
-
: "rgba(
|
| 105 |
-
const r =
|
| 106 |
const C = 2 * Math.PI * r;
|
| 107 |
const dash = C * v;
|
| 108 |
return (
|
| 109 |
-
<svg width="
|
| 110 |
<circle
|
| 111 |
-
cx="
|
| 112 |
-
cy="
|
| 113 |
r={r}
|
| 114 |
-
stroke="rgba(
|
| 115 |
-
strokeWidth="
|
| 116 |
fill="none"
|
| 117 |
/>
|
| 118 |
<circle
|
| 119 |
-
cx="
|
| 120 |
-
cy="
|
| 121 |
r={r}
|
| 122 |
stroke={tone}
|
| 123 |
-
strokeWidth="
|
| 124 |
fill="none"
|
| 125 |
strokeLinecap="round"
|
| 126 |
strokeDasharray={`${dash} ${C - dash}`}
|
| 127 |
-
transform="rotate(-90
|
| 128 |
style={{ transition: "stroke-dasharray 0.4s cubic-bezier(0.16,1,0.3,1)" }}
|
| 129 |
/>
|
| 130 |
<text
|
| 131 |
-
x="
|
| 132 |
-
y="
|
| 133 |
textAnchor="middle"
|
| 134 |
-
fontSize="
|
| 135 |
-
fill="rgba(
|
| 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 = ["
|
| 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="
|
| 12 |
-
<
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 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-
|
| 39 |
style={{ animationDelay: "0s" }}
|
| 40 |
/>
|
| 41 |
<span
|
| 42 |
-
className="w-1.5 h-1.5 rounded-full bg-
|
| 43 |
-
style={{ animationDelay: "0.
|
| 44 |
/>
|
| 45 |
<span
|
| 46 |
-
className="w-1.5 h-1.5 rounded-full bg-
|
| 47 |
-
style={{ animationDelay: "0.
|
| 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-[
|
| 26 |
-
{
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 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 |
-
|
| 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="
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
<div className="
|
| 55 |
-
<
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 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 |
-
</
|
| 94 |
-
</
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
<span
|
| 99 |
-
className=
|
| 100 |
-
|
| 101 |
-
|
| 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 |
-
? "
|
| 118 |
-
: "
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</span>
|
| 121 |
-
</
|
| 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-
|
| 132 |
style={{
|
| 133 |
-
background:
|
| 134 |
-
|
|
|
|
|
|
|
| 135 |
}}
|
| 136 |
>
|
| 137 |
-
<
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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 β
|
| 5 |
*
|
| 6 |
-
* A
|
| 7 |
-
*
|
| 8 |
-
* carrying display moments
|
| 9 |
-
*
|
| 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 |
-
// ββ
|
| 22 |
-
|
| 23 |
-
DEFAULT: "#
|
| 24 |
-
deep: "#
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
},
|
| 28 |
-
// ββ
|
| 29 |
-
|
| 30 |
-
DEFAULT: "
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 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 |
-
// ββ
|
| 39 |
-
|
| 40 |
-
DEFAULT: "#
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
},
|
| 47 |
-
// ββ
|
| 48 |
-
|
| 49 |
-
DEFAULT: "#
|
| 50 |
-
soft: "#
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
},
|
| 53 |
-
// ββ Status (
|
| 54 |
status: {
|
| 55 |
-
ok: "#
|
| 56 |
-
"ok-glow": "rgba(
|
| 57 |
-
warn: "#
|
| 58 |
-
"warn-glow": "rgba(
|
| 59 |
-
err: "#
|
| 60 |
-
"err-glow": "rgba(
|
| 61 |
},
|
| 62 |
},
|
| 63 |
fontFamily: {
|
| 64 |
-
//
|
| 65 |
-
|
| 66 |
-
// UI / body
|
| 67 |
-
sans: ["var(--font-
|
| 68 |
-
//
|
| 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 |
-
//
|
| 75 |
-
|
| 76 |
-
"display-
|
| 77 |
-
"display-
|
| 78 |
-
"display-
|
| 79 |
-
"display-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 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 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
| 90 |
},
|
| 91 |
borderRadius: {
|
| 92 |
none: "0",
|
| 93 |
-
xs: "
|
| 94 |
-
sm: "
|
| 95 |
-
DEFAULT: "
|
| 96 |
-
md: "
|
| 97 |
-
lg: "
|
|
|
|
|
|
|
| 98 |
pill: "9999px",
|
| 99 |
},
|
| 100 |
boxShadow: {
|
| 101 |
-
//
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
"
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 124 |
-
"
|
| 125 |
-
"
|
| 126 |
-
"
|
| 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(
|
| 136 |
"100%": { opacity: "1", transform: "translateY(0)" },
|
| 137 |
},
|
| 138 |
-
|
| 139 |
-
"0%": {
|
| 140 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
},
|
| 142 |
-
|
| 143 |
-
"0%
|
| 144 |
-
"
|
| 145 |
},
|
| 146 |
thinkingDot: {
|
| 147 |
-
"0%, 80%, 100%": { opacity: "0.
|
| 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 |
},
|