Berkkirik commited on
Commit
6db5454
·
0 Parent(s):

Deploy: 2026-05-04T11:20:23Z

Browse files
.dockerignore ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Next.js
2
+ .next/
3
+ out/
4
+ dist/
5
+
6
+ # Dependencies — installed inside the image
7
+ node_modules/
8
+ npm-debug.log*
9
+
10
+ # Local env (NEVER ship secrets in the image)
11
+ .env
12
+ .env.local
13
+ .env.*.local
14
+
15
+ # Git / IDE
16
+ .git/
17
+ .gitignore
18
+ .vscode/
19
+ .idea/
20
+ .DS_Store
21
+
22
+ # Tests / coverage
23
+ coverage/
24
+ .nyc_output/
25
+
26
+ # Docs that don't belong in image
27
+ README.md
28
+ CLAUDE.md
29
+ *.md
30
+ !app/**/*.md # keep any in-app markdown files
31
+
32
+ # Build artifacts
33
+ *.tsbuildinfo
34
+ next-env.d.ts
35
+
36
+ # Other
37
+ public/.DS_Store
.env.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─── Etiya doc-to-lora UI — environment ──────────────────────────────
2
+ #
3
+ # Copy to `.env.local` and fill in.
4
+ # `.env.local` is gitignored. Never commit the real token.
5
+ #
6
+ # Auth model:
7
+ # - HF_TOKEN is read by Next.js server-side ONLY (no NEXT_PUBLIC_ prefix).
8
+ # - Browser calls /api/proxy/* → Next server adds Authorization header
9
+ # before forwarding to https://etiya-d2l-api.hf.space/.
10
+ # - Token never reaches the browser. Safe in production.
11
+ #
12
+
13
+ # Required: HuggingFace write token with access to the Etiya/d2l-api Space.
14
+ # Get from https://huggingface.co/settings/tokens
15
+ HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
16
+
17
+ # Optional: override backend URL (default points to production HF Space).
18
+ D2L_API_URL=https://etiya-d2l-api.hf.space
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .next/
3
+ out/
4
+ dist/
5
+ .DS_Store
6
+ *.log
7
+
8
+ # Local env files
9
+ .env.local
10
+ .env.*.local
11
+ .env
12
+
13
+ # IDE
14
+ .vscode/
15
+ .idea/
16
+
17
+ # OS
18
+ Thumbs.db
19
+
20
+ # TypeScript build info
21
+ *.tsbuildinfo
22
+ next-env.d.ts
CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
Dockerfile ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.7
2
+
3
+ # ─── Stage 1: deps ─────────────────────────────────────────────────
4
+ # Install only production dependencies. node_modules cached aggressively.
5
+ FROM node:20-alpine AS deps
6
+ RUN apk add --no-cache libc6-compat
7
+ WORKDIR /app
8
+
9
+ # Lockfile-first copy enables Docker layer caching. Re-runs only on
10
+ # package.json / lockfile changes, not on source edits.
11
+ COPY package.json package-lock.json* ./
12
+ RUN npm ci --no-audit --no-fund
13
+
14
+
15
+ # ─── Stage 2: builder ──────────────────────────────────────────────
16
+ # Compile Next.js with standalone output. Outputs go to .next/standalone
17
+ # and .next/static — both copied into the runtime stage.
18
+ FROM node:20-alpine AS builder
19
+ WORKDIR /app
20
+
21
+ COPY --from=deps /app/node_modules ./node_modules
22
+ COPY . .
23
+
24
+ # Build-time env: NEXT_TELEMETRY_DISABLED suppresses telemetry; no secrets here.
25
+ # HF_TOKEN is intentionally NOT baked in — it's passed at runtime.
26
+ ENV NEXT_TELEMETRY_DISABLED=1
27
+ RUN npm run build
28
+
29
+
30
+ # ─── Stage 3: runner ───────────────────────────────────────────────
31
+ # Minimal runtime image. Only contains the compiled standalone server,
32
+ # static assets, and node binary.
33
+ FROM node:20-alpine AS runner
34
+ WORKDIR /app
35
+
36
+ ENV NODE_ENV=production
37
+ ENV NEXT_TELEMETRY_DISABLED=1
38
+ # Default to 7860 (HF Spaces convention). Local docker-compose overrides to 3000.
39
+ ENV PORT=7860
40
+ ENV HOSTNAME=0.0.0.0
41
+
42
+ # Run as non-root for defense-in-depth (HF Spaces injects USER 1000 too).
43
+ RUN addgroup --system --gid 1001 nextjs \
44
+ && adduser --system --uid 1001 --ingroup nextjs nextjs
45
+
46
+ # Standalone bundle: server.js + minimum required node_modules.
47
+ COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./
48
+ COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static
49
+ COPY --from=builder --chown=nextjs:nextjs /app/public ./public
50
+
51
+ USER nextjs
52
+ EXPOSE 7860
53
+
54
+ # Healthcheck — uses ash builtin to expand $PORT.
55
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
56
+ CMD wget --quiet --tries=1 --spider "http://localhost:${PORT:-7860}" || exit 1
57
+
58
+ # server.js (generated by next build with standalone output) reads PORT env var.
59
+ CMD ["node", "server.js"]
README.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: doc-to-lora UI
3
+ emoji: 🪞
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ suggested_hardware: cpu-basic
9
+ pinned: false
10
+ ---
11
+
12
+ # Etiya doc-to-lora — Web UI
13
+
14
+ Companion frontend for [`Etiya/d2l-api`](https://huggingface.co/spaces/Etiya/d2l-api).
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ Browser
20
+
21
+
22
+ Etiya/d2l-ui (this Space — Docker SDK, CPU basic, Next.js)
23
+
24
+ │ /api/proxy/* — server-side, adds Authorization: Bearer ${HF_TOKEN}
25
+
26
+ Etiya/d2l-api (backend Space — A100, doc-to-lora + Gemma-2-2b-it)
27
+ ```
28
+
29
+ The frontend calls the backend Space using a server-side proxy. The HF token is set as a Space Secret on this UI Space and never reaches the browser.
30
+
31
+ ## Pages
32
+
33
+ - `/` — hero + quick ask
34
+ - `/ask` — chat interface with advanced parameter sliders
35
+ - `/documents` — CRUD for the 1,166 BSS document corpus
36
+ - `/system` — health, GPU, latency, re-index controls
37
+
38
+ ## Required Space secret
39
+
40
+ Set on this Space's `Settings → Variables and secrets → New secret`:
41
+
42
+ | Name | Type | Value |
43
+ |---|---|---|
44
+ | `HF_TOKEN` | Secret | An HF write token with read access to `Etiya/d2l-api` |
45
+
46
+ Optional:
47
+
48
+ | Name | Type | Default |
49
+ |---|---|---|
50
+ | `D2L_API_URL` | Variable | `https://etiya-d2l-api.hf.space` |
51
+
52
+ ## Local development
53
+
54
+ This Space is a regular Next.js 14 app. To work on it locally:
55
+
56
+ ```bash
57
+ git clone https://huggingface.co/spaces/Etiya/d2l-ui
58
+ cd d2l-ui
59
+ npm install
60
+ cp .env.example .env.local # add HF_TOKEN
61
+ npm run dev # http://localhost:3000
62
+ ```
63
+
64
+ Or via Docker:
65
+
66
+ ```bash
67
+ docker compose up --build # http://localhost:3000
68
+ ```
69
+
70
+ ## Hardware
71
+
72
+ This Space runs on `cpu-basic` (free) — Next.js is light. The heavy lifting (GPU inference) happens on the backend Space.
SPACE_README.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: doc-to-lora UI
3
+ emoji: 🪞
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ suggested_hardware: cpu-basic
9
+ pinned: false
10
+ ---
11
+
12
+ # Etiya doc-to-lora — Web UI
13
+
14
+ Companion frontend for [`Etiya/d2l-api`](https://huggingface.co/spaces/Etiya/d2l-api).
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ Browser
20
+
21
+
22
+ Etiya/d2l-ui (this Space — Docker SDK, CPU basic, Next.js)
23
+
24
+ │ /api/proxy/* — server-side, adds Authorization: Bearer ${HF_TOKEN}
25
+
26
+ Etiya/d2l-api (backend Space — A100, doc-to-lora + Gemma-2-2b-it)
27
+ ```
28
+
29
+ The frontend calls the backend Space using a server-side proxy. The HF token is set as a Space Secret on this UI Space and never reaches the browser.
30
+
31
+ ## Pages
32
+
33
+ - `/` — hero + quick ask
34
+ - `/ask` — chat interface with advanced parameter sliders
35
+ - `/documents` — CRUD for the 1,166 BSS document corpus
36
+ - `/system` — health, GPU, latency, re-index controls
37
+
38
+ ## Required Space secret
39
+
40
+ Set on this Space's `Settings → Variables and secrets → New secret`:
41
+
42
+ | Name | Type | Value |
43
+ |---|---|---|
44
+ | `HF_TOKEN` | Secret | An HF write token with read access to `Etiya/d2l-api` |
45
+
46
+ Optional:
47
+
48
+ | Name | Type | Default |
49
+ |---|---|---|
50
+ | `D2L_API_URL` | Variable | `https://etiya-d2l-api.hf.space` |
51
+
52
+ ## Local development
53
+
54
+ This Space is a regular Next.js 14 app. To work on it locally:
55
+
56
+ ```bash
57
+ git clone https://huggingface.co/spaces/Etiya/d2l-ui
58
+ cd d2l-ui
59
+ npm install
60
+ cp .env.example .env.local # add HF_TOKEN
61
+ npm run dev # http://localhost:3000
62
+ ```
63
+
64
+ Or via Docker:
65
+
66
+ ```bash
67
+ docker compose up --build # http://localhost:3000
68
+ ```
69
+
70
+ ## Hardware
71
+
72
+ This Space runs on `cpu-basic` (free) — Next.js is light. The heavy lifting (GPU inference) happens on the backend Space.
app/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
app/api/proxy/[...path]/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
app/api/proxy/[...path]/route.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Server-side proxy to https://etiya-d2l-api.hf.space.
3
+ *
4
+ * Browser → /api/proxy/<path> (NO auth header in browser request)
5
+ * ↓
6
+ * Next.js server adds Authorization: Bearer ${HF_TOKEN}
7
+ * ↓
8
+ * Forwards to D2L_API_URL/<path>
9
+ *
10
+ * Why a proxy and not NEXT_PUBLIC_HF_TOKEN?
11
+ * - NEXT_PUBLIC_* values are inlined into the client bundle.
12
+ * Anyone viewing the page source can read them.
13
+ * - Server-only env vars (no NEXT_PUBLIC_) stay on the server.
14
+ * The browser cannot read them. This is the only safe pattern
15
+ * for Bearer tokens.
16
+ */
17
+
18
+ import { NextRequest, NextResponse } from "next/server";
19
+
20
+ const API_URL = process.env.D2L_API_URL || "https://etiya-d2l-api.hf.space";
21
+ const TOKEN = process.env.HF_TOKEN;
22
+
23
+ export const dynamic = "force-dynamic";
24
+ export const runtime = "nodejs";
25
+
26
+ async function handle(req: NextRequest, params: { path: string[] }) {
27
+ if (!TOKEN) {
28
+ return NextResponse.json(
29
+ {
30
+ error:
31
+ "HF_TOKEN env var not set on server. Copy .env.example to .env.local and add your token.",
32
+ },
33
+ { status: 500 }
34
+ );
35
+ }
36
+
37
+ const path = (params.path ?? []).join("/");
38
+ const search = req.nextUrl.search; // includes leading "?"
39
+ const upstreamUrl = `${API_URL}/${path}${search}`;
40
+
41
+ const upstreamHeaders: Record<string, string> = {
42
+ Authorization: `Bearer ${TOKEN}`,
43
+ Accept: "application/json",
44
+ };
45
+
46
+ let body: BodyInit | null = null;
47
+ if (req.method !== "GET" && req.method !== "HEAD") {
48
+ const contentType = req.headers.get("content-type") || "application/json";
49
+ upstreamHeaders["Content-Type"] = contentType;
50
+ body = await req.text();
51
+ }
52
+
53
+ let resp: Response;
54
+ try {
55
+ resp = await fetch(upstreamUrl, {
56
+ method: req.method,
57
+ headers: upstreamHeaders,
58
+ body,
59
+ cache: "no-store",
60
+ });
61
+ } catch (e: unknown) {
62
+ return NextResponse.json(
63
+ {
64
+ error: "upstream_unreachable",
65
+ message: e instanceof Error ? e.message : String(e),
66
+ upstream: upstreamUrl,
67
+ },
68
+ { status: 502 }
69
+ );
70
+ }
71
+
72
+ // Stream the upstream response body back to the client.
73
+ // Forward content-type and status; strip auth-sensitive headers.
74
+ const contentType = resp.headers.get("content-type") || "application/json";
75
+ const text = await resp.text();
76
+ return new NextResponse(text, {
77
+ status: resp.status,
78
+ headers: { "content-type": contentType },
79
+ });
80
+ }
81
+
82
+ // Next.js 15+ — `params` is a Promise that must be awaited inside the route.
83
+ type RouteCtx = { params: Promise<{ path: string[] }> };
84
+
85
+ export async function GET(req: NextRequest, ctx: RouteCtx) {
86
+ return handle(req, await ctx.params);
87
+ }
88
+ export async function POST(req: NextRequest, ctx: RouteCtx) {
89
+ return handle(req, await ctx.params);
90
+ }
91
+ export async function PUT(req: NextRequest, ctx: RouteCtx) {
92
+ return handle(req, await ctx.params);
93
+ }
94
+ export async function DELETE(req: NextRequest, ctx: RouteCtx) {
95
+ return handle(req, await ctx.params);
96
+ }
97
+ export async function PATCH(req: NextRequest, ctx: RouteCtx) {
98
+ return handle(req, await ctx.params);
99
+ }
app/api/proxy/route.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Sibling route to `[...path]/route.ts` that handles the upstream root
3
+ * (`GET /` on the backend — health/summary).
4
+ *
5
+ * Why: Next 15's catch-all segment `[...path]` does not match an empty
6
+ * path, so `fetch("/api/proxy/")` 308-redirects to `/api/proxy` and 404s.
7
+ * `lib/api.ts` calls `health()` as `request("")`, which lands here.
8
+ */
9
+
10
+ import { NextRequest, NextResponse } from "next/server";
11
+
12
+ const API_URL = process.env.D2L_API_URL || "https://etiya-d2l-api.hf.space";
13
+ const TOKEN = process.env.HF_TOKEN;
14
+
15
+ export const dynamic = "force-dynamic";
16
+ export const runtime = "nodejs";
17
+
18
+ async function forwardRoot(req: NextRequest) {
19
+ if (!TOKEN) {
20
+ return NextResponse.json(
21
+ {
22
+ error:
23
+ "HF_TOKEN env var not set on server. Copy .env.example to .env.local and add your token.",
24
+ },
25
+ { status: 500 }
26
+ );
27
+ }
28
+
29
+ const upstreamUrl = `${API_URL}/${req.nextUrl.search}`;
30
+
31
+ const upstreamHeaders: Record<string, string> = {
32
+ Authorization: `Bearer ${TOKEN}`,
33
+ Accept: "application/json",
34
+ };
35
+
36
+ let body: BodyInit | null = null;
37
+ if (req.method !== "GET" && req.method !== "HEAD") {
38
+ const contentType = req.headers.get("content-type") || "application/json";
39
+ upstreamHeaders["Content-Type"] = contentType;
40
+ body = await req.text();
41
+ }
42
+
43
+ let resp: Response;
44
+ try {
45
+ resp = await fetch(upstreamUrl, {
46
+ method: req.method,
47
+ headers: upstreamHeaders,
48
+ body,
49
+ cache: "no-store",
50
+ });
51
+ } catch (e: unknown) {
52
+ return NextResponse.json(
53
+ {
54
+ error: "upstream_unreachable",
55
+ message: e instanceof Error ? e.message : String(e),
56
+ upstream: upstreamUrl,
57
+ },
58
+ { status: 502 }
59
+ );
60
+ }
61
+
62
+ const contentType = resp.headers.get("content-type") || "application/json";
63
+ const text = await resp.text();
64
+ return new NextResponse(text, {
65
+ status: resp.status,
66
+ headers: { "content-type": contentType },
67
+ });
68
+ }
69
+
70
+ export const GET = forwardRoot;
71
+ export const POST = forwardRoot;
72
+ export const PUT = forwardRoot;
73
+ export const DELETE = forwardRoot;
74
+ export const PATCH = forwardRoot;
app/ask/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
app/ask/page.tsx ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { AskInput } from "@/components/feature/AskInput";
4
+ import { AnswerCard } from "@/components/feature/AnswerCard";
5
+ import {
6
+ AdvancedParams,
7
+ DEFAULTS,
8
+ } from "@/components/feature/AdvancedParams";
9
+ import { Spinner } from "@/components/ui/Spinner";
10
+ import { api, ApiError } from "@/lib/api";
11
+ import { AskSmartRequest, AskSmartResponse } from "@/lib/types";
12
+ import { useMutation } from "@tanstack/react-query";
13
+ import { useState } from "react";
14
+
15
+ type ConversationItem = {
16
+ id: string;
17
+ question: string;
18
+ response?: AskSmartResponse;
19
+ error?: { status: number; message: string };
20
+ pending?: boolean;
21
+ };
22
+
23
+ export default function AskPage() {
24
+ const [conversation, setConversation] = useState<ConversationItem[]>([]);
25
+ const [params, setParams] = useState<AskSmartRequest>({
26
+ question: "",
27
+ ...DEFAULTS,
28
+ });
29
+
30
+ const askMutation = useMutation<AskSmartResponse, unknown, string>({
31
+ mutationFn: async (q: string) => api.askSmart({ ...params, question: q }),
32
+ });
33
+
34
+ const handleAsk = (q: string) => {
35
+ const id = crypto.randomUUID();
36
+ setConversation((c) => [...c, { id, question: q, pending: true }]);
37
+ askMutation.mutate(q, {
38
+ onSuccess: (resp) =>
39
+ setConversation((c) =>
40
+ c.map((it) =>
41
+ it.id === id ? { ...it, response: resp, pending: false } : it
42
+ )
43
+ ),
44
+ onError: (err) => {
45
+ const status = err instanceof ApiError ? err.status : 0;
46
+ const message = err instanceof Error ? err.message : String(err);
47
+ setConversation((c) =>
48
+ c.map((it) =>
49
+ it.id === id
50
+ ? { ...it, error: { status, message }, pending: false }
51
+ : it
52
+ )
53
+ );
54
+ },
55
+ });
56
+ };
57
+
58
+ return (
59
+ <>
60
+ {/* ───── Hero strip (parchment, condensed) ──────────────────── */}
61
+ <section className="tile-parchment !py-12">
62
+ <div className="container-narrow text-center">
63
+ <h1 className="text-display-lg">Ask</h1>
64
+ <p className="text-body text-ink-48 mt-2">
65
+ Tune retrieval &amp; inference parameters in real time.
66
+ </p>
67
+ </div>
68
+ </section>
69
+
70
+ {/* ───── Conversation surface ──────────────────────────────── */}
71
+ <section className="tile-light">
72
+ <div className="container-narrow space-y-8">
73
+ {conversation.length === 0 && <EmptyState />}
74
+
75
+ {conversation.map((it) => (
76
+ <div key={it.id} className="space-y-3 animate-fade-in">
77
+ {it.pending && <ThinkingPlaceholder question={it.question} />}
78
+ {it.error && <ErrorCard error={it.error} question={it.question} />}
79
+ {it.response && (
80
+ <AnswerCard response={it.response} question={it.question} />
81
+ )}
82
+ </div>
83
+ ))}
84
+
85
+ <div className="space-y-4 pt-4">
86
+ <AskInput
87
+ onAsk={handleAsk}
88
+ loading={askMutation.isPending}
89
+ placeholder="Ask another question…"
90
+ />
91
+ <div id="advanced">
92
+ <AdvancedParams values={params} onChange={setParams} />
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </section>
97
+ </>
98
+ );
99
+ }
100
+
101
+ function EmptyState() {
102
+ return (
103
+ <div className="text-center py-section animate-fade-in">
104
+ <p className="text-display-md text-ink-48">
105
+ Type a question to begin.
106
+ </p>
107
+ <p className="text-body text-ink-48 mt-2">
108
+ Adjust thresholds in the panel below to see how retrieval changes the answer.
109
+ </p>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ function ThinkingPlaceholder({ question }: { question: string }) {
115
+ return (
116
+ <div className="bg-canvas rounded-lg shadow-product p-8 flex items-center gap-4">
117
+ <Spinner className="text-primary" />
118
+ <div className="flex-1 min-w-0">
119
+ <div className="text-caption text-ink-48 truncate">{question}</div>
120
+ <div className="text-body-strong text-ink mt-1">
121
+ Retrieving · reranking · grounding · generating…
122
+ </div>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function ErrorCard({
129
+ error,
130
+ question,
131
+ }: {
132
+ error: { status: number; message: string };
133
+ question: string;
134
+ }) {
135
+ return (
136
+ <div className="utility-card border-status-err/20">
137
+ <p className="text-caption-strong text-status-err uppercase tracking-[0.08em]">
138
+ Error · HTTP {error.status || "—"}
139
+ </p>
140
+ <p className="text-body-strong mt-2">{error.message}</p>
141
+ <p className="text-caption text-ink-48 mt-2">
142
+ Question: <span className="text-mono">{question}</span>
143
+ </p>
144
+ </div>
145
+ );
146
+ }
app/documents/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
app/documents/page.tsx ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Spinner } from "@/components/ui/Spinner";
4
+ import { api, ApiError } from "@/lib/api";
5
+ import { DocumentMeta, ReindexResponse } from "@/lib/types";
6
+ import {
7
+ useMutation,
8
+ useQuery,
9
+ useQueryClient,
10
+ } from "@tanstack/react-query";
11
+ import { useMemo, useState } from "react";
12
+
13
+ export default function DocumentsPage() {
14
+ const queryClient = useQueryClient();
15
+ const [filter, setFilter] = useState("");
16
+ const [page, setPage] = useState(0);
17
+ const [confirmDelete, setConfirmDelete] = useState<DocumentMeta | null>(null);
18
+ const [reindexResult, setReindexResult] = useState<ReindexResponse | null>(null);
19
+
20
+ const { data, isLoading, error } = useQuery({
21
+ queryKey: ["documents"],
22
+ queryFn: () => api.listDocuments(),
23
+ staleTime: 10_000,
24
+ });
25
+
26
+ const reindexMutation = useMutation({
27
+ mutationFn: () => api.reindex(false, false),
28
+ onSuccess: (resp) => setReindexResult(resp),
29
+ });
30
+
31
+ const deleteMutation = useMutation({
32
+ mutationFn: (doc_id: string) => api.deleteDocument(doc_id),
33
+ onSuccess: () => {
34
+ queryClient.invalidateQueries({ queryKey: ["documents"] });
35
+ setConfirmDelete(null);
36
+ },
37
+ });
38
+
39
+ const filtered = useMemo(() => {
40
+ if (!data) return [];
41
+ const f = filter.trim().toLowerCase();
42
+ if (!f) return data.documents;
43
+ return data.documents.filter(
44
+ (d) =>
45
+ d.name.toLowerCase().includes(f) ||
46
+ d.doc_id.toLowerCase().includes(f)
47
+ );
48
+ }, [data, filter]);
49
+
50
+ const PAGE_SIZE = 50;
51
+ const totalPages = Math.ceil(filtered.length / PAGE_SIZE);
52
+ const visible = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
53
+
54
+ return (
55
+ <>
56
+ {/* ───── Header strip ──────────────────────────────────────── */}
57
+ <section className="tile-parchment !py-12">
58
+ <div className="container-content flex items-end justify-between flex-wrap gap-4">
59
+ <div>
60
+ <h1 className="text-display-lg">Documents</h1>
61
+ <p className="text-body text-ink-48 mt-2">
62
+ {data
63
+ ? `${data.count.toLocaleString()} indexed · `
64
+ : ""}
65
+ persistent at <span className="text-mono">/data/docs/</span>
66
+ </p>
67
+ </div>
68
+ <div className="flex items-center gap-3">
69
+ <button
70
+ onClick={() => reindexMutation.mutate()}
71
+ disabled={reindexMutation.isPending}
72
+ className="btn-secondary"
73
+ >
74
+ {reindexMutation.isPending ? (
75
+ <>
76
+ <Spinner className="mr-2" /> Re-indexing
77
+ </>
78
+ ) : (
79
+ "Re-index now"
80
+ )}
81
+ </button>
82
+ <a href="#new" className="btn-primary">
83
+ Add document
84
+ </a>
85
+ </div>
86
+ </div>
87
+ </section>
88
+
89
+ {/* ───── Reindex banner ────────────────────────────────────── */}
90
+ {reindexResult && (
91
+ <section className="tile-light !py-6 border-b border-divider-soft">
92
+ <div className="container-content">
93
+ <div className="flex items-center justify-between flex-wrap gap-3">
94
+ <div className="text-caption">
95
+ <span className="text-caption-strong text-status-ok mr-2">
96
+ Re-index ✓
97
+ </span>
98
+ <span className="text-mono">
99
+ mode={reindexResult.mode ?? "none"} added=
100
+ {reindexResult.added} removed={reindexResult.removed} indexed=
101
+ {reindexResult.indexed_count} ·{" "}
102
+ {reindexResult.elapsed_seconds.toFixed(2)}s
103
+ </span>
104
+ </div>
105
+ <button
106
+ onClick={() => setReindexResult(null)}
107
+ className="text-caption text-ink-48 hover:text-ink"
108
+ >
109
+ dismiss
110
+ </button>
111
+ </div>
112
+ </div>
113
+ </section>
114
+ )}
115
+
116
+ {/* ───── Search + list ─────────────────────────────────────── */}
117
+ <section className="tile-light">
118
+ <div className="container-content">
119
+ <div className="flex items-center justify-between gap-4 mb-6">
120
+ <input
121
+ type="text"
122
+ placeholder="Search by name or doc_id…"
123
+ value={filter}
124
+ onChange={(e) => {
125
+ setFilter(e.target.value);
126
+ setPage(0);
127
+ }}
128
+ className="input-pill max-w-[480px]"
129
+ />
130
+ <span className="text-caption text-ink-48 text-mono shrink-0">
131
+ {filtered.length.toLocaleString()} match
132
+ {filtered.length === 1 ? "" : "es"}
133
+ </span>
134
+ </div>
135
+
136
+ {error && (
137
+ <div className="utility-card border-status-err/20 mb-4">
138
+ <p className="text-status-err text-body-strong">
139
+ Failed to load documents
140
+ </p>
141
+ <p className="text-caption text-ink-48 mt-1">
142
+ {error instanceof Error ? error.message : String(error)}
143
+ </p>
144
+ </div>
145
+ )}
146
+
147
+ {isLoading && (
148
+ <div className="flex items-center gap-3 text-ink-48">
149
+ <Spinner /> Loading documents…
150
+ </div>
151
+ )}
152
+
153
+ {visible.length > 0 && (
154
+ <ul className="divide-y divide-divider-soft border-t border-b border-divider-soft">
155
+ {visible.map((d) => (
156
+ <li
157
+ key={d.doc_id}
158
+ className="flex items-center justify-between gap-4 py-4"
159
+ >
160
+ <div className="min-w-0 flex-1">
161
+ <div className="text-body-strong text-ink truncate">
162
+ {d.name}
163
+ </div>
164
+ <div className="flex items-center gap-3 mt-1 text-caption text-ink-48">
165
+ <span className="text-mono">{d.doc_id}</span>
166
+ <span>·</span>
167
+ <span>
168
+ {d.length_chars.toLocaleString()} chars
169
+ </span>
170
+ <span>·</span>
171
+ <span>{new Date(d.created_at * 1000).toLocaleDateString()}</span>
172
+ </div>
173
+ </div>
174
+ <button
175
+ onClick={() => setConfirmDelete(d)}
176
+ className="text-caption text-status-err hover:underline"
177
+ >
178
+ Delete
179
+ </button>
180
+ </li>
181
+ ))}
182
+ </ul>
183
+ )}
184
+
185
+ {totalPages > 1 && (
186
+ <div className="flex items-center justify-between mt-6">
187
+ <button
188
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
189
+ disabled={page === 0}
190
+ className="btn-pearl disabled:opacity-40"
191
+ >
192
+ ← Prev
193
+ </button>
194
+ <span className="text-caption text-ink-48 text-mono">
195
+ Page {page + 1} of {totalPages}
196
+ </span>
197
+ <button
198
+ onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
199
+ disabled={page >= totalPages - 1}
200
+ className="btn-pearl disabled:opacity-40"
201
+ >
202
+ Next →
203
+ </button>
204
+ </div>
205
+ )}
206
+ </div>
207
+ </section>
208
+
209
+ {/* ───── Add document form ─────────────────────────────────── */}
210
+ <AddDocumentForm
211
+ onCreated={() => {
212
+ queryClient.invalidateQueries({ queryKey: ["documents"] });
213
+ }}
214
+ />
215
+
216
+ {/* ───── Confirm delete modal ──────────────────────────────── */}
217
+ {confirmDelete && (
218
+ <ConfirmDelete
219
+ doc={confirmDelete}
220
+ loading={deleteMutation.isPending}
221
+ onCancel={() => setConfirmDelete(null)}
222
+ onConfirm={() => deleteMutation.mutate(confirmDelete.doc_id)}
223
+ />
224
+ )}
225
+ </>
226
+ );
227
+ }
228
+
229
+ function AddDocumentForm({ onCreated }: { onCreated: () => void }) {
230
+ const [name, setName] = useState("");
231
+ const [text, setText] = useState("");
232
+ const [doReindex, setDoReindex] = useState(true);
233
+ const [feedback, setFeedback] = useState<string | null>(null);
234
+
235
+ const create = useMutation({
236
+ mutationFn: async (payload: { text: string; name?: string }) => {
237
+ const created = await api.createDocument(payload);
238
+ if (doReindex) {
239
+ await api.reindex(false, false);
240
+ }
241
+ return created;
242
+ },
243
+ onSuccess: (created) => {
244
+ setFeedback(`✓ Added: ${created.name} (${created.doc_id})`);
245
+ setText("");
246
+ setName("");
247
+ onCreated();
248
+ },
249
+ onError: (err) => {
250
+ const msg = err instanceof Error ? err.message : String(err);
251
+ setFeedback(`✗ Error: ${msg}`);
252
+ },
253
+ });
254
+
255
+ const submit = (e: React.FormEvent) => {
256
+ e.preventDefault();
257
+ if (!text.trim()) return;
258
+ create.mutate({ text: text.trim(), name: name.trim() || undefined });
259
+ };
260
+
261
+ return (
262
+ <section id="new" className="tile-parchment">
263
+ <div className="container-content max-w-[800px]">
264
+ <h2 className="text-display-md">Add document</h2>
265
+ <p className="text-body text-ink-48 mt-2">
266
+ New documents persist immediately. Re-index syncs them into RAG retrieval —
267
+ enabled by default below.
268
+ </p>
269
+
270
+ <form onSubmit={submit} className="mt-8 space-y-4">
271
+ <div>
272
+ <label className="text-caption-strong block mb-2">
273
+ Name <span className="text-ink-48 font-normal">(optional)</span>
274
+ </label>
275
+ <input
276
+ type="text"
277
+ value={name}
278
+ onChange={(e) => setName(e.target.value)}
279
+ placeholder="e.g. Customer Onboarding Flow v2"
280
+ className="input-pill"
281
+ disabled={create.isPending}
282
+ />
283
+ </div>
284
+
285
+ <div>
286
+ <label className="text-caption-strong block mb-2">
287
+ Content
288
+ </label>
289
+ <textarea
290
+ value={text}
291
+ onChange={(e) => setText(e.target.value)}
292
+ required
293
+ minLength={1}
294
+ rows={10}
295
+ placeholder="Paste markdown / plain text…"
296
+ className="w-full bg-canvas rounded-lg p-4 text-body shadow-hairline focus:shadow-[0_0_0_2px_#0071e3] outline-none"
297
+ disabled={create.isPending}
298
+ />
299
+ <p className="text-fine-print text-ink-48 mt-1 text-mono">
300
+ {text.length.toLocaleString()} chars
301
+ </p>
302
+ </div>
303
+
304
+ <label className="flex items-center gap-3 text-caption">
305
+ <input
306
+ type="checkbox"
307
+ checked={doReindex}
308
+ onChange={(e) => setDoReindex(e.target.checked)}
309
+ className="w-4 h-4 accent-primary"
310
+ />
311
+ <span>
312
+ Auto re-index after add
313
+ <span className="text-ink-48 ml-2 text-fine-print">
314
+ (~350ms — enables retrieval via /ask_smart)
315
+ </span>
316
+ </span>
317
+ </label>
318
+
319
+ <div className="flex items-center gap-4">
320
+ <button
321
+ type="submit"
322
+ disabled={create.isPending || !text.trim()}
323
+ className="btn-primary"
324
+ >
325
+ {create.isPending ? (
326
+ <>
327
+ <Spinner className="mr-2" /> Saving
328
+ </>
329
+ ) : (
330
+ "Save document"
331
+ )}
332
+ </button>
333
+ {feedback && (
334
+ <span
335
+ className={
336
+ feedback.startsWith("✓")
337
+ ? "text-caption text-status-ok"
338
+ : "text-caption text-status-err"
339
+ }
340
+ >
341
+ {feedback}
342
+ </span>
343
+ )}
344
+ </div>
345
+ </form>
346
+ </div>
347
+ </section>
348
+ );
349
+ }
350
+
351
+ function ConfirmDelete({
352
+ doc,
353
+ loading,
354
+ onCancel,
355
+ onConfirm,
356
+ }: {
357
+ doc: DocumentMeta;
358
+ loading: boolean;
359
+ onCancel: () => void;
360
+ onConfirm: () => void;
361
+ }) {
362
+ return (
363
+ <div
364
+ className="fixed inset-0 z-50 bg-surface-black/60 flex items-center justify-center p-6 animate-fade-in"
365
+ onClick={onCancel}
366
+ >
367
+ <div
368
+ onClick={(e) => e.stopPropagation()}
369
+ className="bg-canvas rounded-lg shadow-product max-w-[480px] w-full p-8"
370
+ >
371
+ <h3 className="text-display-md">Delete document?</h3>
372
+ <p className="text-body text-ink-48 mt-2">
373
+ <span className="text-body-strong text-ink">{doc.name}</span>
374
+ <br />
375
+ <span className="text-mono text-caption">{doc.doc_id}</span>
376
+ </p>
377
+ <p className="text-caption text-ink-48 mt-4">
378
+ This is permanent. Re-indexing will remove it from /ask_smart retrieval.
379
+ </p>
380
+ <div className="mt-8 flex items-center justify-end gap-3">
381
+ <button onClick={onCancel} className="btn-pearl">
382
+ Cancel
383
+ </button>
384
+ <button
385
+ onClick={onConfirm}
386
+ disabled={loading}
387
+ className="btn-primary !bg-status-err"
388
+ >
389
+ {loading ? (
390
+ <>
391
+ <Spinner className="mr-2" /> Deleting
392
+ </>
393
+ ) : (
394
+ "Delete"
395
+ )}
396
+ </button>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ );
401
+ }
app/globals.css ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --font-inter: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
8
+ --font-jetbrains: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
9
+ }
10
+
11
+ html {
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ text-rendering: optimizeLegibility;
15
+ }
16
+
17
+ body {
18
+ background: #f5f5f7;
19
+ color: #1d1d1f;
20
+ font-family: var(--font-inter);
21
+ font-size: 17px;
22
+ line-height: 1.47;
23
+ letter-spacing: -0.374px;
24
+ font-weight: 400;
25
+ /* Inter substitution: font-feature-settings approximate SF Pro's rounded "a" */
26
+ font-feature-settings: "ss03", "cv11", "calt";
27
+ }
28
+
29
+ /* Display sizes use slightly tighter tracking via Tailwind tokens.
30
+ This is the supplementary nudge for non-Apple platforms (Inter > SF Pro tracking). */
31
+ h1, h2, h3, h4 {
32
+ letter-spacing: -0.01em;
33
+ }
34
+
35
+ ::selection {
36
+ background: rgba(0, 102, 204, 0.18);
37
+ }
38
+
39
+ /* Focus ring — Apple Focus Blue #0071e3, 2px solid */
40
+ :focus-visible {
41
+ outline: 2px solid #0071e3;
42
+ outline-offset: 2px;
43
+ }
44
+ }
45
+
46
+ @layer components {
47
+ /* ─── Button grammars (matching the design tokens) ─────────────── */
48
+ .btn-primary {
49
+ @apply inline-flex items-center justify-center
50
+ bg-primary text-white
51
+ rounded-pill
52
+ px-[22px] py-[11px]
53
+ text-body
54
+ transition-transform duration-200 ease-apple
55
+ active:scale-[0.96]
56
+ disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100;
57
+ }
58
+
59
+ .btn-secondary {
60
+ @apply inline-flex items-center justify-center
61
+ border border-primary text-primary
62
+ rounded-pill
63
+ px-[22px] py-[11px]
64
+ text-body
65
+ transition-transform duration-200 ease-apple
66
+ active:scale-[0.96];
67
+ }
68
+
69
+ .btn-utility-dark {
70
+ @apply inline-flex items-center justify-center
71
+ bg-ink text-white
72
+ rounded-sm
73
+ px-[15px] py-[8px]
74
+ text-button-utility
75
+ transition-transform duration-200 ease-apple
76
+ active:scale-[0.96];
77
+ }
78
+
79
+ .btn-pearl {
80
+ @apply inline-flex items-center justify-center
81
+ bg-surface-pearl text-ink-80
82
+ rounded-md
83
+ px-[14px] py-[8px]
84
+ text-caption
85
+ shadow-hairline
86
+ transition-transform duration-200 ease-apple
87
+ active:scale-[0.96];
88
+ }
89
+
90
+ /* ─── Input grammars ───────────────────────────────────────────── */
91
+ .input-pill {
92
+ @apply w-full
93
+ bg-canvas text-ink
94
+ rounded-pill
95
+ px-[20px] py-[12px]
96
+ text-body
97
+ shadow-hairline
98
+ outline-none
99
+ transition-shadow duration-200
100
+ focus:shadow-[0_0_0_2px_#0071e3];
101
+ }
102
+
103
+ /* ─── Card grammars ────────────────────────────────────────────── */
104
+ .utility-card {
105
+ @apply bg-canvas border border-hairline rounded-lg p-6;
106
+ }
107
+
108
+ /* ─── Typography helpers ───────────────────────────────────────── */
109
+ .text-mono {
110
+ font-family: var(--font-jetbrains);
111
+ font-feature-settings: "calt", "ss03";
112
+ }
113
+
114
+ /* ─── Tile rhythms ─────────────────────────────────────────────── */
115
+ .tile-light {
116
+ @apply bg-canvas text-ink py-section;
117
+ }
118
+ .tile-parchment {
119
+ @apply bg-canvas-parchment text-ink py-section;
120
+ }
121
+ .tile-dark {
122
+ @apply bg-surface-tile-1 text-white py-section;
123
+ }
124
+ .tile-dark-2 {
125
+ @apply bg-surface-tile-2 text-white py-section;
126
+ }
127
+
128
+ /* ─── Frosted nav ──────────────────────────────────────────────── */
129
+ .frosted {
130
+ backdrop-filter: saturate(180%) blur(20px);
131
+ -webkit-backdrop-filter: saturate(180%) blur(20px);
132
+ }
133
+
134
+ /* ─── Section container ────────────────────────────────────────── */
135
+ .container-content {
136
+ @apply max-w-[1440px] mx-auto px-6 md:px-12;
137
+ }
138
+
139
+ .container-narrow {
140
+ @apply max-w-[980px] mx-auto px-6 md:px-12;
141
+ }
142
+ }
143
+
144
+ /* Scrollbar polish (cross-browser, subtle) */
145
+ @layer utilities {
146
+ .scrollbar-fine {
147
+ scrollbar-width: thin;
148
+ scrollbar-color: rgba(0, 0, 0, 0.18) transparent;
149
+ }
150
+ .scrollbar-fine::-webkit-scrollbar {
151
+ width: 8px;
152
+ height: 8px;
153
+ }
154
+ .scrollbar-fine::-webkit-scrollbar-thumb {
155
+ background-color: rgba(0, 0, 0, 0.18);
156
+ border-radius: 9999px;
157
+ }
158
+ .scrollbar-fine::-webkit-scrollbar-track {
159
+ background: transparent;
160
+ }
161
+ }
app/layout.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Inter, JetBrains_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+ import { Providers } from "./providers";
5
+ import { GlobalNav } from "@/components/nav/GlobalNav";
6
+ import { SubNav } from "@/components/nav/SubNav";
7
+ import { Footer } from "@/components/nav/Footer";
8
+
9
+ const inter = Inter({
10
+ subsets: ["latin"],
11
+ variable: "--font-inter",
12
+ display: "swap",
13
+ });
14
+
15
+ const jetbrains = JetBrains_Mono({
16
+ subsets: ["latin"],
17
+ variable: "--font-jetbrains",
18
+ display: "swap",
19
+ });
20
+
21
+ export const metadata: Metadata = {
22
+ title: "doc-to-lora — Etiya BSS knowledge interface",
23
+ description:
24
+ "Stateless retrieval-augmented inference over 1,166 Etiya BSS documents. Built on doc-to-lora hypernetwork (Sakana AI) and Gemma-2-2b-it.",
25
+ };
26
+
27
+ export default function RootLayout({
28
+ children,
29
+ }: {
30
+ children: React.ReactNode;
31
+ }) {
32
+ return (
33
+ <html lang="en" className={`${inter.variable} ${jetbrains.variable}`}>
34
+ <body className="min-h-screen flex flex-col">
35
+ <Providers>
36
+ <GlobalNav />
37
+ <SubNav />
38
+ <main className="flex-1">{children}</main>
39
+ <Footer />
40
+ </Providers>
41
+ </body>
42
+ </html>
43
+ );
44
+ }
app/page.tsx ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { AskInput } from "@/components/feature/AskInput";
4
+ import { AnswerCard } from "@/components/feature/AnswerCard";
5
+ import { Spinner } from "@/components/ui/Spinner";
6
+ import { api, ApiError } from "@/lib/api";
7
+ import { AskSmartResponse, HealthResponse } from "@/lib/types";
8
+ import { useMutation, useQuery } from "@tanstack/react-query";
9
+ import Link from "next/link";
10
+ import { useState } from "react";
11
+
12
+ const SAMPLE_QUESTIONS = [
13
+ "What are the four customer order item action types?",
14
+ "What is a Bill of Materials in Etiya BSS?",
15
+ "How does Etiya manage organizational contracts?",
16
+ "Can a category be localized in multiple languages?",
17
+ ];
18
+
19
+ export default function HomePage() {
20
+ const [question, setQuestion] = useState("");
21
+
22
+ const askMutation = useMutation({
23
+ mutationFn: (q: string) => api.askSmart({ question: q }),
24
+ });
25
+
26
+ const handleAsk = (q: string) => {
27
+ setQuestion(q);
28
+ askMutation.mutate(q);
29
+ };
30
+
31
+ return (
32
+ <>
33
+ {/* ───── Hero tile (light) ──────────────────────────────────── */}
34
+ <section className="tile-light">
35
+ <div className="container-narrow text-center">
36
+ <h1 className="text-hero-display text-ink animate-fade-in">
37
+ Ask anything.
38
+ </h1>
39
+ <p className="text-lead text-ink-48 mt-4 animate-stagger-1">
40
+ Stateless retrieval-augmented inference over{" "}
41
+ <span className="text-mono text-ink">1,166</span> Etiya BSS
42
+ documents.
43
+ </p>
44
+
45
+ <div className="mt-12 max-w-[720px] mx-auto animate-stagger-2">
46
+ <AskInput
47
+ onAsk={handleAsk}
48
+ loading={askMutation.isPending}
49
+ placeholder="What are the four customer order item action types?"
50
+ />
51
+
52
+ <div className="flex flex-wrap justify-center gap-2 mt-6 animate-stagger-3">
53
+ {SAMPLE_QUESTIONS.map((q) => (
54
+ <button
55
+ key={q}
56
+ onClick={() => handleAsk(q)}
57
+ disabled={askMutation.isPending}
58
+ className="btn-pearl"
59
+ >
60
+ {q}
61
+ </button>
62
+ ))}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </section>
67
+
68
+ {/* ───── Answer surface (parchment) ─────────────────────────── */}
69
+ {(askMutation.isPending ||
70
+ askMutation.data ||
71
+ askMutation.error) && (
72
+ <section className="tile-parchment py-section">
73
+ <div className="container-narrow">
74
+ {askMutation.isPending && <ThinkingPlaceholder />}
75
+ {askMutation.error && (
76
+ <ErrorCard error={askMutation.error} question={question} />
77
+ )}
78
+ {askMutation.data && (
79
+ <AnswerCard response={askMutation.data} question={question} />
80
+ )}
81
+ </div>
82
+ </section>
83
+ )}
84
+
85
+ {/* ───── Capability tiles (dark — section rhythm) ───────────── */}
86
+ <section className="tile-dark">
87
+ <div className="container-content grid md:grid-cols-3 gap-12">
88
+ <Capability
89
+ kicker="Triple-gate retrieval"
90
+ title="Hallucination defense at 87.5%"
91
+ body="K-means topic anchors + BM25 + dense (text-embedding-3-large) fused via Reciprocal Rank Fusion, then BGE rerank."
92
+ />
93
+ <Capability
94
+ kicker="Stateless inference"
95
+ title="Sub-2s answers on A100"
96
+ body="Each query re-tokenizes the source. No LoRA cache, no state corruption. Concurrent requests serialize on a single GPU lock."
97
+ />
98
+ <Capability
99
+ kicker="Zero hardcoded text"
100
+ title="Topic anchors derived from corpus"
101
+ body="The 38 K-means cluster centroids replace any handcrafted refusal phrase list. Add documents and re-index — anchors auto-update."
102
+ />
103
+ </div>
104
+ </section>
105
+
106
+ {/* ───── Capability stats tile (parchment) ──────────────────── */}
107
+ <SystemSummary />
108
+ </>
109
+ );
110
+ }
111
+
112
+ function Capability({
113
+ kicker,
114
+ title,
115
+ body,
116
+ }: {
117
+ kicker: string;
118
+ title: string;
119
+ body: string;
120
+ }) {
121
+ return (
122
+ <div>
123
+ <p className="text-caption-strong text-primary-on-dark uppercase tracking-[0.08em]">
124
+ {kicker}
125
+ </p>
126
+ <h3 className="text-display-md mt-3 text-white">{title}</h3>
127
+ <p className="text-body text-body-muted mt-4">{body}</p>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function ThinkingPlaceholder() {
133
+ return (
134
+ <div className="bg-canvas rounded-lg shadow-product p-12 flex items-center gap-4 animate-fade-in">
135
+ <Spinner className="text-primary" />
136
+ <span className="text-body text-ink-48">
137
+ Retrieving · reranking · grounding · generating…
138
+ </span>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function ErrorCard({
144
+ error,
145
+ question,
146
+ }: {
147
+ error: unknown;
148
+ question: string;
149
+ }) {
150
+ const status = error instanceof ApiError ? error.status : 0;
151
+ const msg = error instanceof Error ? error.message : String(error);
152
+ return (
153
+ <div className="utility-card">
154
+ <p className="text-caption-strong text-status-err uppercase tracking-[0.08em]">
155
+ Error · HTTP {status || "—"}
156
+ </p>
157
+ <p className="text-body-strong mt-2">{msg}</p>
158
+ <p className="text-caption text-ink-48 mt-3">
159
+ Question: <span className="text-mono">{question}</span>
160
+ </p>
161
+ {status === 502 && (
162
+ <p className="text-caption mt-4">
163
+ Backend not reachable. Check the HF Space stage at{" "}
164
+ <Link
165
+ className="text-primary hover:underline"
166
+ href="https://huggingface.co/spaces/Etiya/d2l-api"
167
+ target="_blank"
168
+ >
169
+ huggingface.co/spaces/Etiya/d2l-api
170
+ </Link>
171
+ .
172
+ </p>
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ function SystemSummary() {
179
+ const { data } = useQuery({
180
+ queryKey: ["health"],
181
+ queryFn: () => api.health(),
182
+ staleTime: 30_000,
183
+ });
184
+ return (
185
+ <section className="tile-parchment">
186
+ <div className="container-content text-center">
187
+ <p className="text-caption-strong text-ink-48 uppercase tracking-[0.08em]">
188
+ Live system
189
+ </p>
190
+ <h2 className="text-display-lg mt-3">
191
+ {data ? formatHealthHeadline(data) : "doc-to-lora · A100 · GPU online"}
192
+ </h2>
193
+ <div className="mt-12 grid grid-cols-2 md:grid-cols-4 gap-8 max-w-[980px] mx-auto">
194
+ <Stat
195
+ label="Documents indexed"
196
+ value={data?.doc_count?.toLocaleString() ?? "—"}
197
+ />
198
+ <Stat
199
+ label="GPU memory"
200
+ value={data ? data.gpu_memory_gb.toFixed(2) : "—"}
201
+ unit="GB"
202
+ />
203
+ <Stat
204
+ label="Model loaded"
205
+ value={
206
+ data === undefined ? "—" : data.model_loaded ? "Yes" : "No"
207
+ }
208
+ />
209
+ <Stat label="Eval pass rate" value="72.0" unit="%" />
210
+ </div>
211
+ <Link href="/system" className="btn-secondary mt-12 inline-flex">
212
+ Open system dashboard
213
+ </Link>
214
+ </div>
215
+ </section>
216
+ );
217
+ }
218
+
219
+ function Stat({
220
+ label,
221
+ value,
222
+ unit,
223
+ }: {
224
+ label: string;
225
+ value: string;
226
+ unit?: string;
227
+ }) {
228
+ return (
229
+ <div className="text-left">
230
+ <p className="text-fine-print uppercase tracking-[0.08em] text-ink-48">
231
+ {label}
232
+ </p>
233
+ <p className="text-display-md mt-2 text-mono">
234
+ {value}
235
+ {unit && <span className="text-ink-48 ml-1 text-lead">{unit}</span>}
236
+ </p>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ function formatHealthHeadline(data: HealthResponse): string {
242
+ if (!data.model_loaded) return "Model warming up…";
243
+ return `${data.doc_count.toLocaleString()} documents · GPU ready`;
244
+ }
app/providers.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import { useState } from "react";
5
+
6
+ export function Providers({ children }: { children: React.ReactNode }) {
7
+ const [client] = useState(
8
+ () =>
9
+ new QueryClient({
10
+ defaultOptions: {
11
+ queries: {
12
+ staleTime: 30_000,
13
+ refetchOnWindowFocus: false,
14
+ retry: 1,
15
+ },
16
+ },
17
+ })
18
+ );
19
+
20
+ return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
21
+ }
app/system/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
app/system/page.tsx ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Spinner } from "@/components/ui/Spinner";
4
+ import { api } from "@/lib/api";
5
+ import { HealthResponse } from "@/lib/types";
6
+ import { useMutation, useQuery } from "@tanstack/react-query";
7
+ import { useEffect, useState } from "react";
8
+
9
+ export default function SystemPage() {
10
+ const { data, error, refetch, isFetching } = useQuery({
11
+ queryKey: ["health"],
12
+ queryFn: () => api.health(),
13
+ refetchInterval: 30_000,
14
+ staleTime: 0,
15
+ });
16
+
17
+ const reindexMutation = useMutation({
18
+ mutationFn: ({
19
+ force_full,
20
+ rebuild_anchors,
21
+ }: {
22
+ force_full: boolean;
23
+ rebuild_anchors: boolean;
24
+ }) => api.reindex(force_full, rebuild_anchors),
25
+ });
26
+
27
+ return (
28
+ <>
29
+ {/* ───── Header ────────────────────────────────────────────── */}
30
+ <section className="tile-parchment !py-12">
31
+ <div className="container-content flex items-end justify-between flex-wrap gap-4">
32
+ <div>
33
+ <h1 className="text-display-lg">System</h1>
34
+ <p className="text-body text-ink-48 mt-2">
35
+ Live readout — auto-refresh every 30s.
36
+ </p>
37
+ </div>
38
+ <button
39
+ onClick={() => refetch()}
40
+ disabled={isFetching}
41
+ className="btn-secondary"
42
+ >
43
+ {isFetching ? (
44
+ <>
45
+ <Spinner className="mr-2" /> Refreshing
46
+ </>
47
+ ) : (
48
+ "Refresh now"
49
+ )}
50
+ </button>
51
+ </div>
52
+ </section>
53
+
54
+ {/* ───── Health metrics ────────────────────────────────────── */}
55
+ <section className="tile-light" id="metrics">
56
+ <div className="container-content">
57
+ {error && (
58
+ <div className="utility-card border-status-err/20 mb-8">
59
+ <p className="text-status-err text-body-strong">
60
+ Failed to read /health
61
+ </p>
62
+ <p className="text-caption text-ink-48 mt-1">
63
+ {error instanceof Error ? error.message : String(error)}
64
+ </p>
65
+ </div>
66
+ )}
67
+
68
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
69
+ <BigMetric
70
+ label="Stage"
71
+ value={
72
+ data === undefined
73
+ ? "—"
74
+ : data.model_loaded
75
+ ? "Running"
76
+ : "Loading"
77
+ }
78
+ tone={
79
+ data === undefined
80
+ ? "neutral"
81
+ : data.model_loaded
82
+ ? "ok"
83
+ : "warn"
84
+ }
85
+ />
86
+ <BigMetric
87
+ label="Documents indexed"
88
+ value={data?.doc_count?.toLocaleString() ?? "—"}
89
+ />
90
+ <BigMetric
91
+ label="GPU memory"
92
+ value={data ? data.gpu_memory_gb.toFixed(2) : "—"}
93
+ unit="GB"
94
+ />
95
+ <BigMetric
96
+ label="Hardware"
97
+ value="A100"
98
+ unit="80GB"
99
+ />
100
+ </div>
101
+
102
+ <div className="mt-12 grid md:grid-cols-2 gap-6">
103
+ <Latencies />
104
+ <CorpusBreakdown data={data} />
105
+ </div>
106
+ </div>
107
+ </section>
108
+
109
+ {/* ───── Re-index controls ────────────────────────────────── */}
110
+ <section className="tile-parchment" id="index">
111
+ <div className="container-content max-w-[800px]">
112
+ <h2 className="text-display-md">Index management</h2>
113
+ <p className="text-body text-ink-48 mt-2">
114
+ The RAG index needs to be synchronized after document changes.
115
+ Incremental is the default — it only embeds new/removed docs.
116
+ </p>
117
+
118
+ <div className="mt-8 grid md:grid-cols-2 gap-6">
119
+ <ReindexCard
120
+ title="Incremental"
121
+ description="Sync new and removed docs. ~350ms."
122
+ cost="≈ $0.0001"
123
+ loading={
124
+ reindexMutation.isPending &&
125
+ !reindexMutation.variables?.force_full
126
+ }
127
+ onClick={() =>
128
+ reindexMutation.mutate({ force_full: false, rebuild_anchors: false })
129
+ }
130
+ />
131
+ <ReindexCard
132
+ title="Full rebuild"
133
+ description="Re-embed all 1,166 docs + rebuild K-means anchors. ~30s."
134
+ cost="≈ $0.16"
135
+ danger
136
+ loading={
137
+ reindexMutation.isPending &&
138
+ !!reindexMutation.variables?.force_full
139
+ }
140
+ onClick={() =>
141
+ reindexMutation.mutate({ force_full: true, rebuild_anchors: true })
142
+ }
143
+ />
144
+ </div>
145
+
146
+ {reindexMutation.data && (
147
+ <div className="utility-card mt-6">
148
+ <p className="text-caption-strong text-status-ok mb-2">
149
+ Re-index complete
150
+ </p>
151
+ <pre className="text-mono text-caption text-ink whitespace-pre-wrap">
152
+ {JSON.stringify(reindexMutation.data, null, 2)}
153
+ </pre>
154
+ </div>
155
+ )}
156
+ {reindexMutation.error && (
157
+ <div className="utility-card border-status-err/20 mt-6">
158
+ <p className="text-caption-strong text-status-err">
159
+ Re-index failed
160
+ </p>
161
+ <p className="text-caption mt-1">
162
+ {reindexMutation.error instanceof Error
163
+ ? reindexMutation.error.message
164
+ : String(reindexMutation.error)}
165
+ </p>
166
+ </div>
167
+ )}
168
+ </div>
169
+ </section>
170
+
171
+ {/* ───── Eval / quality (static, sourced from production data) ──── */}
172
+ <section className="tile-dark">
173
+ <div className="container-content">
174
+ <p className="text-caption-strong text-primary-on-dark uppercase tracking-[0.08em]">
175
+ Eval suite
176
+ </p>
177
+ <h2 className="text-display-lg text-white mt-3 mb-12">
178
+ 100-question benchmark
179
+ </h2>
180
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-8">
181
+ <DarkMetric label="Hallucination defense" value="87.5" unit="%" />
182
+ <DarkMetric label="Concept queries" value="84" unit="%" />
183
+ <DarkMetric label="Cross-domain" value="55" unit="%" />
184
+ <DarkMetric label="Fact recall" value="59" unit="%" />
185
+ </div>
186
+ <p className="text-caption text-body-muted mt-12 max-w-[600px]">
187
+ Pure numerical signals (anchor / dense / rerank) with zero hardcoded
188
+ reference text. The eval set lives at
189
+ <span className="text-mono"> eval/eval_set.jsonl</span> in the
190
+ backend repo and can be replayed any time.
191
+ </p>
192
+ </div>
193
+ </section>
194
+ </>
195
+ );
196
+ }
197
+
198
+ function BigMetric({
199
+ label,
200
+ value,
201
+ unit,
202
+ tone = "neutral",
203
+ }: {
204
+ label: string;
205
+ value: string | number;
206
+ unit?: string;
207
+ tone?: "ok" | "warn" | "err" | "neutral";
208
+ }) {
209
+ const dotClass = {
210
+ ok: "bg-status-ok",
211
+ warn: "bg-status-warn",
212
+ err: "bg-status-err",
213
+ neutral: "bg-ink-48",
214
+ }[tone];
215
+ return (
216
+ <div className="utility-card">
217
+ <p className="text-fine-print uppercase tracking-[0.08em] text-ink-48 flex items-center gap-2">
218
+ <span className={`inline-block w-1.5 h-1.5 rounded-full ${dotClass}`} />
219
+ {label}
220
+ </p>
221
+ <p className="text-display-md mt-2 text-mono text-ink">
222
+ {value}
223
+ {unit && <span className="text-ink-48 ml-1 text-lead">{unit}</span>}
224
+ </p>
225
+ </div>
226
+ );
227
+ }
228
+
229
+ function DarkMetric({
230
+ label,
231
+ value,
232
+ unit,
233
+ }: {
234
+ label: string;
235
+ value: string;
236
+ unit?: string;
237
+ }) {
238
+ return (
239
+ <div>
240
+ <p className="text-fine-print uppercase tracking-[0.08em] text-body-muted">
241
+ {label}
242
+ </p>
243
+ <p className="text-display-lg mt-2 text-mono text-white">
244
+ {value}
245
+ {unit && <span className="text-body-muted ml-1 text-lead">{unit}</span>}
246
+ </p>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ function Latencies() {
252
+ const [pingMs, setPingMs] = useState<number | null>(null);
253
+ useEffect(() => {
254
+ let alive = true;
255
+ const tick = async () => {
256
+ const t0 = performance.now();
257
+ try {
258
+ await api.ping();
259
+ const dt = performance.now() - t0;
260
+ if (alive) setPingMs(dt);
261
+ } catch {
262
+ if (alive) setPingMs(null);
263
+ }
264
+ };
265
+ tick();
266
+ const id = setInterval(tick, 30_000);
267
+ return () => {
268
+ alive = false;
269
+ clearInterval(id);
270
+ };
271
+ }, []);
272
+
273
+ return (
274
+ <div className="utility-card">
275
+ <h3 className="text-body-strong">Round-trip latencies</h3>
276
+ <div className="mt-4 space-y-3 text-caption">
277
+ <Row label="Browser → ping" value={pingMs ? `${pingMs.toFixed(0)} ms` : "—"} />
278
+ <Row label="ask_smart (typical reject)" value="≈ 400 ms" />
279
+ <Row label="ask_smart (with inference)" value="≈ 1.3-2.0 s" />
280
+ <Row label="reindex (incremental)" value="≈ 350 ms" />
281
+ <Row label="reindex (full)" value="≈ 30 s" />
282
+ </div>
283
+ </div>
284
+ );
285
+ }
286
+
287
+ function CorpusBreakdown({ data }: { data: HealthResponse | undefined }) {
288
+ return (
289
+ <div className="utility-card">
290
+ <h3 className="text-body-strong">Corpus &amp; index</h3>
291
+ <div className="mt-4 space-y-3 text-caption">
292
+ <Row
293
+ label="Documents on disk"
294
+ value={data ? data.doc_count.toLocaleString() : "—"}
295
+ />
296
+ <Row label="Embedding model" value="text-embedding-3-large" />
297
+ <Row label="Embedding dim" value="3072" />
298
+ <Row label="Reranker" value="bge-reranker-v2-m3" />
299
+ <Row label="Refusal classifier" value="all-MiniLM-L6-v2" />
300
+ <Row label="Topic anchors (K)" value="38" />
301
+ </div>
302
+ </div>
303
+ );
304
+ }
305
+
306
+ function ReindexCard({
307
+ title,
308
+ description,
309
+ cost,
310
+ loading,
311
+ danger,
312
+ onClick,
313
+ }: {
314
+ title: string;
315
+ description: string;
316
+ cost: string;
317
+ loading: boolean;
318
+ danger?: boolean;
319
+ onClick: () => void;
320
+ }) {
321
+ return (
322
+ <div className="utility-card flex flex-col gap-4">
323
+ <div>
324
+ <h3 className="text-body-strong">{title}</h3>
325
+ <p className="text-caption text-ink-48 mt-1">{description}</p>
326
+ <p className="text-fine-print text-ink-48 mt-2 text-mono">
327
+ OpenAI cost {cost}
328
+ </p>
329
+ </div>
330
+ <button
331
+ onClick={onClick}
332
+ disabled={loading}
333
+ className={danger ? "btn-secondary self-start" : "btn-primary self-start"}
334
+ >
335
+ {loading ? (
336
+ <>
337
+ <Spinner className="mr-2" /> Running
338
+ </>
339
+ ) : (
340
+ `Run ${title.toLowerCase()}`
341
+ )}
342
+ </button>
343
+ </div>
344
+ );
345
+ }
346
+
347
+ function Row({ label, value }: { label: string; value: string }) {
348
+ return (
349
+ <div className="flex items-center justify-between">
350
+ <span className="text-ink-48">{label}</span>
351
+ <span className="text-mono text-ink">{value}</span>
352
+ </div>
353
+ );
354
+ }
components/feature/AdvancedParams.tsx ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { AskSmartRequest } from "@/lib/types";
4
+ import clsx from "clsx";
5
+ import { useState } from "react";
6
+
7
+ const DEFAULTS: Required<Omit<AskSmartRequest, "question">> = {
8
+ top_k: 1,
9
+ max_new_tokens: 200,
10
+ similarity_threshold: 0.45,
11
+ rerank_threshold: 0.6,
12
+ anchor_threshold: 0.2,
13
+ use_grounding: true,
14
+ repetition_penalty: 1.15,
15
+ no_repeat_ngram_size: 4,
16
+ scaler: 1.0,
17
+ bias_scaler: 1.0,
18
+ };
19
+
20
+ type ParamMeta<K extends keyof typeof DEFAULTS> = {
21
+ key: K;
22
+ label: string;
23
+ desc: string;
24
+ min: number;
25
+ max: number;
26
+ step: number;
27
+ unit?: string;
28
+ };
29
+
30
+ const PARAMS: ParamMeta<keyof typeof DEFAULTS>[] = [
31
+ {
32
+ key: "top_k",
33
+ label: "Top-K retrieval",
34
+ desc: "How many documents to surface as context. 1 = single best doc; 3 = blend (experimental on gemma_demo checkpoint).",
35
+ min: 1,
36
+ max: 3,
37
+ step: 1,
38
+ },
39
+ {
40
+ key: "max_new_tokens",
41
+ label: "Max new tokens",
42
+ desc: "Generation cap. > 200 risks repetition loops with greedy decoding.",
43
+ min: 32,
44
+ max: 512,
45
+ step: 8,
46
+ },
47
+ {
48
+ key: "similarity_threshold",
49
+ label: "Dense similarity gate",
50
+ desc: "Reject if top-1 dense cosine is below this. Lower = more permissive recall, higher = stricter rejection.",
51
+ min: 0,
52
+ max: 1,
53
+ step: 0.01,
54
+ },
55
+ {
56
+ key: "rerank_threshold",
57
+ label: "BGE rerank gate",
58
+ desc: "Reject if BGE cross-encoder score is below this. Higher = trust only confidently relevant docs.",
59
+ min: -2,
60
+ max: 5,
61
+ step: 0.05,
62
+ },
63
+ {
64
+ key: "anchor_threshold",
65
+ label: "Anchor (topic) gate",
66
+ desc: "Reject if question is far from every K-means topic centroid. Catches out-of-corpus probes.",
67
+ min: 0,
68
+ max: 1,
69
+ step: 0.01,
70
+ },
71
+ {
72
+ key: "scaler",
73
+ label: "doc-to-lora scaler",
74
+ desc: "LoRA intensity. 0 = ignore document, 1 = normal, >1 = amplify, <0 = invert. Hypernet-specific.",
75
+ min: -2,
76
+ max: 2,
77
+ step: 0.05,
78
+ },
79
+ {
80
+ key: "bias_scaler",
81
+ label: "Bias scaler",
82
+ desc: "Bias-side LoRA intensity. Usually leave at 1.0.",
83
+ min: -2,
84
+ max: 2,
85
+ step: 0.05,
86
+ },
87
+ {
88
+ key: "repetition_penalty",
89
+ label: "Repetition penalty",
90
+ desc: "1.0 = off. 1.1–1.2 cuts loops without hurting fluency.",
91
+ min: 1,
92
+ max: 2,
93
+ step: 0.05,
94
+ },
95
+ {
96
+ key: "no_repeat_ngram_size",
97
+ label: "No-repeat n-gram",
98
+ desc: "Forbid the same n-gram twice. 0 = off. 4 = cuts most loops without harming list output.",
99
+ min: 0,
100
+ max: 8,
101
+ step: 1,
102
+ },
103
+ ];
104
+
105
+ export function AdvancedParams({
106
+ values,
107
+ onChange,
108
+ }: {
109
+ values: AskSmartRequest;
110
+ onChange: (next: AskSmartRequest) => void;
111
+ }) {
112
+ const [open, setOpen] = useState(false);
113
+
114
+ const reset = () => onChange({ question: values.question, ...DEFAULTS });
115
+
116
+ return (
117
+ <div className="utility-card">
118
+ <button
119
+ onClick={() => setOpen((o) => !o)}
120
+ className="w-full flex items-center justify-between text-body-strong"
121
+ aria-expanded={open}
122
+ >
123
+ <span>Advanced parameters</span>
124
+ <span
125
+ className={clsx(
126
+ "text-ink-48 transition-transform duration-300",
127
+ open && "rotate-180"
128
+ )}
129
+ >
130
+
131
+ </span>
132
+ </button>
133
+
134
+ {open && (
135
+ <div className="mt-6 animate-fade-in">
136
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
137
+ {PARAMS.map((p) => (
138
+ <ParamRow
139
+ key={p.key}
140
+ meta={p}
141
+ value={values[p.key] ?? DEFAULTS[p.key]}
142
+ onChange={(v) => onChange({ ...values, [p.key]: v })}
143
+ />
144
+ ))}
145
+ </div>
146
+
147
+ <div className="mt-8 pt-4 border-t border-divider-soft flex items-center justify-between">
148
+ <label className="flex items-center gap-3 text-caption text-ink">
149
+ <input
150
+ type="checkbox"
151
+ checked={values.use_grounding ?? true}
152
+ onChange={(e) =>
153
+ onChange({ ...values, use_grounding: e.target.checked })
154
+ }
155
+ className="w-4 h-4 accent-primary"
156
+ />
157
+ <span>Wrap question with grounding instruction</span>
158
+ <span className="text-ink-48 text-fine-print">
159
+ (forces model to refuse if context is insufficient)
160
+ </span>
161
+ </label>
162
+ <button onClick={reset} className="btn-pearl">
163
+ Reset to defaults
164
+ </button>
165
+ </div>
166
+ </div>
167
+ )}
168
+ </div>
169
+ );
170
+ }
171
+
172
+ function ParamRow<K extends keyof typeof DEFAULTS>({
173
+ meta,
174
+ value,
175
+ onChange,
176
+ }: {
177
+ meta: ParamMeta<K>;
178
+ value: number | boolean;
179
+ onChange: (v: number) => void;
180
+ }) {
181
+ const num = typeof value === "number" ? value : 0;
182
+ return (
183
+ <div>
184
+ <div className="flex items-center justify-between mb-1">
185
+ <label className="text-caption-strong text-ink">{meta.label}</label>
186
+ <span className="text-mono text-caption text-ink-48">
187
+ {num.toFixed(meta.step < 1 ? 2 : 0)}
188
+ {meta.unit ?? ""}
189
+ </span>
190
+ </div>
191
+ <input
192
+ type="range"
193
+ min={meta.min}
194
+ max={meta.max}
195
+ step={meta.step}
196
+ value={num}
197
+ onChange={(e) => onChange(parseFloat(e.target.value))}
198
+ className="w-full accent-primary"
199
+ aria-label={meta.label}
200
+ />
201
+ <p className="text-fine-print text-ink-48 mt-1">{meta.desc}</p>
202
+ </div>
203
+ );
204
+ }
205
+
206
+ export { DEFAULTS };
components/feature/AnswerCard.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { AskSmartResponse } from "@/lib/types";
4
+ import { StatusBadge } from "@/components/ui/StatusBadge";
5
+ import { MetricBlock } from "@/components/ui/MetricBlock";
6
+ import { useState } from "react";
7
+
8
+ /**
9
+ * The "product render" of the application — the answer surface.
10
+ * Carries the system's only sanctioned shadow (`shadow-product`).
11
+ * Centered, breathing, with metrics as the spec-sheet readout.
12
+ */
13
+ export function AnswerCard({
14
+ response,
15
+ question,
16
+ }: {
17
+ response: AskSmartResponse;
18
+ question: string;
19
+ }) {
20
+ const top = response.source_docs?.[0];
21
+ return (
22
+ <article className="bg-canvas rounded-lg shadow-product p-8 md:p-12 animate-slide-up">
23
+ <header className="flex items-start justify-between gap-4 mb-6">
24
+ <span className="text-caption text-ink-48 line-clamp-2 max-w-[80%]">
25
+ {question}
26
+ </span>
27
+ <StatusBadge status={response._grounding_status} />
28
+ </header>
29
+
30
+ <div className="text-display-md whitespace-pre-wrap text-ink leading-snug">
31
+ {response.answer}
32
+ </div>
33
+
34
+ {/* Metrics row — Apple's spec-sheet voice in JetBrains Mono */}
35
+ <div className="mt-10 pt-6 border-t border-divider-soft grid grid-cols-2 md:grid-cols-4 gap-6">
36
+ <MetricBlock
37
+ label="Top similarity"
38
+ value={response._top_similarity?.toFixed(3) ?? "—"}
39
+ />
40
+ <MetricBlock
41
+ label="Rerank score"
42
+ value={response._top_rerank_score?.toFixed(3) ?? "—"}
43
+ />
44
+ <MetricBlock
45
+ label="Anchor"
46
+ value={response._anchor_score?.toFixed(3) ?? "—"}
47
+ />
48
+ <MetricBlock
49
+ label="Total"
50
+ value={response.total_seconds.toFixed(2)}
51
+ unit="s"
52
+ />
53
+ </div>
54
+
55
+ {top && <SourceDocList docs={response.source_docs} />}
56
+ </article>
57
+ );
58
+ }
59
+
60
+ function SourceDocList({ docs }: { docs: AskSmartResponse["source_docs"] }) {
61
+ const [expanded, setExpanded] = useState(false);
62
+ if (!docs?.length) return null;
63
+ const visible = expanded ? docs : docs.slice(0, 1);
64
+ return (
65
+ <section className="mt-6">
66
+ <h4 className="text-caption-strong text-ink-48 uppercase tracking-[0.08em] mb-3">
67
+ Sources ({docs.length})
68
+ </h4>
69
+ <ul className="space-y-2">
70
+ {visible.map((d) => (
71
+ <li
72
+ key={d.doc_id}
73
+ className="utility-card flex items-center justify-between gap-4"
74
+ >
75
+ <div className="min-w-0 flex-1">
76
+ <div className="text-body-strong text-ink truncate">{d.name}</div>
77
+ <div className="text-caption text-ink-48 text-mono mt-0.5">
78
+ {d.doc_id}
79
+ </div>
80
+ </div>
81
+ <div className="text-mono text-caption text-ink shrink-0">
82
+ {d.similarity.toFixed(3)}
83
+ </div>
84
+ </li>
85
+ ))}
86
+ </ul>
87
+ {docs.length > 1 && (
88
+ <button
89
+ onClick={() => setExpanded((e) => !e)}
90
+ className="mt-3 text-caption text-primary hover:underline"
91
+ >
92
+ {expanded ? "Show less" : `Show ${docs.length - 1} more`}
93
+ </button>
94
+ )}
95
+ </section>
96
+ );
97
+ }
components/feature/AskInput.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Spinner } from "@/components/ui/Spinner";
4
+ import { useState } from "react";
5
+
6
+ /**
7
+ * The "search-input" pill — search-input grammar from Apple's accessories page,
8
+ * here promoted to the hero CTA. 44px touch target, full pill, focus ring.
9
+ */
10
+ export function AskInput({
11
+ onAsk,
12
+ loading,
13
+ initialValue = "",
14
+ placeholder = "Ask a question about Etiya BSS…",
15
+ }: {
16
+ onAsk: (q: string) => void;
17
+ loading: boolean;
18
+ initialValue?: string;
19
+ placeholder?: string;
20
+ }) {
21
+ const [value, setValue] = useState(initialValue);
22
+
23
+ const submit = (e: React.FormEvent) => {
24
+ e.preventDefault();
25
+ const trimmed = value.trim();
26
+ if (trimmed && !loading) onAsk(trimmed);
27
+ };
28
+
29
+ return (
30
+ <form onSubmit={submit} className="w-full flex flex-col sm:flex-row gap-3">
31
+ <div className="relative flex-1">
32
+ <span className="absolute left-5 top-1/2 -translate-y-1/2 text-ink-48 pointer-events-none">
33
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
34
+ <circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
35
+ <path d="m11 11 3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
36
+ </svg>
37
+ </span>
38
+ <input
39
+ type="text"
40
+ value={value}
41
+ onChange={(e) => setValue(e.target.value)}
42
+ placeholder={placeholder}
43
+ disabled={loading}
44
+ className="input-pill pl-12 disabled:opacity-50"
45
+ aria-label="Question"
46
+ />
47
+ </div>
48
+ <button
49
+ type="submit"
50
+ disabled={loading || !value.trim()}
51
+ className="btn-primary min-w-[120px]"
52
+ >
53
+ {loading ? (
54
+ <>
55
+ <Spinner className="mr-2" /> Thinking
56
+ </>
57
+ ) : (
58
+ "Ask"
59
+ )}
60
+ </button>
61
+ </form>
62
+ );
63
+ }
components/feature/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
components/nav/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
components/nav/Footer.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Footer with parchment background, dense link columns at 17/2.41 leading,
3
+ * fine-print legal row.
4
+ */
5
+ export function Footer() {
6
+ return (
7
+ <footer className="bg-canvas-parchment text-ink-80">
8
+ <div className="container-content py-16">
9
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
10
+ <Column title="System">
11
+ <FooterLink href="/system">Health</FooterLink>
12
+ <FooterLink href="/system#index">Index status</FooterLink>
13
+ <FooterLink href="/system#metrics">GPU metrics</FooterLink>
14
+ </Column>
15
+ <Column title="Inference">
16
+ <FooterLink href="/ask">Ask</FooterLink>
17
+ <FooterLink href="/ask#advanced">Advanced params</FooterLink>
18
+ </Column>
19
+ <Column title="Corpus">
20
+ <FooterLink href="/documents">All documents</FooterLink>
21
+ <FooterLink href="/documents#new">Add document</FooterLink>
22
+ <FooterLink href="/documents#reindex">Re-index</FooterLink>
23
+ </Column>
24
+ <Column title="Reference">
25
+ <FooterLink
26
+ href="https://huggingface.co/spaces/Etiya/d2l-api"
27
+ external
28
+ >
29
+ HF Space
30
+ </FooterLink>
31
+ <FooterLink
32
+ href="https://github.com/SakanaAI/doc-to-lora"
33
+ external
34
+ >
35
+ doc-to-lora paper
36
+ </FooterLink>
37
+ <FooterLink
38
+ href="https://huggingface.co/google/gemma-2-2b-it"
39
+ external
40
+ >
41
+ Gemma-2-2b-it
42
+ </FooterLink>
43
+ </Column>
44
+ </div>
45
+
46
+ <div className="mt-16 pt-6 border-t border-hairline flex flex-col md:flex-row justify-between gap-3 text-fine-print text-ink-48">
47
+ <span>© Etiya · Internal · doc-to-lora UI</span>
48
+ <span className="text-mono">
49
+ Built on Sakana AI · Gemma-2-2b-it · text-embedding-3-large · BGE
50
+ </span>
51
+ </div>
52
+ </div>
53
+ </footer>
54
+ );
55
+ }
56
+
57
+ function Column({
58
+ title,
59
+ children,
60
+ }: {
61
+ title: string;
62
+ children: React.ReactNode;
63
+ }) {
64
+ return (
65
+ <div>
66
+ <h3 className="text-caption-strong mb-1">{title}</h3>
67
+ <ul className="space-y-0">{children}</ul>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ function FooterLink({
73
+ href,
74
+ external,
75
+ children,
76
+ }: {
77
+ href: string;
78
+ external?: boolean;
79
+ children: React.ReactNode;
80
+ }) {
81
+ return (
82
+ <li>
83
+ <a
84
+ href={href}
85
+ className="text-dense-link text-ink-80 hover:text-primary transition-colors"
86
+ {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
87
+ >
88
+ {children}
89
+ </a>
90
+ </li>
91
+ );
92
+ }
components/nav/GlobalNav.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useEffect, useState } from "react";
5
+ import { api } from "@/lib/api";
6
+
7
+ /**
8
+ * Apple's `global-nav`: ultra-thin true-black bar pinned to the top.
9
+ * Contains the brand wordmark, a tight link row, and a status pulse on the right.
10
+ */
11
+ export function GlobalNav() {
12
+ const [pulse, setPulse] = useState<"alive" | "down" | "loading">("loading");
13
+
14
+ useEffect(() => {
15
+ let alive = true;
16
+ const tick = async () => {
17
+ try {
18
+ const r = await api.ping();
19
+ if (alive) setPulse(r.status === "alive" ? "alive" : "down");
20
+ } catch {
21
+ if (alive) setPulse("down");
22
+ }
23
+ };
24
+ tick();
25
+ const id = setInterval(tick, 30_000);
26
+ return () => {
27
+ alive = false;
28
+ clearInterval(id);
29
+ };
30
+ }, []);
31
+
32
+ return (
33
+ <header className="sticky top-0 z-50 bg-surface-black text-white h-[44px] flex items-center">
34
+ <nav className="container-content flex items-center justify-between w-full">
35
+ <Link href="/" className="flex items-center gap-2 text-nav-link">
36
+ <span className="text-mono text-[11px] tracking-[-0.05em] opacity-90">
37
+ ⌘ d2l
38
+ </span>
39
+ <span className="hidden sm:inline opacity-60">·</span>
40
+ <span className="hidden sm:inline opacity-90">Etiya BSS</span>
41
+ </Link>
42
+
43
+ <div className="flex items-center gap-5">
44
+ <Link
45
+ href="/ask"
46
+ className="text-nav-link opacity-90 hover:opacity-100 transition-opacity"
47
+ >
48
+ Ask
49
+ </Link>
50
+ <Link
51
+ href="/documents"
52
+ className="text-nav-link opacity-90 hover:opacity-100 transition-opacity"
53
+ >
54
+ Documents
55
+ </Link>
56
+ <Link
57
+ href="/system"
58
+ className="text-nav-link opacity-90 hover:opacity-100 transition-opacity"
59
+ >
60
+ System
61
+ </Link>
62
+ <span
63
+ className="flex items-center gap-2 text-nav-link"
64
+ aria-live="polite"
65
+ >
66
+ <span
67
+ className={
68
+ "inline-block w-[6px] h-[6px] rounded-full transition-colors " +
69
+ (pulse === "alive"
70
+ ? "bg-status-ok"
71
+ : pulse === "down"
72
+ ? "bg-status-err"
73
+ : "bg-body-muted")
74
+ }
75
+ aria-label={pulse}
76
+ />
77
+ <span className="hidden md:inline opacity-60 text-mono text-[11px]">
78
+ {pulse === "alive" ? "ALIVE" : pulse === "down" ? "DOWN" : "•••"}
79
+ </span>
80
+ </span>
81
+ </div>
82
+ </nav>
83
+ </header>
84
+ );
85
+ }
components/nav/SubNav.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ /**
7
+ * Apple's `sub-nav-frosted`: parchment surface at 80% opacity with backdrop blur.
8
+ * Section title left, contextual CTA right. Sticky just below GlobalNav.
9
+ */
10
+ export function SubNav() {
11
+ const pathname = usePathname();
12
+
13
+ const ctx = pathnameToContext(pathname);
14
+
15
+ return (
16
+ <div
17
+ className="sticky top-[44px] z-40 frosted bg-canvas-parchment/80 border-b border-hairline h-[52px] flex items-center"
18
+ style={{ backdropFilter: "saturate(180%) blur(20px)" }}
19
+ >
20
+ <div className="container-content flex items-center justify-between w-full">
21
+ <span className="text-tagline">{ctx.title}</span>
22
+ <div className="flex items-center gap-4">
23
+ <span className="hidden md:inline text-button-utility text-ink-48">
24
+ {ctx.subtitle}
25
+ </span>
26
+ {ctx.cta && (
27
+ <Link href={ctx.cta.href} className="btn-primary text-caption">
28
+ {ctx.cta.label}
29
+ </Link>
30
+ )}
31
+ </div>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ function pathnameToContext(pathname: string) {
38
+ if (pathname === "/" || pathname === "")
39
+ return {
40
+ title: "Overview",
41
+ subtitle: "Stateless RAG over 1,166 BSS documents",
42
+ cta: { href: "/ask", label: "Ask now" },
43
+ };
44
+ if (pathname.startsWith("/ask"))
45
+ return {
46
+ title: "Ask",
47
+ subtitle: "doc-to-lora hypernet · Gemma-2-2b-it",
48
+ cta: { href: "/system", label: "System" },
49
+ };
50
+ if (pathname.startsWith("/documents"))
51
+ return {
52
+ title: "Documents",
53
+ subtitle: "Persistent BSS corpus · CRUD",
54
+ cta: null,
55
+ };
56
+ if (pathname.startsWith("/system"))
57
+ return {
58
+ title: "System",
59
+ subtitle: "Health · GPU · Index",
60
+ cta: null,
61
+ };
62
+ return { title: "doc-to-lora", subtitle: "", cta: null };
63
+ }
components/ui/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
components/ui/MetricBlock.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import clsx from "clsx";
2
+
3
+ /**
4
+ * "Spec sheet" inline numerical readout — JetBrains Mono variable for technical
5
+ * voice. Used in the answer card metrics row and system dashboard.
6
+ */
7
+ export function MetricBlock({
8
+ label,
9
+ value,
10
+ unit,
11
+ className,
12
+ }: {
13
+ label: string;
14
+ value: string | number;
15
+ unit?: string;
16
+ className?: string;
17
+ }) {
18
+ return (
19
+ <div className={clsx("flex flex-col gap-0.5", className)}>
20
+ <span className="text-fine-print uppercase tracking-[0.08em] text-ink-48">
21
+ {label}
22
+ </span>
23
+ <span className="text-mono text-body-strong text-ink">
24
+ {value}
25
+ {unit ? <span className="text-ink-48 ml-0.5">{unit}</span> : null}
26
+ </span>
27
+ </div>
28
+ );
29
+ }
components/ui/Spinner.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import clsx from "clsx";
2
+
3
+ export function Spinner({ className }: { className?: string }) {
4
+ return (
5
+ <svg
6
+ className={clsx("animate-spin", className)}
7
+ width="18"
8
+ height="18"
9
+ viewBox="0 0 18 18"
10
+ fill="none"
11
+ aria-hidden
12
+ >
13
+ <circle
14
+ cx="9"
15
+ cy="9"
16
+ r="7"
17
+ stroke="currentColor"
18
+ strokeWidth="2"
19
+ opacity="0.2"
20
+ />
21
+ <path
22
+ d="M9 2 a 7 7 0 0 1 7 7"
23
+ stroke="currentColor"
24
+ strokeWidth="2"
25
+ strokeLinecap="round"
26
+ />
27
+ </svg>
28
+ );
29
+ }
components/ui/StatusBadge.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { GroundingStatus } from "@/lib/types";
2
+ import clsx from "clsx";
3
+
4
+ const STATUS_META: Record<
5
+ GroundingStatus,
6
+ { label: string; tone: "ok" | "warn" | "err" | "neutral"; description: string }
7
+ > = {
8
+ answered: {
9
+ label: "Answered",
10
+ tone: "ok",
11
+ description:
12
+ "Source document found and answer is grounded. Highest confidence.",
13
+ },
14
+ answered_partial: {
15
+ label: "Partial",
16
+ tone: "warn",
17
+ description:
18
+ "Answer is partially grounded — model may have paraphrased beyond source.",
19
+ },
20
+ ungrounded: {
21
+ label: "Ungrounded",
22
+ tone: "warn",
23
+ description:
24
+ "Answer was generated but doesn't strongly match the source. Treat with caution.",
25
+ },
26
+ rejected_low_similarity: {
27
+ label: "Refused",
28
+ tone: "neutral",
29
+ description:
30
+ "Question rejected before inference — corpus has no sufficiently relevant document.",
31
+ },
32
+ model_refused: {
33
+ label: "Refused",
34
+ tone: "neutral",
35
+ description: "Model declined to answer based on the provided context.",
36
+ },
37
+ };
38
+
39
+ export function StatusBadge({
40
+ status,
41
+ className,
42
+ }: {
43
+ status: GroundingStatus;
44
+ className?: string;
45
+ }) {
46
+ const meta = STATUS_META[status] ?? {
47
+ label: status,
48
+ tone: "neutral",
49
+ description: "",
50
+ };
51
+ const toneClass = {
52
+ ok: "bg-status-ok/10 text-status-ok",
53
+ warn: "bg-status-warn/10 text-status-warn",
54
+ err: "bg-status-err/10 text-status-err",
55
+ neutral: "bg-ink-48/10 text-ink-48",
56
+ }[meta.tone];
57
+ return (
58
+ <span
59
+ title={meta.description}
60
+ className={clsx(
61
+ "inline-flex items-center gap-1.5 rounded-pill px-3 py-1 text-caption-strong",
62
+ toneClass,
63
+ className
64
+ )}
65
+ >
66
+ <Dot tone={meta.tone} />
67
+ {meta.label}
68
+ </span>
69
+ );
70
+ }
71
+
72
+ function Dot({ tone }: { tone: "ok" | "warn" | "err" | "neutral" }) {
73
+ const cls = {
74
+ ok: "bg-status-ok",
75
+ warn: "bg-status-warn",
76
+ err: "bg-status-err",
77
+ neutral: "bg-ink-48",
78
+ }[tone];
79
+ return <span className={clsx("inline-block w-1.5 h-1.5 rounded-full", cls)} />;
80
+ }
docker-compose.yml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Etiya doc-to-lora UI — local container deployment
2
+ #
3
+ # Usage:
4
+ # 1. cp .env.example .env.local (or set HF_TOKEN in your shell)
5
+ # 2. docker compose up --build (builds image, starts container)
6
+ # 3. open http://localhost:3000
7
+ #
8
+ # To rebuild after code changes:
9
+ # docker compose up --build --force-recreate
10
+ #
11
+ # To inspect inside the container:
12
+ # docker compose exec d2l-ui sh
13
+
14
+ services:
15
+ d2l-ui:
16
+ build:
17
+ context: .
18
+ dockerfile: Dockerfile
19
+ image: etiya/d2l-ui:latest
20
+ container_name: d2l-ui
21
+ restart: unless-stopped
22
+ ports:
23
+ # Local dev exposes 3000; container internally listens on PORT env (default 7860 in image).
24
+ # Override PORT inside container to 3000 to match the local convention.
25
+ - "3000:3000"
26
+ environment:
27
+ PORT: 3000
28
+ # HF_TOKEN read from .env.local (host) at container startup —
29
+ # never baked into the image. Rotate by editing .env.local + restarting.
30
+ HF_TOKEN: ${HF_TOKEN:?HF_TOKEN must be set in .env.local or shell}
31
+ D2L_API_URL: ${D2L_API_URL:-https://etiya-d2l-api.hf.space}
32
+ NODE_ENV: production
33
+ env_file:
34
+ - .env.local
35
+ healthcheck:
36
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"]
37
+ interval: 30s
38
+ timeout: 5s
39
+ retries: 3
40
+ start_period: 15s
41
+ # Resource limits — tune to host. Frontend is light (~150MB image, ~80MB RAM).
42
+ deploy:
43
+ resources:
44
+ limits:
45
+ cpus: "1.0"
46
+ memory: 512M
47
+ reservations:
48
+ cpus: "0.25"
49
+ memory: 128M
frontend/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
frontend/app/api/proxy/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
frontend/app/api/proxy/[...path]/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
frontend/lib/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
lib/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
lib/api.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Browser-side API client.
3
+ * All calls go through /api/proxy/* — token added server-side.
4
+ */
5
+
6
+ import type {
7
+ AskSmartRequest,
8
+ AskSmartResponse,
9
+ CreateDocumentRequest,
10
+ DocumentMeta,
11
+ DocumentsListResponse,
12
+ HealthResponse,
13
+ PingResponse,
14
+ ReindexResponse,
15
+ } from "./types";
16
+
17
+ const PROXY = "/api/proxy";
18
+
19
+ class ApiError extends Error {
20
+ status: number;
21
+ body: unknown;
22
+ constructor(status: number, message: string, body?: unknown) {
23
+ super(message);
24
+ this.status = status;
25
+ this.body = body;
26
+ }
27
+ }
28
+
29
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
30
+ // Empty path = upstream root (`/` health). Avoid `/api/proxy/` which Next 15
31
+ // 308-redirects to `/api/proxy`; hit the no-slash form directly.
32
+ const url = path ? `${PROXY}/${path}` : PROXY;
33
+ const res = await fetch(url, {
34
+ ...init,
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ ...(init?.headers || {}),
38
+ },
39
+ });
40
+ const text = await res.text();
41
+ let data: unknown;
42
+ try {
43
+ data = text ? JSON.parse(text) : null;
44
+ } catch {
45
+ data = text;
46
+ }
47
+ if (!res.ok) {
48
+ const msg =
49
+ typeof data === "object" && data && "detail" in data
50
+ ? String((data as { detail: unknown }).detail)
51
+ : `HTTP ${res.status}`;
52
+ throw new ApiError(res.status, msg, data);
53
+ }
54
+ return data as T;
55
+ }
56
+
57
+ export { ApiError };
58
+
59
+ // ─── Health & Ops ─────────────────────────────────────────────────
60
+ export const api = {
61
+ health: () => request<HealthResponse>(""),
62
+ ping: () => request<PingResponse>("ping"),
63
+
64
+ reindex: (force_full = false, rebuild_anchors = false) =>
65
+ request<ReindexResponse>("reindex", {
66
+ method: "POST",
67
+ body: JSON.stringify({ force_full, rebuild_anchors }),
68
+ }),
69
+
70
+ // ─── Documents ───────────────────────────────────────────────────
71
+ listDocuments: () => request<DocumentsListResponse>("documents"),
72
+ getDocument: (doc_id: string) => request<DocumentMeta>(`documents/${doc_id}`),
73
+ createDocument: (req: CreateDocumentRequest) =>
74
+ request<DocumentMeta>("documents", {
75
+ method: "POST",
76
+ body: JSON.stringify(req),
77
+ }),
78
+ deleteDocument: (doc_id: string) =>
79
+ request<{ status: string; doc_id: string }>(`documents/${doc_id}`, {
80
+ method: "DELETE",
81
+ }),
82
+
83
+ // ─── Inference ───────────────────────────────────────────────────
84
+ askSmart: (req: AskSmartRequest) =>
85
+ request<AskSmartResponse>("ask_smart", {
86
+ method: "POST",
87
+ body: JSON.stringify(req),
88
+ }),
89
+ };
lib/types.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TypeScript types matching the FastAPI backend.
3
+ * See d2l-space/app.py for source-of-truth.
4
+ */
5
+
6
+ // ─── Health & Ops ──────────────────────────────────────────────────
7
+ export type HealthResponse = {
8
+ status: "ok";
9
+ model_loaded: boolean;
10
+ doc_count: number;
11
+ gpu_memory_gb: number;
12
+ };
13
+
14
+ export type PingResponse = {
15
+ status: "alive";
16
+ ts: number;
17
+ };
18
+
19
+ export type ReindexResponse = {
20
+ status: "ok" | "no_change";
21
+ mode?: "incremental" | "full_rebuild";
22
+ added: number;
23
+ removed: number;
24
+ indexed_count: number;
25
+ store_count: number;
26
+ anchors_rebuilt: boolean;
27
+ anchors_k: number;
28
+ elapsed_seconds: number;
29
+ };
30
+
31
+ // ─── Documents ─────────────────────────────────────────────────────
32
+ export type DocumentMeta = {
33
+ doc_id: string;
34
+ name: string;
35
+ length_chars: number;
36
+ created_at: number;
37
+ };
38
+
39
+ export type DocumentsListResponse = {
40
+ documents: DocumentMeta[];
41
+ count: number;
42
+ };
43
+
44
+ export type CreateDocumentRequest = {
45
+ text: string;
46
+ name?: string;
47
+ };
48
+
49
+ // ─── Inference ─────────────────────────────────────────────────────
50
+ export type GroundingStatus =
51
+ | "answered"
52
+ | "answered_partial"
53
+ | "ungrounded"
54
+ | "rejected_low_similarity"
55
+ | "model_refused";
56
+
57
+ export type SourceDoc = {
58
+ doc_id: string;
59
+ name: string;
60
+ similarity: number;
61
+ dense_similarity?: number;
62
+ rerank_score?: number;
63
+ };
64
+
65
+ export type AskSmartRequest = {
66
+ question: string;
67
+ top_k?: number;
68
+ max_new_tokens?: number;
69
+ similarity_threshold?: number;
70
+ rerank_threshold?: number;
71
+ anchor_threshold?: number;
72
+ use_grounding?: boolean;
73
+ repetition_penalty?: number;
74
+ no_repeat_ngram_size?: number;
75
+ scaler?: number;
76
+ bias_scaler?: number;
77
+ };
78
+
79
+ export type AskSmartResponse = {
80
+ answer: string;
81
+ source_docs: SourceDoc[];
82
+ _grounding_status: GroundingStatus;
83
+ _grounding_score?: number | null;
84
+ _top_similarity: number;
85
+ _top_rerank_score?: number | null;
86
+ _anchor_score?: number;
87
+ _corpus_relevance?: number;
88
+ _threshold: number;
89
+ retrieve_seconds: number;
90
+ inference_seconds: number;
91
+ total_seconds: number;
92
+ };
next.config.mjs ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+
6
+ /** @type {import('next').NextConfig} */
7
+ const nextConfig = {
8
+ reactStrictMode: true,
9
+ // The HF token is server-side only; no NEXT_PUBLIC_ exposure
10
+ // API calls go through /api/proxy/* which adds Authorization header server-side
11
+
12
+ // Standalone output for Docker — produces a minimal self-contained
13
+ // server.js bundle. Final image stays ~150MB instead of ~1GB.
14
+ output: "standalone",
15
+
16
+ // Pin the trace root to this folder so Next 15 doesn't follow a parent
17
+ // lockfile (e.g. /Users/<me>/package-lock.json) and produce confusing
18
+ // warnings or copy unrelated files into the standalone output.
19
+ outputFileTracingRoot: __dirname,
20
+ };
21
+
22
+ export default nextConfig;
package-lock.json ADDED
@@ -0,0 +1,2175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "etiya-d2l-ui",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "etiya-d2l-ui",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "@tanstack/react-query": "^5.59.0",
12
+ "clsx": "^2.1.1",
13
+ "next": "^15.5.15",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1",
16
+ "zustand": "^4.5.5"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.16.10",
20
+ "@types/react": "^18.3.11",
21
+ "@types/react-dom": "^18.3.0",
22
+ "autoprefixer": "^10.4.20",
23
+ "postcss": "^8.5.13",
24
+ "tailwindcss": "^3.4.13",
25
+ "typescript": "^5.6.2"
26
+ }
27
+ },
28
+ "node_modules/@alloc/quick-lru": {
29
+ "version": "5.2.0",
30
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
31
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
32
+ "dev": true,
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=10"
36
+ },
37
+ "funding": {
38
+ "url": "https://github.com/sponsors/sindresorhus"
39
+ }
40
+ },
41
+ "node_modules/@emnapi/runtime": {
42
+ "version": "1.10.0",
43
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
44
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
45
+ "license": "MIT",
46
+ "optional": true,
47
+ "dependencies": {
48
+ "tslib": "^2.4.0"
49
+ }
50
+ },
51
+ "node_modules/@img/colour": {
52
+ "version": "1.1.0",
53
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
54
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
55
+ "license": "MIT",
56
+ "optional": true,
57
+ "engines": {
58
+ "node": ">=18"
59
+ }
60
+ },
61
+ "node_modules/@img/sharp-darwin-arm64": {
62
+ "version": "0.34.5",
63
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
64
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
65
+ "cpu": [
66
+ "arm64"
67
+ ],
68
+ "license": "Apache-2.0",
69
+ "optional": true,
70
+ "os": [
71
+ "darwin"
72
+ ],
73
+ "engines": {
74
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
75
+ },
76
+ "funding": {
77
+ "url": "https://opencollective.com/libvips"
78
+ },
79
+ "optionalDependencies": {
80
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
81
+ }
82
+ },
83
+ "node_modules/@img/sharp-darwin-x64": {
84
+ "version": "0.34.5",
85
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
86
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
87
+ "cpu": [
88
+ "x64"
89
+ ],
90
+ "license": "Apache-2.0",
91
+ "optional": true,
92
+ "os": [
93
+ "darwin"
94
+ ],
95
+ "engines": {
96
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
97
+ },
98
+ "funding": {
99
+ "url": "https://opencollective.com/libvips"
100
+ },
101
+ "optionalDependencies": {
102
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
103
+ }
104
+ },
105
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
106
+ "version": "1.2.4",
107
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
108
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
109
+ "cpu": [
110
+ "arm64"
111
+ ],
112
+ "license": "LGPL-3.0-or-later",
113
+ "optional": true,
114
+ "os": [
115
+ "darwin"
116
+ ],
117
+ "funding": {
118
+ "url": "https://opencollective.com/libvips"
119
+ }
120
+ },
121
+ "node_modules/@img/sharp-libvips-darwin-x64": {
122
+ "version": "1.2.4",
123
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
124
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
125
+ "cpu": [
126
+ "x64"
127
+ ],
128
+ "license": "LGPL-3.0-or-later",
129
+ "optional": true,
130
+ "os": [
131
+ "darwin"
132
+ ],
133
+ "funding": {
134
+ "url": "https://opencollective.com/libvips"
135
+ }
136
+ },
137
+ "node_modules/@img/sharp-libvips-linux-arm": {
138
+ "version": "1.2.4",
139
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
140
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
141
+ "cpu": [
142
+ "arm"
143
+ ],
144
+ "license": "LGPL-3.0-or-later",
145
+ "optional": true,
146
+ "os": [
147
+ "linux"
148
+ ],
149
+ "funding": {
150
+ "url": "https://opencollective.com/libvips"
151
+ }
152
+ },
153
+ "node_modules/@img/sharp-libvips-linux-arm64": {
154
+ "version": "1.2.4",
155
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
156
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
157
+ "cpu": [
158
+ "arm64"
159
+ ],
160
+ "license": "LGPL-3.0-or-later",
161
+ "optional": true,
162
+ "os": [
163
+ "linux"
164
+ ],
165
+ "funding": {
166
+ "url": "https://opencollective.com/libvips"
167
+ }
168
+ },
169
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
170
+ "version": "1.2.4",
171
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
172
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
173
+ "cpu": [
174
+ "ppc64"
175
+ ],
176
+ "license": "LGPL-3.0-or-later",
177
+ "optional": true,
178
+ "os": [
179
+ "linux"
180
+ ],
181
+ "funding": {
182
+ "url": "https://opencollective.com/libvips"
183
+ }
184
+ },
185
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
186
+ "version": "1.2.4",
187
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
188
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
189
+ "cpu": [
190
+ "riscv64"
191
+ ],
192
+ "license": "LGPL-3.0-or-later",
193
+ "optional": true,
194
+ "os": [
195
+ "linux"
196
+ ],
197
+ "funding": {
198
+ "url": "https://opencollective.com/libvips"
199
+ }
200
+ },
201
+ "node_modules/@img/sharp-libvips-linux-s390x": {
202
+ "version": "1.2.4",
203
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
204
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
205
+ "cpu": [
206
+ "s390x"
207
+ ],
208
+ "license": "LGPL-3.0-or-later",
209
+ "optional": true,
210
+ "os": [
211
+ "linux"
212
+ ],
213
+ "funding": {
214
+ "url": "https://opencollective.com/libvips"
215
+ }
216
+ },
217
+ "node_modules/@img/sharp-libvips-linux-x64": {
218
+ "version": "1.2.4",
219
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
220
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
221
+ "cpu": [
222
+ "x64"
223
+ ],
224
+ "license": "LGPL-3.0-or-later",
225
+ "optional": true,
226
+ "os": [
227
+ "linux"
228
+ ],
229
+ "funding": {
230
+ "url": "https://opencollective.com/libvips"
231
+ }
232
+ },
233
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
234
+ "version": "1.2.4",
235
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
236
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
237
+ "cpu": [
238
+ "arm64"
239
+ ],
240
+ "license": "LGPL-3.0-or-later",
241
+ "optional": true,
242
+ "os": [
243
+ "linux"
244
+ ],
245
+ "funding": {
246
+ "url": "https://opencollective.com/libvips"
247
+ }
248
+ },
249
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
250
+ "version": "1.2.4",
251
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
252
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
253
+ "cpu": [
254
+ "x64"
255
+ ],
256
+ "license": "LGPL-3.0-or-later",
257
+ "optional": true,
258
+ "os": [
259
+ "linux"
260
+ ],
261
+ "funding": {
262
+ "url": "https://opencollective.com/libvips"
263
+ }
264
+ },
265
+ "node_modules/@img/sharp-linux-arm": {
266
+ "version": "0.34.5",
267
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
268
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
269
+ "cpu": [
270
+ "arm"
271
+ ],
272
+ "license": "Apache-2.0",
273
+ "optional": true,
274
+ "os": [
275
+ "linux"
276
+ ],
277
+ "engines": {
278
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
279
+ },
280
+ "funding": {
281
+ "url": "https://opencollective.com/libvips"
282
+ },
283
+ "optionalDependencies": {
284
+ "@img/sharp-libvips-linux-arm": "1.2.4"
285
+ }
286
+ },
287
+ "node_modules/@img/sharp-linux-arm64": {
288
+ "version": "0.34.5",
289
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
290
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
291
+ "cpu": [
292
+ "arm64"
293
+ ],
294
+ "license": "Apache-2.0",
295
+ "optional": true,
296
+ "os": [
297
+ "linux"
298
+ ],
299
+ "engines": {
300
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
301
+ },
302
+ "funding": {
303
+ "url": "https://opencollective.com/libvips"
304
+ },
305
+ "optionalDependencies": {
306
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
307
+ }
308
+ },
309
+ "node_modules/@img/sharp-linux-ppc64": {
310
+ "version": "0.34.5",
311
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
312
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
313
+ "cpu": [
314
+ "ppc64"
315
+ ],
316
+ "license": "Apache-2.0",
317
+ "optional": true,
318
+ "os": [
319
+ "linux"
320
+ ],
321
+ "engines": {
322
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
323
+ },
324
+ "funding": {
325
+ "url": "https://opencollective.com/libvips"
326
+ },
327
+ "optionalDependencies": {
328
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
329
+ }
330
+ },
331
+ "node_modules/@img/sharp-linux-riscv64": {
332
+ "version": "0.34.5",
333
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
334
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
335
+ "cpu": [
336
+ "riscv64"
337
+ ],
338
+ "license": "Apache-2.0",
339
+ "optional": true,
340
+ "os": [
341
+ "linux"
342
+ ],
343
+ "engines": {
344
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
345
+ },
346
+ "funding": {
347
+ "url": "https://opencollective.com/libvips"
348
+ },
349
+ "optionalDependencies": {
350
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
351
+ }
352
+ },
353
+ "node_modules/@img/sharp-linux-s390x": {
354
+ "version": "0.34.5",
355
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
356
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
357
+ "cpu": [
358
+ "s390x"
359
+ ],
360
+ "license": "Apache-2.0",
361
+ "optional": true,
362
+ "os": [
363
+ "linux"
364
+ ],
365
+ "engines": {
366
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
367
+ },
368
+ "funding": {
369
+ "url": "https://opencollective.com/libvips"
370
+ },
371
+ "optionalDependencies": {
372
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
373
+ }
374
+ },
375
+ "node_modules/@img/sharp-linux-x64": {
376
+ "version": "0.34.5",
377
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
378
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
379
+ "cpu": [
380
+ "x64"
381
+ ],
382
+ "license": "Apache-2.0",
383
+ "optional": true,
384
+ "os": [
385
+ "linux"
386
+ ],
387
+ "engines": {
388
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
389
+ },
390
+ "funding": {
391
+ "url": "https://opencollective.com/libvips"
392
+ },
393
+ "optionalDependencies": {
394
+ "@img/sharp-libvips-linux-x64": "1.2.4"
395
+ }
396
+ },
397
+ "node_modules/@img/sharp-linuxmusl-arm64": {
398
+ "version": "0.34.5",
399
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
400
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
401
+ "cpu": [
402
+ "arm64"
403
+ ],
404
+ "license": "Apache-2.0",
405
+ "optional": true,
406
+ "os": [
407
+ "linux"
408
+ ],
409
+ "engines": {
410
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
411
+ },
412
+ "funding": {
413
+ "url": "https://opencollective.com/libvips"
414
+ },
415
+ "optionalDependencies": {
416
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
417
+ }
418
+ },
419
+ "node_modules/@img/sharp-linuxmusl-x64": {
420
+ "version": "0.34.5",
421
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
422
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
423
+ "cpu": [
424
+ "x64"
425
+ ],
426
+ "license": "Apache-2.0",
427
+ "optional": true,
428
+ "os": [
429
+ "linux"
430
+ ],
431
+ "engines": {
432
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
433
+ },
434
+ "funding": {
435
+ "url": "https://opencollective.com/libvips"
436
+ },
437
+ "optionalDependencies": {
438
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
439
+ }
440
+ },
441
+ "node_modules/@img/sharp-wasm32": {
442
+ "version": "0.34.5",
443
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
444
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
445
+ "cpu": [
446
+ "wasm32"
447
+ ],
448
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
449
+ "optional": true,
450
+ "dependencies": {
451
+ "@emnapi/runtime": "^1.7.0"
452
+ },
453
+ "engines": {
454
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
455
+ },
456
+ "funding": {
457
+ "url": "https://opencollective.com/libvips"
458
+ }
459
+ },
460
+ "node_modules/@img/sharp-win32-arm64": {
461
+ "version": "0.34.5",
462
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
463
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
464
+ "cpu": [
465
+ "arm64"
466
+ ],
467
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
468
+ "optional": true,
469
+ "os": [
470
+ "win32"
471
+ ],
472
+ "engines": {
473
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
474
+ },
475
+ "funding": {
476
+ "url": "https://opencollective.com/libvips"
477
+ }
478
+ },
479
+ "node_modules/@img/sharp-win32-ia32": {
480
+ "version": "0.34.5",
481
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
482
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
483
+ "cpu": [
484
+ "ia32"
485
+ ],
486
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
487
+ "optional": true,
488
+ "os": [
489
+ "win32"
490
+ ],
491
+ "engines": {
492
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
493
+ },
494
+ "funding": {
495
+ "url": "https://opencollective.com/libvips"
496
+ }
497
+ },
498
+ "node_modules/@img/sharp-win32-x64": {
499
+ "version": "0.34.5",
500
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
501
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
502
+ "cpu": [
503
+ "x64"
504
+ ],
505
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
506
+ "optional": true,
507
+ "os": [
508
+ "win32"
509
+ ],
510
+ "engines": {
511
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
512
+ },
513
+ "funding": {
514
+ "url": "https://opencollective.com/libvips"
515
+ }
516
+ },
517
+ "node_modules/@jridgewell/gen-mapping": {
518
+ "version": "0.3.13",
519
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
520
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
521
+ "dev": true,
522
+ "license": "MIT",
523
+ "dependencies": {
524
+ "@jridgewell/sourcemap-codec": "^1.5.0",
525
+ "@jridgewell/trace-mapping": "^0.3.24"
526
+ }
527
+ },
528
+ "node_modules/@jridgewell/resolve-uri": {
529
+ "version": "3.1.2",
530
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
531
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
532
+ "dev": true,
533
+ "license": "MIT",
534
+ "engines": {
535
+ "node": ">=6.0.0"
536
+ }
537
+ },
538
+ "node_modules/@jridgewell/sourcemap-codec": {
539
+ "version": "1.5.5",
540
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
541
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
542
+ "dev": true,
543
+ "license": "MIT"
544
+ },
545
+ "node_modules/@jridgewell/trace-mapping": {
546
+ "version": "0.3.31",
547
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
548
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
549
+ "dev": true,
550
+ "license": "MIT",
551
+ "dependencies": {
552
+ "@jridgewell/resolve-uri": "^3.1.0",
553
+ "@jridgewell/sourcemap-codec": "^1.4.14"
554
+ }
555
+ },
556
+ "node_modules/@next/env": {
557
+ "version": "15.5.15",
558
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz",
559
+ "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==",
560
+ "license": "MIT"
561
+ },
562
+ "node_modules/@next/swc-darwin-arm64": {
563
+ "version": "15.5.15",
564
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz",
565
+ "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==",
566
+ "cpu": [
567
+ "arm64"
568
+ ],
569
+ "license": "MIT",
570
+ "optional": true,
571
+ "os": [
572
+ "darwin"
573
+ ],
574
+ "engines": {
575
+ "node": ">= 10"
576
+ }
577
+ },
578
+ "node_modules/@next/swc-darwin-x64": {
579
+ "version": "15.5.15",
580
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz",
581
+ "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==",
582
+ "cpu": [
583
+ "x64"
584
+ ],
585
+ "license": "MIT",
586
+ "optional": true,
587
+ "os": [
588
+ "darwin"
589
+ ],
590
+ "engines": {
591
+ "node": ">= 10"
592
+ }
593
+ },
594
+ "node_modules/@next/swc-linux-arm64-gnu": {
595
+ "version": "15.5.15",
596
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz",
597
+ "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==",
598
+ "cpu": [
599
+ "arm64"
600
+ ],
601
+ "license": "MIT",
602
+ "optional": true,
603
+ "os": [
604
+ "linux"
605
+ ],
606
+ "engines": {
607
+ "node": ">= 10"
608
+ }
609
+ },
610
+ "node_modules/@next/swc-linux-arm64-musl": {
611
+ "version": "15.5.15",
612
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz",
613
+ "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==",
614
+ "cpu": [
615
+ "arm64"
616
+ ],
617
+ "license": "MIT",
618
+ "optional": true,
619
+ "os": [
620
+ "linux"
621
+ ],
622
+ "engines": {
623
+ "node": ">= 10"
624
+ }
625
+ },
626
+ "node_modules/@next/swc-linux-x64-gnu": {
627
+ "version": "15.5.15",
628
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz",
629
+ "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==",
630
+ "cpu": [
631
+ "x64"
632
+ ],
633
+ "license": "MIT",
634
+ "optional": true,
635
+ "os": [
636
+ "linux"
637
+ ],
638
+ "engines": {
639
+ "node": ">= 10"
640
+ }
641
+ },
642
+ "node_modules/@next/swc-linux-x64-musl": {
643
+ "version": "15.5.15",
644
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz",
645
+ "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==",
646
+ "cpu": [
647
+ "x64"
648
+ ],
649
+ "license": "MIT",
650
+ "optional": true,
651
+ "os": [
652
+ "linux"
653
+ ],
654
+ "engines": {
655
+ "node": ">= 10"
656
+ }
657
+ },
658
+ "node_modules/@next/swc-win32-arm64-msvc": {
659
+ "version": "15.5.15",
660
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz",
661
+ "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==",
662
+ "cpu": [
663
+ "arm64"
664
+ ],
665
+ "license": "MIT",
666
+ "optional": true,
667
+ "os": [
668
+ "win32"
669
+ ],
670
+ "engines": {
671
+ "node": ">= 10"
672
+ }
673
+ },
674
+ "node_modules/@next/swc-win32-x64-msvc": {
675
+ "version": "15.5.15",
676
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz",
677
+ "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==",
678
+ "cpu": [
679
+ "x64"
680
+ ],
681
+ "license": "MIT",
682
+ "optional": true,
683
+ "os": [
684
+ "win32"
685
+ ],
686
+ "engines": {
687
+ "node": ">= 10"
688
+ }
689
+ },
690
+ "node_modules/@nodelib/fs.scandir": {
691
+ "version": "2.1.5",
692
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
693
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
694
+ "dev": true,
695
+ "license": "MIT",
696
+ "dependencies": {
697
+ "@nodelib/fs.stat": "2.0.5",
698
+ "run-parallel": "^1.1.9"
699
+ },
700
+ "engines": {
701
+ "node": ">= 8"
702
+ }
703
+ },
704
+ "node_modules/@nodelib/fs.stat": {
705
+ "version": "2.0.5",
706
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
707
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
708
+ "dev": true,
709
+ "license": "MIT",
710
+ "engines": {
711
+ "node": ">= 8"
712
+ }
713
+ },
714
+ "node_modules/@nodelib/fs.walk": {
715
+ "version": "1.2.8",
716
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
717
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
718
+ "dev": true,
719
+ "license": "MIT",
720
+ "dependencies": {
721
+ "@nodelib/fs.scandir": "2.1.5",
722
+ "fastq": "^1.6.0"
723
+ },
724
+ "engines": {
725
+ "node": ">= 8"
726
+ }
727
+ },
728
+ "node_modules/@swc/helpers": {
729
+ "version": "0.5.15",
730
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
731
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
732
+ "license": "Apache-2.0",
733
+ "dependencies": {
734
+ "tslib": "^2.8.0"
735
+ }
736
+ },
737
+ "node_modules/@tanstack/query-core": {
738
+ "version": "5.100.9",
739
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
740
+ "integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==",
741
+ "license": "MIT",
742
+ "funding": {
743
+ "type": "github",
744
+ "url": "https://github.com/sponsors/tannerlinsley"
745
+ }
746
+ },
747
+ "node_modules/@tanstack/react-query": {
748
+ "version": "5.100.9",
749
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz",
750
+ "integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==",
751
+ "license": "MIT",
752
+ "dependencies": {
753
+ "@tanstack/query-core": "5.100.9"
754
+ },
755
+ "funding": {
756
+ "type": "github",
757
+ "url": "https://github.com/sponsors/tannerlinsley"
758
+ },
759
+ "peerDependencies": {
760
+ "react": "^18 || ^19"
761
+ }
762
+ },
763
+ "node_modules/@types/node": {
764
+ "version": "20.19.39",
765
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
766
+ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
767
+ "dev": true,
768
+ "license": "MIT",
769
+ "dependencies": {
770
+ "undici-types": "~6.21.0"
771
+ }
772
+ },
773
+ "node_modules/@types/prop-types": {
774
+ "version": "15.7.15",
775
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
776
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
777
+ "devOptional": true,
778
+ "license": "MIT"
779
+ },
780
+ "node_modules/@types/react": {
781
+ "version": "18.3.28",
782
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
783
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
784
+ "devOptional": true,
785
+ "license": "MIT",
786
+ "dependencies": {
787
+ "@types/prop-types": "*",
788
+ "csstype": "^3.2.2"
789
+ }
790
+ },
791
+ "node_modules/@types/react-dom": {
792
+ "version": "18.3.7",
793
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
794
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
795
+ "dev": true,
796
+ "license": "MIT",
797
+ "peerDependencies": {
798
+ "@types/react": "^18.0.0"
799
+ }
800
+ },
801
+ "node_modules/any-promise": {
802
+ "version": "1.3.0",
803
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
804
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
805
+ "dev": true,
806
+ "license": "MIT"
807
+ },
808
+ "node_modules/anymatch": {
809
+ "version": "3.1.3",
810
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
811
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
812
+ "dev": true,
813
+ "license": "ISC",
814
+ "dependencies": {
815
+ "normalize-path": "^3.0.0",
816
+ "picomatch": "^2.0.4"
817
+ },
818
+ "engines": {
819
+ "node": ">= 8"
820
+ }
821
+ },
822
+ "node_modules/arg": {
823
+ "version": "5.0.2",
824
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
825
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
826
+ "dev": true,
827
+ "license": "MIT"
828
+ },
829
+ "node_modules/autoprefixer": {
830
+ "version": "10.5.0",
831
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
832
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
833
+ "dev": true,
834
+ "funding": [
835
+ {
836
+ "type": "opencollective",
837
+ "url": "https://opencollective.com/postcss/"
838
+ },
839
+ {
840
+ "type": "tidelift",
841
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
842
+ },
843
+ {
844
+ "type": "github",
845
+ "url": "https://github.com/sponsors/ai"
846
+ }
847
+ ],
848
+ "license": "MIT",
849
+ "dependencies": {
850
+ "browserslist": "^4.28.2",
851
+ "caniuse-lite": "^1.0.30001787",
852
+ "fraction.js": "^5.3.4",
853
+ "picocolors": "^1.1.1",
854
+ "postcss-value-parser": "^4.2.0"
855
+ },
856
+ "bin": {
857
+ "autoprefixer": "bin/autoprefixer"
858
+ },
859
+ "engines": {
860
+ "node": "^10 || ^12 || >=14"
861
+ },
862
+ "peerDependencies": {
863
+ "postcss": "^8.1.0"
864
+ }
865
+ },
866
+ "node_modules/baseline-browser-mapping": {
867
+ "version": "2.10.27",
868
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz",
869
+ "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==",
870
+ "dev": true,
871
+ "license": "Apache-2.0",
872
+ "bin": {
873
+ "baseline-browser-mapping": "dist/cli.cjs"
874
+ },
875
+ "engines": {
876
+ "node": ">=6.0.0"
877
+ }
878
+ },
879
+ "node_modules/binary-extensions": {
880
+ "version": "2.3.0",
881
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
882
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
883
+ "dev": true,
884
+ "license": "MIT",
885
+ "engines": {
886
+ "node": ">=8"
887
+ },
888
+ "funding": {
889
+ "url": "https://github.com/sponsors/sindresorhus"
890
+ }
891
+ },
892
+ "node_modules/braces": {
893
+ "version": "3.0.3",
894
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
895
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
896
+ "dev": true,
897
+ "license": "MIT",
898
+ "dependencies": {
899
+ "fill-range": "^7.1.1"
900
+ },
901
+ "engines": {
902
+ "node": ">=8"
903
+ }
904
+ },
905
+ "node_modules/browserslist": {
906
+ "version": "4.28.2",
907
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
908
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
909
+ "dev": true,
910
+ "funding": [
911
+ {
912
+ "type": "opencollective",
913
+ "url": "https://opencollective.com/browserslist"
914
+ },
915
+ {
916
+ "type": "tidelift",
917
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
918
+ },
919
+ {
920
+ "type": "github",
921
+ "url": "https://github.com/sponsors/ai"
922
+ }
923
+ ],
924
+ "license": "MIT",
925
+ "dependencies": {
926
+ "baseline-browser-mapping": "^2.10.12",
927
+ "caniuse-lite": "^1.0.30001782",
928
+ "electron-to-chromium": "^1.5.328",
929
+ "node-releases": "^2.0.36",
930
+ "update-browserslist-db": "^1.2.3"
931
+ },
932
+ "bin": {
933
+ "browserslist": "cli.js"
934
+ },
935
+ "engines": {
936
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
937
+ }
938
+ },
939
+ "node_modules/camelcase-css": {
940
+ "version": "2.0.1",
941
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
942
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
943
+ "dev": true,
944
+ "license": "MIT",
945
+ "engines": {
946
+ "node": ">= 6"
947
+ }
948
+ },
949
+ "node_modules/caniuse-lite": {
950
+ "version": "1.0.30001791",
951
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
952
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
953
+ "funding": [
954
+ {
955
+ "type": "opencollective",
956
+ "url": "https://opencollective.com/browserslist"
957
+ },
958
+ {
959
+ "type": "tidelift",
960
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
961
+ },
962
+ {
963
+ "type": "github",
964
+ "url": "https://github.com/sponsors/ai"
965
+ }
966
+ ],
967
+ "license": "CC-BY-4.0"
968
+ },
969
+ "node_modules/chokidar": {
970
+ "version": "3.6.0",
971
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
972
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
973
+ "dev": true,
974
+ "license": "MIT",
975
+ "dependencies": {
976
+ "anymatch": "~3.1.2",
977
+ "braces": "~3.0.2",
978
+ "glob-parent": "~5.1.2",
979
+ "is-binary-path": "~2.1.0",
980
+ "is-glob": "~4.0.1",
981
+ "normalize-path": "~3.0.0",
982
+ "readdirp": "~3.6.0"
983
+ },
984
+ "engines": {
985
+ "node": ">= 8.10.0"
986
+ },
987
+ "funding": {
988
+ "url": "https://paulmillr.com/funding/"
989
+ },
990
+ "optionalDependencies": {
991
+ "fsevents": "~2.3.2"
992
+ }
993
+ },
994
+ "node_modules/chokidar/node_modules/glob-parent": {
995
+ "version": "5.1.2",
996
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
997
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
998
+ "dev": true,
999
+ "license": "ISC",
1000
+ "dependencies": {
1001
+ "is-glob": "^4.0.1"
1002
+ },
1003
+ "engines": {
1004
+ "node": ">= 6"
1005
+ }
1006
+ },
1007
+ "node_modules/client-only": {
1008
+ "version": "0.0.1",
1009
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
1010
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
1011
+ "license": "MIT"
1012
+ },
1013
+ "node_modules/clsx": {
1014
+ "version": "2.1.1",
1015
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1016
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1017
+ "license": "MIT",
1018
+ "engines": {
1019
+ "node": ">=6"
1020
+ }
1021
+ },
1022
+ "node_modules/commander": {
1023
+ "version": "4.1.1",
1024
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
1025
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
1026
+ "dev": true,
1027
+ "license": "MIT",
1028
+ "engines": {
1029
+ "node": ">= 6"
1030
+ }
1031
+ },
1032
+ "node_modules/cssesc": {
1033
+ "version": "3.0.0",
1034
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
1035
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
1036
+ "dev": true,
1037
+ "license": "MIT",
1038
+ "bin": {
1039
+ "cssesc": "bin/cssesc"
1040
+ },
1041
+ "engines": {
1042
+ "node": ">=4"
1043
+ }
1044
+ },
1045
+ "node_modules/csstype": {
1046
+ "version": "3.2.3",
1047
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1048
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1049
+ "devOptional": true,
1050
+ "license": "MIT"
1051
+ },
1052
+ "node_modules/detect-libc": {
1053
+ "version": "2.1.2",
1054
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1055
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1056
+ "license": "Apache-2.0",
1057
+ "optional": true,
1058
+ "engines": {
1059
+ "node": ">=8"
1060
+ }
1061
+ },
1062
+ "node_modules/didyoumean": {
1063
+ "version": "1.2.2",
1064
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
1065
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
1066
+ "dev": true,
1067
+ "license": "Apache-2.0"
1068
+ },
1069
+ "node_modules/dlv": {
1070
+ "version": "1.1.3",
1071
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
1072
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
1073
+ "dev": true,
1074
+ "license": "MIT"
1075
+ },
1076
+ "node_modules/electron-to-chromium": {
1077
+ "version": "1.5.349",
1078
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz",
1079
+ "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==",
1080
+ "dev": true,
1081
+ "license": "ISC"
1082
+ },
1083
+ "node_modules/es-errors": {
1084
+ "version": "1.3.0",
1085
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1086
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1087
+ "dev": true,
1088
+ "license": "MIT",
1089
+ "engines": {
1090
+ "node": ">= 0.4"
1091
+ }
1092
+ },
1093
+ "node_modules/escalade": {
1094
+ "version": "3.2.0",
1095
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1096
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1097
+ "dev": true,
1098
+ "license": "MIT",
1099
+ "engines": {
1100
+ "node": ">=6"
1101
+ }
1102
+ },
1103
+ "node_modules/fast-glob": {
1104
+ "version": "3.3.3",
1105
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1106
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1107
+ "dev": true,
1108
+ "license": "MIT",
1109
+ "dependencies": {
1110
+ "@nodelib/fs.stat": "^2.0.2",
1111
+ "@nodelib/fs.walk": "^1.2.3",
1112
+ "glob-parent": "^5.1.2",
1113
+ "merge2": "^1.3.0",
1114
+ "micromatch": "^4.0.8"
1115
+ },
1116
+ "engines": {
1117
+ "node": ">=8.6.0"
1118
+ }
1119
+ },
1120
+ "node_modules/fast-glob/node_modules/glob-parent": {
1121
+ "version": "5.1.2",
1122
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1123
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1124
+ "dev": true,
1125
+ "license": "ISC",
1126
+ "dependencies": {
1127
+ "is-glob": "^4.0.1"
1128
+ },
1129
+ "engines": {
1130
+ "node": ">= 6"
1131
+ }
1132
+ },
1133
+ "node_modules/fastq": {
1134
+ "version": "1.20.1",
1135
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
1136
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
1137
+ "dev": true,
1138
+ "license": "ISC",
1139
+ "dependencies": {
1140
+ "reusify": "^1.0.4"
1141
+ }
1142
+ },
1143
+ "node_modules/fill-range": {
1144
+ "version": "7.1.1",
1145
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1146
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1147
+ "dev": true,
1148
+ "license": "MIT",
1149
+ "dependencies": {
1150
+ "to-regex-range": "^5.0.1"
1151
+ },
1152
+ "engines": {
1153
+ "node": ">=8"
1154
+ }
1155
+ },
1156
+ "node_modules/fraction.js": {
1157
+ "version": "5.3.4",
1158
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
1159
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
1160
+ "dev": true,
1161
+ "license": "MIT",
1162
+ "engines": {
1163
+ "node": "*"
1164
+ },
1165
+ "funding": {
1166
+ "type": "github",
1167
+ "url": "https://github.com/sponsors/rawify"
1168
+ }
1169
+ },
1170
+ "node_modules/fsevents": {
1171
+ "version": "2.3.3",
1172
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1173
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1174
+ "dev": true,
1175
+ "hasInstallScript": true,
1176
+ "license": "MIT",
1177
+ "optional": true,
1178
+ "os": [
1179
+ "darwin"
1180
+ ],
1181
+ "engines": {
1182
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1183
+ }
1184
+ },
1185
+ "node_modules/function-bind": {
1186
+ "version": "1.1.2",
1187
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1188
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1189
+ "dev": true,
1190
+ "license": "MIT",
1191
+ "funding": {
1192
+ "url": "https://github.com/sponsors/ljharb"
1193
+ }
1194
+ },
1195
+ "node_modules/glob-parent": {
1196
+ "version": "6.0.2",
1197
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1198
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1199
+ "dev": true,
1200
+ "license": "ISC",
1201
+ "dependencies": {
1202
+ "is-glob": "^4.0.3"
1203
+ },
1204
+ "engines": {
1205
+ "node": ">=10.13.0"
1206
+ }
1207
+ },
1208
+ "node_modules/hasown": {
1209
+ "version": "2.0.3",
1210
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
1211
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
1212
+ "dev": true,
1213
+ "license": "MIT",
1214
+ "dependencies": {
1215
+ "function-bind": "^1.1.2"
1216
+ },
1217
+ "engines": {
1218
+ "node": ">= 0.4"
1219
+ }
1220
+ },
1221
+ "node_modules/is-binary-path": {
1222
+ "version": "2.1.0",
1223
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1224
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1225
+ "dev": true,
1226
+ "license": "MIT",
1227
+ "dependencies": {
1228
+ "binary-extensions": "^2.0.0"
1229
+ },
1230
+ "engines": {
1231
+ "node": ">=8"
1232
+ }
1233
+ },
1234
+ "node_modules/is-core-module": {
1235
+ "version": "2.16.1",
1236
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
1237
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
1238
+ "dev": true,
1239
+ "license": "MIT",
1240
+ "dependencies": {
1241
+ "hasown": "^2.0.2"
1242
+ },
1243
+ "engines": {
1244
+ "node": ">= 0.4"
1245
+ },
1246
+ "funding": {
1247
+ "url": "https://github.com/sponsors/ljharb"
1248
+ }
1249
+ },
1250
+ "node_modules/is-extglob": {
1251
+ "version": "2.1.1",
1252
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1253
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1254
+ "dev": true,
1255
+ "license": "MIT",
1256
+ "engines": {
1257
+ "node": ">=0.10.0"
1258
+ }
1259
+ },
1260
+ "node_modules/is-glob": {
1261
+ "version": "4.0.3",
1262
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1263
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1264
+ "dev": true,
1265
+ "license": "MIT",
1266
+ "dependencies": {
1267
+ "is-extglob": "^2.1.1"
1268
+ },
1269
+ "engines": {
1270
+ "node": ">=0.10.0"
1271
+ }
1272
+ },
1273
+ "node_modules/is-number": {
1274
+ "version": "7.0.0",
1275
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1276
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1277
+ "dev": true,
1278
+ "license": "MIT",
1279
+ "engines": {
1280
+ "node": ">=0.12.0"
1281
+ }
1282
+ },
1283
+ "node_modules/jiti": {
1284
+ "version": "1.21.7",
1285
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1286
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1287
+ "dev": true,
1288
+ "license": "MIT",
1289
+ "bin": {
1290
+ "jiti": "bin/jiti.js"
1291
+ }
1292
+ },
1293
+ "node_modules/js-tokens": {
1294
+ "version": "4.0.0",
1295
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1296
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1297
+ "license": "MIT"
1298
+ },
1299
+ "node_modules/lilconfig": {
1300
+ "version": "3.1.3",
1301
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1302
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1303
+ "dev": true,
1304
+ "license": "MIT",
1305
+ "engines": {
1306
+ "node": ">=14"
1307
+ },
1308
+ "funding": {
1309
+ "url": "https://github.com/sponsors/antonk52"
1310
+ }
1311
+ },
1312
+ "node_modules/lines-and-columns": {
1313
+ "version": "1.2.4",
1314
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1315
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1316
+ "dev": true,
1317
+ "license": "MIT"
1318
+ },
1319
+ "node_modules/loose-envify": {
1320
+ "version": "1.4.0",
1321
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1322
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1323
+ "license": "MIT",
1324
+ "dependencies": {
1325
+ "js-tokens": "^3.0.0 || ^4.0.0"
1326
+ },
1327
+ "bin": {
1328
+ "loose-envify": "cli.js"
1329
+ }
1330
+ },
1331
+ "node_modules/merge2": {
1332
+ "version": "1.4.1",
1333
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1334
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1335
+ "dev": true,
1336
+ "license": "MIT",
1337
+ "engines": {
1338
+ "node": ">= 8"
1339
+ }
1340
+ },
1341
+ "node_modules/micromatch": {
1342
+ "version": "4.0.8",
1343
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1344
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1345
+ "dev": true,
1346
+ "license": "MIT",
1347
+ "dependencies": {
1348
+ "braces": "^3.0.3",
1349
+ "picomatch": "^2.3.1"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">=8.6"
1353
+ }
1354
+ },
1355
+ "node_modules/mz": {
1356
+ "version": "2.7.0",
1357
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1358
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1359
+ "dev": true,
1360
+ "license": "MIT",
1361
+ "dependencies": {
1362
+ "any-promise": "^1.0.0",
1363
+ "object-assign": "^4.0.1",
1364
+ "thenify-all": "^1.0.0"
1365
+ }
1366
+ },
1367
+ "node_modules/nanoid": {
1368
+ "version": "3.3.12",
1369
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
1370
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
1371
+ "funding": [
1372
+ {
1373
+ "type": "github",
1374
+ "url": "https://github.com/sponsors/ai"
1375
+ }
1376
+ ],
1377
+ "license": "MIT",
1378
+ "bin": {
1379
+ "nanoid": "bin/nanoid.cjs"
1380
+ },
1381
+ "engines": {
1382
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1383
+ }
1384
+ },
1385
+ "node_modules/next": {
1386
+ "version": "15.5.15",
1387
+ "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz",
1388
+ "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==",
1389
+ "license": "MIT",
1390
+ "dependencies": {
1391
+ "@next/env": "15.5.15",
1392
+ "@swc/helpers": "0.5.15",
1393
+ "caniuse-lite": "^1.0.30001579",
1394
+ "postcss": "8.4.31",
1395
+ "styled-jsx": "5.1.6"
1396
+ },
1397
+ "bin": {
1398
+ "next": "dist/bin/next"
1399
+ },
1400
+ "engines": {
1401
+ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
1402
+ },
1403
+ "optionalDependencies": {
1404
+ "@next/swc-darwin-arm64": "15.5.15",
1405
+ "@next/swc-darwin-x64": "15.5.15",
1406
+ "@next/swc-linux-arm64-gnu": "15.5.15",
1407
+ "@next/swc-linux-arm64-musl": "15.5.15",
1408
+ "@next/swc-linux-x64-gnu": "15.5.15",
1409
+ "@next/swc-linux-x64-musl": "15.5.15",
1410
+ "@next/swc-win32-arm64-msvc": "15.5.15",
1411
+ "@next/swc-win32-x64-msvc": "15.5.15",
1412
+ "sharp": "^0.34.3"
1413
+ },
1414
+ "peerDependencies": {
1415
+ "@opentelemetry/api": "^1.1.0",
1416
+ "@playwright/test": "^1.51.1",
1417
+ "babel-plugin-react-compiler": "*",
1418
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1419
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1420
+ "sass": "^1.3.0"
1421
+ },
1422
+ "peerDependenciesMeta": {
1423
+ "@opentelemetry/api": {
1424
+ "optional": true
1425
+ },
1426
+ "@playwright/test": {
1427
+ "optional": true
1428
+ },
1429
+ "babel-plugin-react-compiler": {
1430
+ "optional": true
1431
+ },
1432
+ "sass": {
1433
+ "optional": true
1434
+ }
1435
+ }
1436
+ },
1437
+ "node_modules/node-releases": {
1438
+ "version": "2.0.38",
1439
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
1440
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
1441
+ "dev": true,
1442
+ "license": "MIT"
1443
+ },
1444
+ "node_modules/normalize-path": {
1445
+ "version": "3.0.0",
1446
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1447
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1448
+ "dev": true,
1449
+ "license": "MIT",
1450
+ "engines": {
1451
+ "node": ">=0.10.0"
1452
+ }
1453
+ },
1454
+ "node_modules/object-assign": {
1455
+ "version": "4.1.1",
1456
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1457
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1458
+ "dev": true,
1459
+ "license": "MIT",
1460
+ "engines": {
1461
+ "node": ">=0.10.0"
1462
+ }
1463
+ },
1464
+ "node_modules/object-hash": {
1465
+ "version": "3.0.0",
1466
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1467
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1468
+ "dev": true,
1469
+ "license": "MIT",
1470
+ "engines": {
1471
+ "node": ">= 6"
1472
+ }
1473
+ },
1474
+ "node_modules/path-parse": {
1475
+ "version": "1.0.7",
1476
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1477
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1478
+ "dev": true,
1479
+ "license": "MIT"
1480
+ },
1481
+ "node_modules/picocolors": {
1482
+ "version": "1.1.1",
1483
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1484
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1485
+ "license": "ISC"
1486
+ },
1487
+ "node_modules/picomatch": {
1488
+ "version": "2.3.2",
1489
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
1490
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1491
+ "dev": true,
1492
+ "license": "MIT",
1493
+ "engines": {
1494
+ "node": ">=8.6"
1495
+ },
1496
+ "funding": {
1497
+ "url": "https://github.com/sponsors/jonschlinkert"
1498
+ }
1499
+ },
1500
+ "node_modules/pify": {
1501
+ "version": "2.3.0",
1502
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1503
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1504
+ "dev": true,
1505
+ "license": "MIT",
1506
+ "engines": {
1507
+ "node": ">=0.10.0"
1508
+ }
1509
+ },
1510
+ "node_modules/pirates": {
1511
+ "version": "4.0.7",
1512
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1513
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1514
+ "dev": true,
1515
+ "license": "MIT",
1516
+ "engines": {
1517
+ "node": ">= 6"
1518
+ }
1519
+ },
1520
+ "node_modules/postcss": {
1521
+ "version": "8.5.13",
1522
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
1523
+ "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
1524
+ "funding": [
1525
+ {
1526
+ "type": "opencollective",
1527
+ "url": "https://opencollective.com/postcss/"
1528
+ },
1529
+ {
1530
+ "type": "tidelift",
1531
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1532
+ },
1533
+ {
1534
+ "type": "github",
1535
+ "url": "https://github.com/sponsors/ai"
1536
+ }
1537
+ ],
1538
+ "license": "MIT",
1539
+ "dependencies": {
1540
+ "nanoid": "^3.3.11",
1541
+ "picocolors": "^1.1.1",
1542
+ "source-map-js": "^1.2.1"
1543
+ },
1544
+ "engines": {
1545
+ "node": "^10 || ^12 || >=14"
1546
+ }
1547
+ },
1548
+ "node_modules/postcss-import": {
1549
+ "version": "15.1.0",
1550
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1551
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1552
+ "dev": true,
1553
+ "license": "MIT",
1554
+ "dependencies": {
1555
+ "postcss-value-parser": "^4.0.0",
1556
+ "read-cache": "^1.0.0",
1557
+ "resolve": "^1.1.7"
1558
+ },
1559
+ "engines": {
1560
+ "node": ">=14.0.0"
1561
+ },
1562
+ "peerDependencies": {
1563
+ "postcss": "^8.0.0"
1564
+ }
1565
+ },
1566
+ "node_modules/postcss-js": {
1567
+ "version": "4.1.0",
1568
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
1569
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
1570
+ "dev": true,
1571
+ "funding": [
1572
+ {
1573
+ "type": "opencollective",
1574
+ "url": "https://opencollective.com/postcss/"
1575
+ },
1576
+ {
1577
+ "type": "github",
1578
+ "url": "https://github.com/sponsors/ai"
1579
+ }
1580
+ ],
1581
+ "license": "MIT",
1582
+ "dependencies": {
1583
+ "camelcase-css": "^2.0.1"
1584
+ },
1585
+ "engines": {
1586
+ "node": "^12 || ^14 || >= 16"
1587
+ },
1588
+ "peerDependencies": {
1589
+ "postcss": "^8.4.21"
1590
+ }
1591
+ },
1592
+ "node_modules/postcss-load-config": {
1593
+ "version": "6.0.1",
1594
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
1595
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
1596
+ "dev": true,
1597
+ "funding": [
1598
+ {
1599
+ "type": "opencollective",
1600
+ "url": "https://opencollective.com/postcss/"
1601
+ },
1602
+ {
1603
+ "type": "github",
1604
+ "url": "https://github.com/sponsors/ai"
1605
+ }
1606
+ ],
1607
+ "license": "MIT",
1608
+ "dependencies": {
1609
+ "lilconfig": "^3.1.1"
1610
+ },
1611
+ "engines": {
1612
+ "node": ">= 18"
1613
+ },
1614
+ "peerDependencies": {
1615
+ "jiti": ">=1.21.0",
1616
+ "postcss": ">=8.0.9",
1617
+ "tsx": "^4.8.1",
1618
+ "yaml": "^2.4.2"
1619
+ },
1620
+ "peerDependenciesMeta": {
1621
+ "jiti": {
1622
+ "optional": true
1623
+ },
1624
+ "postcss": {
1625
+ "optional": true
1626
+ },
1627
+ "tsx": {
1628
+ "optional": true
1629
+ },
1630
+ "yaml": {
1631
+ "optional": true
1632
+ }
1633
+ }
1634
+ },
1635
+ "node_modules/postcss-nested": {
1636
+ "version": "6.2.0",
1637
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1638
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1639
+ "dev": true,
1640
+ "funding": [
1641
+ {
1642
+ "type": "opencollective",
1643
+ "url": "https://opencollective.com/postcss/"
1644
+ },
1645
+ {
1646
+ "type": "github",
1647
+ "url": "https://github.com/sponsors/ai"
1648
+ }
1649
+ ],
1650
+ "license": "MIT",
1651
+ "dependencies": {
1652
+ "postcss-selector-parser": "^6.1.1"
1653
+ },
1654
+ "engines": {
1655
+ "node": ">=12.0"
1656
+ },
1657
+ "peerDependencies": {
1658
+ "postcss": "^8.2.14"
1659
+ }
1660
+ },
1661
+ "node_modules/postcss-selector-parser": {
1662
+ "version": "6.1.2",
1663
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1664
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1665
+ "dev": true,
1666
+ "license": "MIT",
1667
+ "dependencies": {
1668
+ "cssesc": "^3.0.0",
1669
+ "util-deprecate": "^1.0.2"
1670
+ },
1671
+ "engines": {
1672
+ "node": ">=4"
1673
+ }
1674
+ },
1675
+ "node_modules/postcss-value-parser": {
1676
+ "version": "4.2.0",
1677
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1678
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1679
+ "dev": true,
1680
+ "license": "MIT"
1681
+ },
1682
+ "node_modules/queue-microtask": {
1683
+ "version": "1.2.3",
1684
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1685
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1686
+ "dev": true,
1687
+ "funding": [
1688
+ {
1689
+ "type": "github",
1690
+ "url": "https://github.com/sponsors/feross"
1691
+ },
1692
+ {
1693
+ "type": "patreon",
1694
+ "url": "https://www.patreon.com/feross"
1695
+ },
1696
+ {
1697
+ "type": "consulting",
1698
+ "url": "https://feross.org/support"
1699
+ }
1700
+ ],
1701
+ "license": "MIT"
1702
+ },
1703
+ "node_modules/react": {
1704
+ "version": "18.3.1",
1705
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1706
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1707
+ "license": "MIT",
1708
+ "dependencies": {
1709
+ "loose-envify": "^1.1.0"
1710
+ },
1711
+ "engines": {
1712
+ "node": ">=0.10.0"
1713
+ }
1714
+ },
1715
+ "node_modules/react-dom": {
1716
+ "version": "18.3.1",
1717
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1718
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1719
+ "license": "MIT",
1720
+ "dependencies": {
1721
+ "loose-envify": "^1.1.0",
1722
+ "scheduler": "^0.23.2"
1723
+ },
1724
+ "peerDependencies": {
1725
+ "react": "^18.3.1"
1726
+ }
1727
+ },
1728
+ "node_modules/read-cache": {
1729
+ "version": "1.0.0",
1730
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1731
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1732
+ "dev": true,
1733
+ "license": "MIT",
1734
+ "dependencies": {
1735
+ "pify": "^2.3.0"
1736
+ }
1737
+ },
1738
+ "node_modules/readdirp": {
1739
+ "version": "3.6.0",
1740
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1741
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1742
+ "dev": true,
1743
+ "license": "MIT",
1744
+ "dependencies": {
1745
+ "picomatch": "^2.2.1"
1746
+ },
1747
+ "engines": {
1748
+ "node": ">=8.10.0"
1749
+ }
1750
+ },
1751
+ "node_modules/resolve": {
1752
+ "version": "1.22.12",
1753
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
1754
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
1755
+ "dev": true,
1756
+ "license": "MIT",
1757
+ "dependencies": {
1758
+ "es-errors": "^1.3.0",
1759
+ "is-core-module": "^2.16.1",
1760
+ "path-parse": "^1.0.7",
1761
+ "supports-preserve-symlinks-flag": "^1.0.0"
1762
+ },
1763
+ "bin": {
1764
+ "resolve": "bin/resolve"
1765
+ },
1766
+ "engines": {
1767
+ "node": ">= 0.4"
1768
+ },
1769
+ "funding": {
1770
+ "url": "https://github.com/sponsors/ljharb"
1771
+ }
1772
+ },
1773
+ "node_modules/reusify": {
1774
+ "version": "1.1.0",
1775
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1776
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1777
+ "dev": true,
1778
+ "license": "MIT",
1779
+ "engines": {
1780
+ "iojs": ">=1.0.0",
1781
+ "node": ">=0.10.0"
1782
+ }
1783
+ },
1784
+ "node_modules/run-parallel": {
1785
+ "version": "1.2.0",
1786
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1787
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1788
+ "dev": true,
1789
+ "funding": [
1790
+ {
1791
+ "type": "github",
1792
+ "url": "https://github.com/sponsors/feross"
1793
+ },
1794
+ {
1795
+ "type": "patreon",
1796
+ "url": "https://www.patreon.com/feross"
1797
+ },
1798
+ {
1799
+ "type": "consulting",
1800
+ "url": "https://feross.org/support"
1801
+ }
1802
+ ],
1803
+ "license": "MIT",
1804
+ "dependencies": {
1805
+ "queue-microtask": "^1.2.2"
1806
+ }
1807
+ },
1808
+ "node_modules/scheduler": {
1809
+ "version": "0.23.2",
1810
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1811
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1812
+ "license": "MIT",
1813
+ "dependencies": {
1814
+ "loose-envify": "^1.1.0"
1815
+ }
1816
+ },
1817
+ "node_modules/semver": {
1818
+ "version": "7.7.4",
1819
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1820
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1821
+ "license": "ISC",
1822
+ "optional": true,
1823
+ "bin": {
1824
+ "semver": "bin/semver.js"
1825
+ },
1826
+ "engines": {
1827
+ "node": ">=10"
1828
+ }
1829
+ },
1830
+ "node_modules/sharp": {
1831
+ "version": "0.34.5",
1832
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
1833
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
1834
+ "hasInstallScript": true,
1835
+ "license": "Apache-2.0",
1836
+ "optional": true,
1837
+ "dependencies": {
1838
+ "@img/colour": "^1.0.0",
1839
+ "detect-libc": "^2.1.2",
1840
+ "semver": "^7.7.3"
1841
+ },
1842
+ "engines": {
1843
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1844
+ },
1845
+ "funding": {
1846
+ "url": "https://opencollective.com/libvips"
1847
+ },
1848
+ "optionalDependencies": {
1849
+ "@img/sharp-darwin-arm64": "0.34.5",
1850
+ "@img/sharp-darwin-x64": "0.34.5",
1851
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
1852
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
1853
+ "@img/sharp-libvips-linux-arm": "1.2.4",
1854
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
1855
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
1856
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
1857
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
1858
+ "@img/sharp-libvips-linux-x64": "1.2.4",
1859
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
1860
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
1861
+ "@img/sharp-linux-arm": "0.34.5",
1862
+ "@img/sharp-linux-arm64": "0.34.5",
1863
+ "@img/sharp-linux-ppc64": "0.34.5",
1864
+ "@img/sharp-linux-riscv64": "0.34.5",
1865
+ "@img/sharp-linux-s390x": "0.34.5",
1866
+ "@img/sharp-linux-x64": "0.34.5",
1867
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
1868
+ "@img/sharp-linuxmusl-x64": "0.34.5",
1869
+ "@img/sharp-wasm32": "0.34.5",
1870
+ "@img/sharp-win32-arm64": "0.34.5",
1871
+ "@img/sharp-win32-ia32": "0.34.5",
1872
+ "@img/sharp-win32-x64": "0.34.5"
1873
+ }
1874
+ },
1875
+ "node_modules/source-map-js": {
1876
+ "version": "1.2.1",
1877
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1878
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1879
+ "license": "BSD-3-Clause",
1880
+ "engines": {
1881
+ "node": ">=0.10.0"
1882
+ }
1883
+ },
1884
+ "node_modules/styled-jsx": {
1885
+ "version": "5.1.6",
1886
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
1887
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
1888
+ "license": "MIT",
1889
+ "dependencies": {
1890
+ "client-only": "0.0.1"
1891
+ },
1892
+ "engines": {
1893
+ "node": ">= 12.0.0"
1894
+ },
1895
+ "peerDependencies": {
1896
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
1897
+ },
1898
+ "peerDependenciesMeta": {
1899
+ "@babel/core": {
1900
+ "optional": true
1901
+ },
1902
+ "babel-plugin-macros": {
1903
+ "optional": true
1904
+ }
1905
+ }
1906
+ },
1907
+ "node_modules/sucrase": {
1908
+ "version": "3.35.1",
1909
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
1910
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
1911
+ "dev": true,
1912
+ "license": "MIT",
1913
+ "dependencies": {
1914
+ "@jridgewell/gen-mapping": "^0.3.2",
1915
+ "commander": "^4.0.0",
1916
+ "lines-and-columns": "^1.1.6",
1917
+ "mz": "^2.7.0",
1918
+ "pirates": "^4.0.1",
1919
+ "tinyglobby": "^0.2.11",
1920
+ "ts-interface-checker": "^0.1.9"
1921
+ },
1922
+ "bin": {
1923
+ "sucrase": "bin/sucrase",
1924
+ "sucrase-node": "bin/sucrase-node"
1925
+ },
1926
+ "engines": {
1927
+ "node": ">=16 || 14 >=14.17"
1928
+ }
1929
+ },
1930
+ "node_modules/supports-preserve-symlinks-flag": {
1931
+ "version": "1.0.0",
1932
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1933
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1934
+ "dev": true,
1935
+ "license": "MIT",
1936
+ "engines": {
1937
+ "node": ">= 0.4"
1938
+ },
1939
+ "funding": {
1940
+ "url": "https://github.com/sponsors/ljharb"
1941
+ }
1942
+ },
1943
+ "node_modules/tailwindcss": {
1944
+ "version": "3.4.19",
1945
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
1946
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
1947
+ "dev": true,
1948
+ "license": "MIT",
1949
+ "dependencies": {
1950
+ "@alloc/quick-lru": "^5.2.0",
1951
+ "arg": "^5.0.2",
1952
+ "chokidar": "^3.6.0",
1953
+ "didyoumean": "^1.2.2",
1954
+ "dlv": "^1.1.3",
1955
+ "fast-glob": "^3.3.2",
1956
+ "glob-parent": "^6.0.2",
1957
+ "is-glob": "^4.0.3",
1958
+ "jiti": "^1.21.7",
1959
+ "lilconfig": "^3.1.3",
1960
+ "micromatch": "^4.0.8",
1961
+ "normalize-path": "^3.0.0",
1962
+ "object-hash": "^3.0.0",
1963
+ "picocolors": "^1.1.1",
1964
+ "postcss": "^8.4.47",
1965
+ "postcss-import": "^15.1.0",
1966
+ "postcss-js": "^4.0.1",
1967
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
1968
+ "postcss-nested": "^6.2.0",
1969
+ "postcss-selector-parser": "^6.1.2",
1970
+ "resolve": "^1.22.8",
1971
+ "sucrase": "^3.35.0"
1972
+ },
1973
+ "bin": {
1974
+ "tailwind": "lib/cli.js",
1975
+ "tailwindcss": "lib/cli.js"
1976
+ },
1977
+ "engines": {
1978
+ "node": ">=14.0.0"
1979
+ }
1980
+ },
1981
+ "node_modules/thenify": {
1982
+ "version": "3.3.1",
1983
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
1984
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
1985
+ "dev": true,
1986
+ "license": "MIT",
1987
+ "dependencies": {
1988
+ "any-promise": "^1.0.0"
1989
+ }
1990
+ },
1991
+ "node_modules/thenify-all": {
1992
+ "version": "1.6.0",
1993
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
1994
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
1995
+ "dev": true,
1996
+ "license": "MIT",
1997
+ "dependencies": {
1998
+ "thenify": ">= 3.1.0 < 4"
1999
+ },
2000
+ "engines": {
2001
+ "node": ">=0.8"
2002
+ }
2003
+ },
2004
+ "node_modules/tinyglobby": {
2005
+ "version": "0.2.16",
2006
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2007
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2008
+ "dev": true,
2009
+ "license": "MIT",
2010
+ "dependencies": {
2011
+ "fdir": "^6.5.0",
2012
+ "picomatch": "^4.0.4"
2013
+ },
2014
+ "engines": {
2015
+ "node": ">=12.0.0"
2016
+ },
2017
+ "funding": {
2018
+ "url": "https://github.com/sponsors/SuperchupuDev"
2019
+ }
2020
+ },
2021
+ "node_modules/tinyglobby/node_modules/fdir": {
2022
+ "version": "6.5.0",
2023
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2024
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2025
+ "dev": true,
2026
+ "license": "MIT",
2027
+ "engines": {
2028
+ "node": ">=12.0.0"
2029
+ },
2030
+ "peerDependencies": {
2031
+ "picomatch": "^3 || ^4"
2032
+ },
2033
+ "peerDependenciesMeta": {
2034
+ "picomatch": {
2035
+ "optional": true
2036
+ }
2037
+ }
2038
+ },
2039
+ "node_modules/tinyglobby/node_modules/picomatch": {
2040
+ "version": "4.0.4",
2041
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2042
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2043
+ "dev": true,
2044
+ "license": "MIT",
2045
+ "engines": {
2046
+ "node": ">=12"
2047
+ },
2048
+ "funding": {
2049
+ "url": "https://github.com/sponsors/jonschlinkert"
2050
+ }
2051
+ },
2052
+ "node_modules/to-regex-range": {
2053
+ "version": "5.0.1",
2054
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2055
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2056
+ "dev": true,
2057
+ "license": "MIT",
2058
+ "dependencies": {
2059
+ "is-number": "^7.0.0"
2060
+ },
2061
+ "engines": {
2062
+ "node": ">=8.0"
2063
+ }
2064
+ },
2065
+ "node_modules/ts-interface-checker": {
2066
+ "version": "0.1.13",
2067
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
2068
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
2069
+ "dev": true,
2070
+ "license": "Apache-2.0"
2071
+ },
2072
+ "node_modules/tslib": {
2073
+ "version": "2.8.1",
2074
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2075
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2076
+ "license": "0BSD"
2077
+ },
2078
+ "node_modules/typescript": {
2079
+ "version": "5.9.3",
2080
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
2081
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2082
+ "dev": true,
2083
+ "license": "Apache-2.0",
2084
+ "bin": {
2085
+ "tsc": "bin/tsc",
2086
+ "tsserver": "bin/tsserver"
2087
+ },
2088
+ "engines": {
2089
+ "node": ">=14.17"
2090
+ }
2091
+ },
2092
+ "node_modules/undici-types": {
2093
+ "version": "6.21.0",
2094
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2095
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2096
+ "dev": true,
2097
+ "license": "MIT"
2098
+ },
2099
+ "node_modules/update-browserslist-db": {
2100
+ "version": "1.2.3",
2101
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2102
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2103
+ "dev": true,
2104
+ "funding": [
2105
+ {
2106
+ "type": "opencollective",
2107
+ "url": "https://opencollective.com/browserslist"
2108
+ },
2109
+ {
2110
+ "type": "tidelift",
2111
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2112
+ },
2113
+ {
2114
+ "type": "github",
2115
+ "url": "https://github.com/sponsors/ai"
2116
+ }
2117
+ ],
2118
+ "license": "MIT",
2119
+ "dependencies": {
2120
+ "escalade": "^3.2.0",
2121
+ "picocolors": "^1.1.1"
2122
+ },
2123
+ "bin": {
2124
+ "update-browserslist-db": "cli.js"
2125
+ },
2126
+ "peerDependencies": {
2127
+ "browserslist": ">= 4.21.0"
2128
+ }
2129
+ },
2130
+ "node_modules/use-sync-external-store": {
2131
+ "version": "1.6.0",
2132
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
2133
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
2134
+ "license": "MIT",
2135
+ "peerDependencies": {
2136
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2137
+ }
2138
+ },
2139
+ "node_modules/util-deprecate": {
2140
+ "version": "1.0.2",
2141
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2142
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2143
+ "dev": true,
2144
+ "license": "MIT"
2145
+ },
2146
+ "node_modules/zustand": {
2147
+ "version": "4.5.7",
2148
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
2149
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
2150
+ "license": "MIT",
2151
+ "dependencies": {
2152
+ "use-sync-external-store": "^1.2.2"
2153
+ },
2154
+ "engines": {
2155
+ "node": ">=12.7.0"
2156
+ },
2157
+ "peerDependencies": {
2158
+ "@types/react": ">=16.8",
2159
+ "immer": ">=9.0.6",
2160
+ "react": ">=16.8"
2161
+ },
2162
+ "peerDependenciesMeta": {
2163
+ "@types/react": {
2164
+ "optional": true
2165
+ },
2166
+ "immer": {
2167
+ "optional": true
2168
+ },
2169
+ "react": {
2170
+ "optional": true
2171
+ }
2172
+ }
2173
+ }
2174
+ }
2175
+ }
package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "etiya-d2l-ui",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@tanstack/react-query": "^5.59.0",
14
+ "clsx": "^2.1.1",
15
+ "next": "^15.5.15",
16
+ "react": "^18.3.1",
17
+ "react-dom": "^18.3.1",
18
+ "zustand": "^4.5.5"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.16.10",
22
+ "@types/react": "^18.3.11",
23
+ "@types/react-dom": "^18.3.0",
24
+ "autoprefixer": "^10.4.20",
25
+ "postcss": "^8.5.13",
26
+ "tailwindcss": "^3.4.13",
27
+ "typescript": "^5.6.2"
28
+ },
29
+ "overrides": {
30
+ "postcss": "^8.5.13"
31
+ }
32
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
public/.gitkeep ADDED
File without changes
scripts/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
scripts/deploy-to-hf-space.sh ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Deploy this frontend folder to its own HF Space (Etiya/d2l-ui).
3
+ #
4
+ # Usage:
5
+ # ./scripts/deploy-to-hf-space.sh # incremental push
6
+ # ./scripts/deploy-to-hf-space.sh --create # first-time, create Space then push
7
+ #
8
+ # Pre-requisites:
9
+ # - hf CLI logged in (`hf auth login`)
10
+ # - Write access to Etiya org
11
+ # - Run from frontend/ directory
12
+
13
+ set -euo pipefail
14
+
15
+ SPACE_OWNER="Etiya"
16
+ SPACE_NAME="d2l-ui"
17
+ REMOTE_URL="https://huggingface.co/spaces/${SPACE_OWNER}/${SPACE_NAME}"
18
+
19
+ # Resolve script-relative paths so the script is location-independent.
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ FRONTEND_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
22
+ cd "${FRONTEND_DIR}"
23
+
24
+ if [[ "${1:-}" == "--create" ]]; then
25
+ echo "▶ Creating Space ${SPACE_OWNER}/${SPACE_NAME}…"
26
+ hf repo create "${SPACE_NAME}" \
27
+ --repo-type space \
28
+ --space-sdk docker \
29
+ --private \
30
+ --organization "${SPACE_OWNER}" \
31
+ --exist-ok
32
+ fi
33
+
34
+ # Sanity: ensure SPACE_README.md exists; HF needs the YAML frontmatter.
35
+ if [[ ! -f SPACE_README.md ]]; then
36
+ echo "ERROR: SPACE_README.md missing — Space requires it as the deployed README." >&2
37
+ exit 1
38
+ fi
39
+
40
+ # Stash dev README, swap in SPACE_README, push, restore.
41
+ TMP_DEV_README="$(mktemp /tmp/dev-readme.XXXXXX.md)"
42
+ trap 'mv "${TMP_DEV_README}" README.md 2>/dev/null || true' EXIT
43
+ cp README.md "${TMP_DEV_README}"
44
+ cp SPACE_README.md README.md
45
+
46
+ # Initialize git remote if needed.
47
+ if [[ ! -d .git ]]; then
48
+ echo "▶ Initializing git…"
49
+ git init -b main >/dev/null
50
+ git remote add origin "${REMOTE_URL}"
51
+ fi
52
+
53
+ # If origin remote exists but mismatches, fix.
54
+ CURRENT_REMOTE="$(git remote get-url origin 2>/dev/null || true)"
55
+ if [[ "${CURRENT_REMOTE}" != "${REMOTE_URL}" ]]; then
56
+ if [[ -n "${CURRENT_REMOTE}" ]]; then
57
+ git remote set-url origin "${REMOTE_URL}"
58
+ else
59
+ git remote add origin "${REMOTE_URL}"
60
+ fi
61
+ fi
62
+
63
+ # Pull existing remote (if it exists) — handles HF's auto-generated initial commit.
64
+ git fetch origin main 2>/dev/null || true
65
+ git pull --rebase origin main 2>/dev/null || true
66
+
67
+ # Stage + commit. node_modules / .next ignored by .gitignore.
68
+ git add -A
69
+ if git diff --cached --quiet; then
70
+ echo "▶ No changes to commit; pushing existing HEAD."
71
+ else
72
+ git commit -m "Deploy: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
73
+ fi
74
+
75
+ echo "▶ Pushing to ${REMOTE_URL}…"
76
+ git push -u origin main
77
+
78
+ echo ""
79
+ echo "✓ Done. Build will start automatically. Track at:"
80
+ echo " ${REMOTE_URL}?logs=build"
81
+ echo ""
82
+ echo "Once running, open:"
83
+ echo " https://etiya-d2l-ui.hf.space"
84
+ echo ""
85
+ echo "If you haven't yet: set HF_TOKEN as a Space Secret at"
86
+ echo " ${REMOTE_URL}/settings"
tailwind.config.ts ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ /**
4
+ * Etiya doc-to-lora UI — design tokens.
5
+ *
6
+ * Aesthetic: editorial museum-grade technical instrumentation.
7
+ * Apple-inspired chassis (alternating tile rhythm, single blue accent,
8
+ * Apple-tight typography) reinterpreted for an AI/RAG dashboard.
9
+ *
10
+ * One accent color (Action Blue). One soft elevation (product-shadow,
11
+ * here applied to the answer card). No gradients. No 500 weight.
12
+ */
13
+ const config: Config = {
14
+ content: [
15
+ "./app/**/*.{ts,tsx}",
16
+ "./components/**/*.{ts,tsx}",
17
+ "./lib/**/*.{ts,tsx}",
18
+ ],
19
+ theme: {
20
+ extend: {
21
+ colors: {
22
+ // Brand & Accent
23
+ primary: "#0066cc",
24
+ "primary-focus": "#0071e3",
25
+ "primary-on-dark": "#2997ff",
26
+
27
+ // Surface
28
+ canvas: "#ffffff",
29
+ "canvas-parchment": "#f5f5f7",
30
+ "surface-pearl": "#fafafc",
31
+ "surface-tile-1": "#272729",
32
+ "surface-tile-2": "#2a2a2c",
33
+ "surface-tile-3": "#252527",
34
+ "surface-black": "#000000",
35
+
36
+ // Text
37
+ ink: "#1d1d1f",
38
+ "ink-80": "#333333",
39
+ "ink-48": "#7a7a7a",
40
+ "body-on-dark": "#ffffff",
41
+ "body-muted": "#cccccc",
42
+
43
+ // Borders / Hairlines
44
+ "divider-soft": "#f0f0f0",
45
+ hairline: "#e0e0e0",
46
+
47
+ // Status
48
+ "status-ok": "#1d8348",
49
+ "status-warn": "#b7791f",
50
+ "status-err": "#c73032",
51
+ },
52
+ fontFamily: {
53
+ sans: ["var(--font-inter)", "system-ui", "-apple-system", "sans-serif"],
54
+ mono: ["var(--font-jetbrains)", "ui-monospace", "monospace"],
55
+ },
56
+ fontSize: {
57
+ // Display ladder (Apple-tight tracking applied via letterSpacing)
58
+ "hero-display": ["56px", { lineHeight: "1.07", letterSpacing: "-0.28px", fontWeight: "600" }],
59
+ "display-lg": ["40px", { lineHeight: "1.10", letterSpacing: "-0.4px", fontWeight: "600" }],
60
+ "display-md": ["34px", { lineHeight: "1.18", letterSpacing: "-0.374px", fontWeight: "600" }],
61
+ lead: ["28px", { lineHeight: "1.14", letterSpacing: "0.196px", fontWeight: "400" }],
62
+ "lead-airy": ["24px", { lineHeight: "1.5", letterSpacing: "0", fontWeight: "300" }],
63
+ tagline: ["21px", { lineHeight: "1.19", letterSpacing: "0.231px", fontWeight: "600" }],
64
+ "body-strong": ["17px", { lineHeight: "1.24", letterSpacing: "-0.374px", fontWeight: "600" }],
65
+ body: ["17px", { lineHeight: "1.47", letterSpacing: "-0.374px", fontWeight: "400" }],
66
+ "dense-link": ["17px", { lineHeight: "2.41", letterSpacing: "0", fontWeight: "400" }],
67
+ caption: ["14px", { lineHeight: "1.43", letterSpacing: "-0.224px", fontWeight: "400" }],
68
+ "caption-strong": ["14px", { lineHeight: "1.29", letterSpacing: "-0.224px", fontWeight: "600" }],
69
+ "button-large": ["18px", { lineHeight: "1.0", letterSpacing: "0", fontWeight: "300" }],
70
+ "button-utility": ["14px", { lineHeight: "1.29", letterSpacing: "-0.224px", fontWeight: "400" }],
71
+ "fine-print": ["12px", { lineHeight: "1.0", letterSpacing: "-0.12px", fontWeight: "400" }],
72
+ "micro-legal": ["10px", { lineHeight: "1.3", letterSpacing: "-0.08px", fontWeight: "400" }],
73
+ "nav-link": ["12px", { lineHeight: "1.0", letterSpacing: "-0.12px", fontWeight: "400" }],
74
+ },
75
+ spacing: {
76
+ section: "80px",
77
+ },
78
+ borderRadius: {
79
+ none: "0",
80
+ xs: "5px",
81
+ sm: "8px",
82
+ md: "11px",
83
+ lg: "18px",
84
+ pill: "9999px",
85
+ },
86
+ boxShadow: {
87
+ // The single sanctioned drop-shadow — for the answer card (the "product")
88
+ product: "rgba(0, 0, 0, 0.22) 3px 5px 30px 0",
89
+ // Subtle ring for focus + cards (functions as hairline, not chrome)
90
+ hairline: "0 0 0 1px rgba(0, 0, 0, 0.06)",
91
+ },
92
+ transitionTimingFunction: {
93
+ // Apple's signature easing (used in app launch animations)
94
+ apple: "cubic-bezier(0.32, 0.72, 0, 1)",
95
+ },
96
+ animation: {
97
+ "fade-in": "fadeIn 0.4s cubic-bezier(0.32, 0.72, 0, 1)",
98
+ "slide-up": "slideUp 0.5s cubic-bezier(0.32, 0.72, 0, 1)",
99
+ "stagger-1": "slideUp 0.5s cubic-bezier(0.32, 0.72, 0, 1) 0.1s both",
100
+ "stagger-2": "slideUp 0.5s cubic-bezier(0.32, 0.72, 0, 1) 0.2s both",
101
+ "stagger-3": "slideUp 0.5s cubic-bezier(0.32, 0.72, 0, 1) 0.3s both",
102
+ },
103
+ keyframes: {
104
+ fadeIn: {
105
+ "0%": { opacity: "0" },
106
+ "100%": { opacity: "1" },
107
+ },
108
+ slideUp: {
109
+ "0%": { opacity: "0", transform: "translateY(12px)" },
110
+ "100%": { opacity: "1", transform: "translateY(0)" },
111
+ },
112
+ },
113
+ backdropBlur: {
114
+ nav: "20px",
115
+ },
116
+ },
117
+ },
118
+ plugins: [],
119
+ };
120
+ export default config;
tsconfig.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": false,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "preserve",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }