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