codelion commited on
Commit
09e451a
·
verified ·
1 Parent(s): b243c0b

DataInventor: open-source Invent a Dataset recreation

Browse files
Files changed (9) hide show
  1. Dockerfile +12 -2
  2. README.md +30 -5
  3. app/engine.py +328 -0
  4. app/main.py +168 -0
  5. requirements.txt +3 -0
  6. static/app.css +190 -0
  7. static/app.js +255 -0
  8. static/domains.json +716 -0
  9. static/index.html +35 -0
Dockerfile CHANGED
@@ -1,2 +1,12 @@
1
- FROM huggingface/autotrain-advanced:latest
2
- CMD autotrain api --port 7860 --host 0.0.0.0
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+ ENV PYTHONUNBUFFERED=1 PORT=7860
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+ WORKDIR /home/user/app
7
+ COPY --chown=user requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+ COPY --chown=user app ./app
10
+ COPY --chown=user static ./static
11
+ WORKDIR /home/user/app/app
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,9 +1,34 @@
1
  ---
2
- title: photo-lora
3
- emoji: 🚀
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: docker
 
7
  pinned: false
8
- duplicated_from: autotrain-projects/autotrain-advanced
9
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: DataInventor
3
+ emoji: 🧪
4
+ colorFrom: indigo
5
+ colorTo: pink
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ short_description: Describe a dataset in plain words, get training rows back.
10
  ---
11
+
12
+ # DataInventor
13
+
14
+ An open-source recreation of the [Invent a Dataset](https://adaptionlabs.ai/invent-a-dataset) workflow. Describe the data you want, choose a format, domains and languages, preview a blueprint, and get an instruction or preference dataset back as CSV or JSONL.
15
+
16
+ It works with any OpenAI-compatible endpoint. The default is a free model served through Hugging Face Inference Providers.
17
+
18
+ ## How it writes rows
19
+
20
+ 1. **Blueprint.** The model turns your description into topics, personas, who answers and the shapes questions take.
21
+ 2. **Sampling.** Unique combinations of topic, persona, shape, difficulty, length and language.
22
+ 3. **Writing.** One message per combination, then the reply (preference pairs get two replies and a ranking).
23
+ 4. **Filters.** Cut-off or degenerate text, requests for many items at once, and near-duplicate questions are rejected.
24
+
25
+ ## Run it yourself
26
+
27
+ ```bash
28
+ pip install -r requirements.txt
29
+ cd app && HF_TOKEN=hf_... uvicorn main:app --port 7860
30
+ ```
31
+
32
+ Settings: `MAX_ROWS` (default 1000), `MAX_RUNNING` (default 2), `HF_TOKEN` (used only for the Hugging Face router).
33
+
34
+ Not affiliated with Adaption Labs.
app/engine.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DataInventor engine: one sentence in, a training dataset out, on any OpenAI-compatible endpoint.
2
+
3
+ Pipeline: blueprint (taxonomy + personas + question shapes) -> sampled combinations ->
4
+ question -> answer (or two answers + a ranking for preference pairs) -> filters -> near-duplicate rejection.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import random
10
+ import re
11
+ import threading
12
+ import time
13
+ import urllib.request
14
+ import urllib.error
15
+ from concurrent.futures import ThreadPoolExecutor
16
+ from dataclasses import dataclass, field
17
+ from typing import Callable, Optional
18
+
19
+ HF_BASE = "https://router.huggingface.co/v1"
20
+ HF_MODEL = "prism-ml/Ternary-Bonsai-27B-AWQ-4bit:together"
21
+
22
+
23
+ @dataclass
24
+ class Endpoint:
25
+ base_url: str = HF_BASE
26
+ api_key: str = ""
27
+ model: str = HF_MODEL
28
+ disable_thinking: bool = True
29
+ workers: int = 4
30
+
31
+ def chat(self, prompt: str, max_tokens: int = 1500, temperature: float = 0.8, retries: int = 5,
32
+ validate: Optional[Callable[[str], bool]] = None) -> str:
33
+ body = {"model": self.model, "max_tokens": max_tokens, "temperature": temperature,
34
+ "messages": [{"role": "user", "content": prompt}]}
35
+ if self.disable_thinking:
36
+ body["chat_template_kwargs"] = {"enable_thinking": False}
37
+ data = json.dumps(body).encode()
38
+ url = self.base_url.rstrip("/") + "/chat/completions"
39
+ last = "no attempt"
40
+ for attempt in range(retries):
41
+ try:
42
+ req = urllib.request.Request(url, data, {"Content-Type": "application/json", "User-Agent": "DataInventor/1.0",
43
+ "Authorization": "Bearer " + self.api_key})
44
+ resp = json.load(urllib.request.urlopen(req, timeout=180))
45
+ choice = (resp.get("choices") or [{}])[0] or {}
46
+ if choice.get("finish_reason") == "length":
47
+ last = "reply cut off at max_tokens"
48
+ time.sleep(1.5)
49
+ continue
50
+ text = ((choice.get("message") or {}).get("content") or "").strip()
51
+ text = re.sub(r"<think>.*?</think>", "", text, flags=re.S).strip()
52
+ if text and (validate is None or validate(text)):
53
+ return text
54
+ last = "reply rejected by a quality check"
55
+ except urllib.error.HTTPError as e:
56
+ detail = ""
57
+ try:
58
+ detail = e.read().decode()[:200]
59
+ except Exception:
60
+ pass
61
+ last = f"HTTP {e.code} {detail}"
62
+ if e.code in (401, 403):
63
+ raise EndpointError(f"The endpoint refused the key (HTTP {e.code}). Check the API key.")
64
+ if e.code == 404:
65
+ raise EndpointError("The endpoint or model was not found (HTTP 404). Check the base URL and model name.")
66
+ time.sleep(min(30, 2 ** attempt * 2) if e.code == 429 else 3 * (attempt + 1))
67
+ except Exception as e: # timeouts, resets
68
+ last = str(e)[:160]
69
+ time.sleep(3 * (attempt + 1))
70
+ raise EndpointError(last)
71
+
72
+ def ping(self) -> dict:
73
+ t = time.time()
74
+ text = self.chat("Reply with the single word ready.", max_tokens=16, temperature=0, retries=2)
75
+ return {"ok": True, "latency_ms": int((time.time() - t) * 1000), "sample": text[:40]}
76
+
77
+
78
+ class EndpointError(Exception):
79
+ pass
80
+
81
+
82
+ # ---------------------------------------------------------------- blueprint
83
+
84
+ def _s(x) -> str:
85
+ """Models sometimes return {"persona": "..."} instead of a bare string."""
86
+ if isinstance(x, dict):
87
+ for k in ("name", "persona", "role", "format", "shape", "topic", "description", "title"):
88
+ if isinstance(x.get(k), str):
89
+ return x[k].strip()
90
+ return " ".join(str(v) for v in x.values() if isinstance(v, str)).strip()
91
+ return str(x).strip()
92
+
93
+
94
+ def normalize_blueprint(bp: dict) -> dict:
95
+ out = {"topics": [], "personas": [_s(p) for p in bp.get("personas", []) if _s(p)],
96
+ "formats": [_s(f) for f in bp.get("formats", []) if _s(f)],
97
+ "difficulty": [_s(d) for d in bp.get("difficulty", []) if _s(d)] or ["basic", "intermediate", "advanced"],
98
+ "assistant_role": _s(bp.get("assistant_role") or "an excellent, accurate AI assistant")[:300]}
99
+ for a in bp.get("topics", []):
100
+ subs = []
101
+ for sub in a.get("subtopics", []):
102
+ leaves = [_s(l) for l in sub.get("leaf_topics", []) if _s(l)]
103
+ if leaves:
104
+ subs.append({"subtopic": _s(sub.get("subtopic", "")), "leaf_topics": leaves})
105
+ if subs:
106
+ out["topics"].append({"area": _s(a.get("area", "")), "subtopics": subs})
107
+ if len(out["topics"]) < 1 or len(out["personas"]) < 2 or len(out["formats"]) < 2:
108
+ raise ValueError("the blueprint is missing topics, personas or formats")
109
+ return out
110
+
111
+
112
+ def _json_from(text: str):
113
+ m = re.search(r"\{.*\}", text, re.S)
114
+ if not m:
115
+ raise ValueError("no JSON object in the reply")
116
+ return json.loads(m.group(0))
117
+
118
+
119
+ def make_blueprint(ep: Endpoint, request: str, domains: list[str], subdomains: list[str], languages: list[str],
120
+ constraints: str, rows: int) -> dict:
121
+ scope = ""
122
+ if domains:
123
+ scope += f"Domains: {', '.join(domains)}.\n"
124
+ if subdomains:
125
+ scope += f"Subdomains: {', '.join(subdomains)}.\n"
126
+ if constraints.strip():
127
+ scope += f"Output requirements: {constraints.strip()}\n"
128
+ areas = 6 if rows <= 500 else 8
129
+ prompt = f"""You are planning a training dataset. Dataset request: "{request}"
130
+ {scope}
131
+ Return ONLY JSON with these keys:
132
+ "topics": a list of {areas} objects {{"area": str, "subtopics": [4 objects {{"subtopic": str, "leaf_topics": [4 specific, concrete strings]}}]}}. Cover the whole request evenly.
133
+ "personas": 10 different kinds of people who would write the user side of this dataset (for a customer-support dataset, 10 different kinds of customer; for a coding dataset, 10 kinds of developer).
134
+ "assistant_role": one sentence describing who answers in this dataset and in what voice (for example "a friendly support agent at a small online bookshop who gives concrete next steps").
135
+ "formats": 6 different shapes the instruction could take (for example a short direct ask, a detailed brief, a request with a concrete example, a request with constraints, a follow-up, a comparison).
136
+ "difficulty": ["basic","intermediate","advanced"].
137
+ No commentary, no code fences."""
138
+ last = None
139
+ for _ in range(4):
140
+ try:
141
+ return normalize_blueprint(_json_from(ep.chat(prompt, max_tokens=5000, temperature=0.7)))
142
+ except EndpointError:
143
+ raise
144
+ except Exception as e:
145
+ last = e
146
+ raise EndpointError(f"The model did not return a usable blueprint: {last}")
147
+
148
+
149
+ # ---------------------------------------------------------------- generation
150
+
151
+ LEAK = re.compile(r"the user (asks|wants)|write one|return only|^okay|^sure,|^here is|^here's|^as an ai", re.I)
152
+ END = re.compile(r"[.?!)\"”’'।:。!?]$")
153
+ MULTI = re.compile(r"\b(generate|draft|write|compile|create|give me|produce)\s+(\d+|several|multiple|a set of|a list of)\b", re.I)
154
+ LENGTHS = ["one blunt sentence", "two sentences with one specific detail",
155
+ "three or four sentences with concrete specifics", "a short paragraph that sounds a little rushed"]
156
+
157
+
158
+ def degenerate(t: str) -> bool:
159
+ w = t.lower().split()
160
+ return bool(re.search(r"(.)\1{7,}", t)) or (len(w) >= 8 and len(set(w)) / len(w) < 0.5)
161
+
162
+
163
+ def grams(t: str, n: int = 3) -> set:
164
+ w = t.lower().split()
165
+ return set(zip(*[w[i:] for i in range(n)])) if len(w) >= n else {tuple(w)}
166
+
167
+
168
+ @dataclass
169
+ class Job:
170
+ id: str
171
+ config: dict
172
+ state: str = "queued" # queued | planning | generating | done | failed | stopped
173
+ message: str = ""
174
+ blueprint: Optional[dict] = None
175
+ rows: list = field(default_factory=list)
176
+ target: int = 0
177
+ attempts: int = 0
178
+ rejected: dict = field(default_factory=dict)
179
+ started: float = field(default_factory=time.time)
180
+ finished: float = 0.0
181
+ stop: threading.Event = field(default_factory=threading.Event)
182
+ lock: threading.Lock = field(default_factory=threading.Lock)
183
+
184
+ def reject(self, why: str):
185
+ with self.lock:
186
+ self.rejected[why] = self.rejected.get(why, 0) + 1
187
+
188
+
189
+ def run_job(job: Job, ep: Endpoint):
190
+ cfg = job.config
191
+ rows_n = int(cfg["rows"])
192
+ job.target = rows_n
193
+ kind = cfg.get("format", "instruction")
194
+ request = cfg["request"]
195
+ languages = cfg.get("languages") or ["English"]
196
+ constraints = cfg.get("constraints", "")
197
+ try:
198
+ job.state, job.message = "planning", "Drafting the blueprint"
199
+ bp = normalize_blueprint(cfg["blueprint"]) if cfg.get("blueprint") else make_blueprint(
200
+ ep, request, cfg.get("domains", []), cfg.get("subdomains", []), languages, constraints, rows_n)
201
+ job.blueprint = bp
202
+ leaves = [(a["area"], s["subtopic"], l) for a in bp["topics"] for s in a["subtopics"] for l in s["leaf_topics"]]
203
+ rng = random.Random(cfg.get("seed", 7))
204
+ combos, seen = [], set()
205
+ want = int(rows_n * 1.8) + 20
206
+ guard = 0
207
+ while len(combos) < want and guard < want * 30:
208
+ guard += 1
209
+ c = (rng.choice(leaves), rng.choice(bp["personas"]), rng.choice(bp["formats"]), rng.choice(bp["difficulty"]),
210
+ rng.choice(LENGTHS), rng.choice(languages))
211
+ if c not in seen:
212
+ seen.add(c)
213
+ combos.append(c)
214
+
215
+ scope = f'The dataset being built: "{request}"'
216
+ if constraints.strip():
217
+ scope += f"\nEvery example must satisfy: {constraints.strip()}"
218
+
219
+ def ok_q(t):
220
+ t = t.strip().strip('"')
221
+ return 15 <= len(t) <= 1500 and END.search(t) and not degenerate(t) and not LEAK.search(t[:150]) and not MULTI.search(t[:120])
222
+
223
+ def ok_a(t):
224
+ return len(t.split()) >= 12 and not degenerate(t[:600]) and not LEAK.search(t[:100])
225
+
226
+ kept_grams: list[set] = []
227
+ job.state, job.message = "generating", "Writing examples"
228
+
229
+ def work(idx_combo):
230
+ idx, ((area, sub, leaf), persona, fmt, diff, length, lang) = idx_combo
231
+ if job.stop.is_set():
232
+ return
233
+ with job.lock:
234
+ if len(job.rows) >= rows_n:
235
+ return
236
+ job.attempts += 1
237
+ try:
238
+ role = bp.get("assistant_role", "an excellent AI assistant")
239
+ q = ep.chat(f"""{scope}
240
+ Write ONE realistic message that {persona} would send to {role}, for this dataset, about: {leaf} ({area}, {sub}).
241
+ Shape: {fmt}. Difficulty: {diff}. Length: {length}. Language: {lang}.
242
+ It should sound like a real person typing, specific and a little imperfect, not a textbook prompt. It must be ONE question or ONE task whose answer fits in a single reply: never ask for a list of many items, a set of Q&A pairs, or a dataset. Do not name the shape or the persona.
243
+ Return only the message.""", max_tokens=700, temperature=1.0, validate=ok_q).strip().strip('"')
244
+ g = grams(q)
245
+ with job.lock:
246
+ if any(len(g & k) / max(1, len(g | k)) > 0.5 for k in kept_grams):
247
+ job.reject("near-duplicate question")
248
+ return
249
+ answer_prompt = f"""You are {role}. Reply to the message below. The reply is a training example for this dataset: "{request}".
250
+ Language: {lang}. Be accurate, specific and complete. Start directly with the content, with no preamble about who you are. Do not invent statistics or facts you cannot know.
251
+ {('Requirements: ' + constraints.strip()) if constraints.strip() else ''}
252
+
253
+ Message: {q}"""
254
+ a1 = ep.chat(answer_prompt, max_tokens=2500, temperature=0.7, validate=ok_a)
255
+ row = {"area": area, "subtopic": sub, "topic": leaf, "persona": persona, "shape": fmt,
256
+ "difficulty": diff, "language": lang, "prompt": q}
257
+ if kind == "preference":
258
+ a2 = ep.chat(answer_prompt + "\n\nWrite a different answer from the obvious one, in a different structure and with different emphasis.",
259
+ max_tokens=2500, temperature=1.0, validate=ok_a)
260
+ verdict = ep.chat(f"""Which answer is better for the message? Judge accuracy, completeness and clarity.
261
+ Message: {q}
262
+
263
+ Answer A:
264
+ {a1[:3000]}
265
+
266
+ Answer B:
267
+ {a2[:3000]}
268
+
269
+ Reply with only the single letter A or B.""", max_tokens=8, temperature=0, validate=lambda s: s.strip()[:1].upper() in ("A", "B"))
270
+ a_wins = verdict.strip()[:1].upper() == "A"
271
+ row["chosen"], row["rejected"] = (a1, a2) if a_wins else (a2, a1)
272
+ else:
273
+ row["completion"] = a1
274
+ with job.lock:
275
+ if len(job.rows) >= rows_n:
276
+ return
277
+ if any(len(g & k) / max(1, len(g | k)) > 0.5 for k in kept_grams):
278
+ job.reject("near-duplicate question")
279
+ return
280
+ row["id"] = len(job.rows) + 1
281
+ job.rows.append(row)
282
+ kept_grams.append(g)
283
+ job.message = f"{len(job.rows)} of {rows_n} written"
284
+ if len(job.rows) >= rows_n:
285
+ job.state, job.message, job.finished = "done", f"{len(job.rows)} rows ready", time.time()
286
+ job.stop.set()
287
+ except EndpointError as e:
288
+ if "refused the key" in str(e) or "not found" in str(e):
289
+ job.state, job.message = "failed", str(e)
290
+ job.stop.set()
291
+ else:
292
+ job.reject("model call failed")
293
+ except Exception:
294
+ job.reject("model call failed")
295
+
296
+ with ThreadPoolExecutor(max_workers=max(1, min(ep.workers, 16))) as pool:
297
+ list(pool.map(work, enumerate(combos)))
298
+ if job.state in ("failed", "done"):
299
+ return
300
+ if job.stop.is_set():
301
+ job.state, job.message = "stopped", f"Stopped with {len(job.rows)} rows"
302
+ elif len(job.rows) < rows_n:
303
+ job.state = "done"
304
+ job.message = f"Finished with {len(job.rows)} of {rows_n} rows. The model rejected too many attempts to fill the rest."
305
+ else:
306
+ job.state, job.message = "done", f"{len(job.rows)} rows ready"
307
+ except EndpointError as e:
308
+ job.state, job.message = "failed", str(e)
309
+ except Exception as e: # never leave a job hanging
310
+ job.state, job.message = "failed", f"Unexpected error: {e}"
311
+ finally:
312
+ job.finished = time.time()
313
+
314
+
315
+ # ---------------------------------------------------------------- stats
316
+
317
+ def stats(rows: list) -> dict:
318
+ if not rows:
319
+ return {}
320
+ text = "\n".join(r["prompt"] for r in rows).lower().split()
321
+ tri = list(zip(text, text[1:], text[2:]))
322
+ area_counts: dict = {}
323
+ for r in rows:
324
+ area_counts[r["area"]] = area_counts.get(r["area"], 0) + 1
325
+ comp = [len((r.get("completion") or r.get("chosen") or "").split()) for r in rows]
326
+ return {"rows": len(rows), "distinct3_prompts": round(len(set(tri)) / max(1, len(tri)), 3),
327
+ "avg_prompt_words": round(sum(len(r["prompt"].split()) for r in rows) / len(rows), 1),
328
+ "avg_answer_words": round(sum(comp) / len(rows), 1), "areas": area_counts}
app/main.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DataInventor: an open-source recreation of the Invent a Dataset workflow. Bring any OpenAI-compatible endpoint."""
2
+ from __future__ import annotations
3
+
4
+ import csv
5
+ import io
6
+ import json
7
+ import os
8
+ import threading
9
+ import time
10
+ import uuid
11
+ from pathlib import Path
12
+
13
+ from fastapi import FastAPI, HTTPException
14
+ from fastapi.responses import FileResponse, JSONResponse, Response
15
+ from fastapi.staticfiles import StaticFiles
16
+ from pydantic import BaseModel, Field
17
+
18
+ from engine import HF_BASE, HF_MODEL, Endpoint, EndpointError, Job, make_blueprint, run_job, stats
19
+
20
+ ROOT = Path(__file__).resolve().parent.parent
21
+ STATIC = ROOT / "static"
22
+ MAX_ROWS = int(os.environ.get("MAX_ROWS", "1000"))
23
+ MAX_RUNNING = int(os.environ.get("MAX_RUNNING", "2"))
24
+ SERVER_KEY = os.environ.get("HF_TOKEN", "")
25
+
26
+ app = FastAPI(title="DataInventor")
27
+ JOBS: dict[str, Job] = {}
28
+ ENDPOINTS: dict[str, Endpoint] = {} # kept apart from Job so a key never lands in a response
29
+ GUARD = threading.Lock()
30
+
31
+
32
+ class EngineIn(BaseModel):
33
+ base_url: str = HF_BASE
34
+ api_key: str = ""
35
+ model: str = HF_MODEL
36
+ disable_thinking: bool = True
37
+ workers: int = 4
38
+
39
+
40
+ class SpecIn(BaseModel):
41
+ engine: EngineIn = Field(default_factory=EngineIn)
42
+ format: str = "instruction"
43
+ request: str
44
+ domains: list[str] = []
45
+ subdomains: list[str] = []
46
+ languages: list[str] = ["English"]
47
+ constraints: str = ""
48
+ rows: int = 100
49
+ blueprint: dict | None = None
50
+
51
+
52
+ def make_endpoint(e: EngineIn) -> Endpoint:
53
+ base = e.base_url.strip().rstrip("/")
54
+ if not base.startswith(("http://", "https://")):
55
+ raise HTTPException(400, "The base URL must start with http:// or https://.")
56
+ key = e.api_key.strip()
57
+ if not key:
58
+ if base == HF_BASE and SERVER_KEY:
59
+ key = SERVER_KEY # the hosted default only ever talks to the Hugging Face router
60
+ else:
61
+ raise HTTPException(400, "Add an API key for this endpoint.")
62
+ return Endpoint(base_url=base, api_key=key, model=e.model.strip() or HF_MODEL,
63
+ disable_thinking=e.disable_thinking, workers=max(1, min(e.workers, 8)))
64
+
65
+
66
+ @app.get("/api/defaults")
67
+ def defaults():
68
+ return {"base_url": HF_BASE, "model": HF_MODEL, "hosted_key": bool(SERVER_KEY), "max_rows": MAX_ROWS}
69
+
70
+
71
+ @app.get("/api/domains")
72
+ def domains():
73
+ return JSONResponse(json.loads((STATIC / "domains.json").read_text()))
74
+
75
+
76
+ @app.post("/api/ping")
77
+ def ping(e: EngineIn):
78
+ try:
79
+ return make_endpoint(e).ping()
80
+ except EndpointError as err:
81
+ raise HTTPException(502, str(err))
82
+
83
+
84
+ @app.post("/api/blueprint")
85
+ def blueprint(s: SpecIn):
86
+ ep = make_endpoint(s.engine)
87
+ if not s.request.strip():
88
+ raise HTTPException(400, "Describe the dataset first.")
89
+ try:
90
+ return make_blueprint(ep, s.request, s.domains, s.subdomains, s.languages, s.constraints, s.rows)
91
+ except EndpointError as err:
92
+ raise HTTPException(502, str(err))
93
+
94
+
95
+ @app.post("/api/jobs")
96
+ def start(s: SpecIn):
97
+ ep = make_endpoint(s.engine)
98
+ if not s.request.strip():
99
+ raise HTTPException(400, "Describe the dataset first.")
100
+ if not 1 <= s.rows <= MAX_ROWS:
101
+ raise HTTPException(400, f"Rows must be between 1 and {MAX_ROWS} on this host.")
102
+ with GUARD:
103
+ running = sum(1 for j in JOBS.values() if j.state in ("queued", "planning", "generating"))
104
+ if running >= MAX_RUNNING:
105
+ raise HTTPException(429, "Two datasets are already being written. Wait for one to finish.")
106
+ jid = uuid.uuid4().hex[:10]
107
+ cfg = s.model_dump(exclude={"engine"})
108
+ job = Job(id=jid, config=cfg)
109
+ JOBS[jid] = job
110
+ ENDPOINTS[jid] = ep
111
+ for old in sorted(JOBS, key=lambda k: JOBS[k].started)[:-20]:
112
+ JOBS.pop(old, None); ENDPOINTS.pop(old, None)
113
+ threading.Thread(target=run_job, args=(job, ep), daemon=True).start()
114
+ return {"id": jid}
115
+
116
+
117
+ def get_job(jid: str) -> Job:
118
+ if jid not in JOBS:
119
+ raise HTTPException(404, "That dataset is no longer here. Start a new run.")
120
+ return JOBS[jid]
121
+
122
+
123
+ @app.get("/api/jobs/{jid}")
124
+ def status(jid: str, since: int = 0):
125
+ j = get_job(jid)
126
+ with j.lock:
127
+ rows = list(j.rows)
128
+ out = {"id": j.id, "state": j.state, "message": j.message, "target": j.target, "count": len(rows), "attempts": j.attempts,
129
+ "rejected": dict(j.rejected), "elapsed": round((j.finished or time.time()) - j.started, 1), "new_rows": rows[since:since + 50]}
130
+ if j.state in ("done", "stopped", "failed"):
131
+ out["stats"] = stats(rows)
132
+ out["blueprint"] = j.blueprint
133
+ return out
134
+
135
+
136
+ @app.post("/api/jobs/{jid}/stop")
137
+ def stop(jid: str):
138
+ get_job(jid).stop.set()
139
+ return {"ok": True}
140
+
141
+
142
+ @app.get("/api/jobs/{jid}/download")
143
+ def download(jid: str, format: str = "csv"):
144
+ j = get_job(jid)
145
+ rows = list(j.rows)
146
+ if not rows:
147
+ raise HTTPException(404, "There are no rows to download yet.")
148
+ pref = "chosen" in rows[0]
149
+ cols = ["prompt", "chosen", "rejected"] if pref else ["prompt", "completion"]
150
+ meta = ["area", "subtopic", "topic", "persona", "shape", "difficulty", "language"]
151
+ name = f"datainventor-{jid}"
152
+ if format == "jsonl":
153
+ body = "\n".join(json.dumps({k: r.get(k) for k in cols + meta}, ensure_ascii=False) for r in rows)
154
+ return Response(body, media_type="application/jsonl", headers={"Content-Disposition": f'attachment; filename="{name}.jsonl"'})
155
+ buf = io.StringIO()
156
+ w = csv.writer(buf)
157
+ w.writerow(cols + meta)
158
+ for r in rows:
159
+ w.writerow([r.get(k, "") for k in cols + meta])
160
+ return Response(buf.getvalue().encode("utf-8-sig"), media_type="text/csv", headers={"Content-Disposition": f'attachment; filename="{name}.csv"'})
161
+
162
+
163
+ @app.get("/")
164
+ def index():
165
+ return FileResponse(STATIC / "index.html")
166
+
167
+
168
+ app.mount("/static", StaticFiles(directory=STATIC), name="static")
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi==0.115.*
2
+ uvicorn[standard]==0.32.*
3
+ pydantic>=2.7
static/app.css ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --paper: #e7e8ea;
3
+ --paper-2: #f1f2f4;
4
+ --ink: #1d2036;
5
+ --blue: #3a4bff;
6
+ --pink: #ff5c8f;
7
+ --slate: #5d6376;
8
+ --rule: #c5c8d1;
9
+ --ok: #1f7a55;
10
+ --bad: #b3261e;
11
+ --ui: "Familjen Grotesk", system-ui, sans-serif;
12
+ --doc: "Literata", Georgia, serif;
13
+ --r: 3px;
14
+ }
15
+ * { box-sizing: border-box; }
16
+ html { color-scheme: light; }
17
+ body {
18
+ margin: 0; background: var(--paper); color: var(--ink);
19
+ font: 400 16px/1.5 var(--ui);
20
+ -webkit-font-smoothing: antialiased;
21
+ }
22
+ .skip { position: absolute; left: -999px; top: 0; background: var(--ink); color: #fff; padding: 8px 12px; z-index: 10; }
23
+ .skip:focus { left: 8px; top: 8px; }
24
+ :focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; border-radius: var(--r); }
25
+ button, input, textarea, select { font: inherit; color: inherit; }
26
+ h1, h2, h3, p { margin: 0; }
27
+
28
+ /* top bar */
29
+ .top { display: flex; align-items: center; gap: 28px; padding: 22px 36px 18px; border-bottom: 1px solid var(--rule); }
30
+ .brand { display: flex; align-items: center; gap: 12px; flex: none; }
31
+ .mark { position: relative; width: 30px; height: 30px; display: inline-block; }
32
+ .mark i { position: absolute; inset: 0; border-radius: 50%; mix-blend-mode: multiply; }
33
+ .mark i:first-child { background: var(--blue); transform: translate(-3px, -2px); }
34
+ .mark i:last-child { background: var(--pink); transform: translate(4px, 3px); }
35
+ .name { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; }
36
+ .tag { color: var(--slate); max-width: 62ch; font-size: 15px; }
37
+
38
+ /* layout */
39
+ .shell { display: grid; grid-template-columns: 210px minmax(0, 1fr) 330px; gap: 0; min-height: calc(100vh - 140px); }
40
+ .ledger { border-right: 1px solid var(--rule); padding: 26px 0 26px 36px; }
41
+ .panel { padding: 30px 44px 60px; min-width: 0; outline: none; }
42
+ .sheet { border-left: 1px solid var(--rule); padding: 30px 28px 40px; position: sticky; top: 0; align-self: start; max-height: 100vh; overflow: auto; }
43
+
44
+ /* ledger */
45
+ #steps { list-style: none; margin: 0; padding: 0; }
46
+ #steps li { position: relative; }
47
+ #steps button {
48
+ display: grid; grid-template-columns: 26px 1fr; align-items: baseline; gap: 6px; width: 100%;
49
+ text-align: left; background: none; border: 0; padding: 9px 12px 9px 0; cursor: pointer; color: var(--slate);
50
+ }
51
+ #steps .n { font-variant-numeric: tabular-nums; font-weight: 600; }
52
+ #steps li.done button { color: var(--ink); }
53
+ #steps li.done .n { color: var(--blue); }
54
+ #steps li.on button { color: var(--ink); font-weight: 600; }
55
+ #steps li.on::before { content: ""; position: absolute; left: -36px; top: 8px; bottom: 8px; width: 4px; background: var(--blue); }
56
+ #steps button:disabled { cursor: default; opacity: .55; }
57
+
58
+ /* type */
59
+ .panel h1 { font-size: clamp(30px, 4.2vw, 46px); line-height: 1.06; letter-spacing: -0.03em; font-weight: 700; max-width: 18ch; }
60
+ .panel h2 { font-size: 26px; line-height: 1.15; letter-spacing: -0.02em; font-weight: 700; max-width: 26ch; }
61
+ .lede { color: var(--slate); margin-top: 12px; max-width: 58ch; }
62
+ .group { margin-top: 30px; }
63
+ .group > h3 { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
64
+ .hint { color: var(--slate); font-size: 14px; }
65
+
66
+ /* format specimens */
67
+ .specimens { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; margin-top: 30px; }
68
+ .spec {
69
+ position: relative; text-align: left; background: var(--paper-2); border: 1px solid var(--rule);
70
+ border-radius: var(--r); padding: 20px 20px 22px; cursor: pointer;
71
+ }
72
+ .spec[aria-pressed="true"] { border-color: var(--ink); box-shadow: 5px 5px 0 -1px var(--blue), -3px -3px 0 -1px var(--pink); }
73
+ .spec b { display: block; font-size: 18px; margin-bottom: 4px; }
74
+ .spec .eg { font-family: var(--doc); font-size: 14.5px; line-height: 1.5; margin-top: 14px; color: #2b2f48; }
75
+ .spec .eg u { text-decoration: none; display: block; font-style: italic; color: var(--slate); margin-bottom: 2px; }
76
+ .spec .eg p + p { margin-top: 8px; }
77
+
78
+ /* choice lists */
79
+ .choices { list-style: none; margin: 14px 0 0; padding: 0; columns: 3 190px; column-gap: 28px; }
80
+ .choices li { break-inside: avoid; }
81
+ .choice {
82
+ display: grid; grid-template-columns: 18px 1fr; gap: 10px; align-items: start; width: 100%; text-align: left;
83
+ background: none; border: 0; padding: 7px 0; cursor: pointer;
84
+ }
85
+ .box { width: 16px; height: 16px; margin-top: 3px; border: 1.5px solid var(--ink); border-radius: 2px; background: var(--paper-2); display: grid; place-items: center; }
86
+ .choice[aria-pressed="true"] .box { background: var(--blue); border-color: var(--blue); }
87
+ .choice[aria-pressed="true"] .box::after { content: ""; width: 8px; height: 4px; border-left: 2px solid #fff; border-bottom: 2px solid #fff; transform: rotate(-45deg) translate(1px, -1px); }
88
+ .choice[aria-pressed="true"] span:last-child { font-weight: 600; }
89
+ .sub-block { margin-top: 26px; padding-top: 18px; border-top: 1px solid var(--rule); }
90
+ .sub-block h3 { font-size: 17px; }
91
+
92
+ /* fields */
93
+ label.f { display: block; margin-top: 22px; }
94
+ label.f > span { display: block; font-weight: 600; font-size: 15px; margin-bottom: 6px; }
95
+ .in, textarea.in, select.in {
96
+ width: 100%; background: var(--paper-2); border: 1px solid #aeb2bf; border-radius: var(--r); padding: 11px 13px;
97
+ }
98
+ textarea.in { min-height: 118px; resize: vertical; font-family: var(--doc); font-size: 16.5px; line-height: 1.55; }
99
+ .in:hover { border-color: var(--ink); }
100
+ .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
101
+ .seg { display: inline-flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
102
+ .seg button { background: var(--paper-2); border: 1px solid #aeb2bf; border-radius: var(--r); padding: 8px 15px; cursor: pointer; font-variant-numeric: tabular-nums; }
103
+ .seg button[aria-pressed="true"] { background: var(--ink); color: #fff; border-color: var(--ink); }
104
+ .check { display: flex; gap: 10px; align-items: flex-start; margin-top: 18px; }
105
+ .check input { margin-top: 5px; accent-color: var(--blue); width: 16px; height: 16px; }
106
+
107
+ /* buttons */
108
+ .actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin-top: 36px; }
109
+ .btn { border: 1.5px solid var(--ink); background: var(--paper-2); border-radius: var(--r); padding: 11px 20px; font-weight: 600; cursor: pointer; }
110
+ .btn:hover:not(:disabled) { background: #fff; }
111
+ .btn.go { background: var(--blue); border-color: var(--blue); color: #fff; }
112
+ .btn.go:hover:not(:disabled) { background: #2a39e6; }
113
+ .btn:disabled { opacity: .45; cursor: not-allowed; }
114
+ .linkish { background: none; border: 0; padding: 0; color: var(--blue); font-weight: 600; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
115
+
116
+ /* blueprint */
117
+ .bp { margin-top: 22px; border-top: 1px solid var(--ink); }
118
+ .bp-row { display: grid; grid-template-columns: 150px 1fr; gap: 18px; padding: 14px 0; border-bottom: 1px solid var(--rule); }
119
+ .bp-row > b { font-size: 14px; }
120
+ .bp ul { margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 6px 8px; }
121
+ .bp li { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 2px; padding: 3px 9px; font-size: 14px; }
122
+ .bp .tree { display: grid; gap: 8px; }
123
+ .bp .tree div > strong { font-weight: 600; }
124
+ .bp .tree small { color: var(--slate); }
125
+ .note { margin-top: 14px; padding: 10px 14px; border-left: 4px solid var(--pink); background: var(--paper-2); font-size: 14.5px; }
126
+ .note.bad { border-left-color: var(--bad); }
127
+ .note.ok { border-left-color: var(--ok); }
128
+
129
+ /* spec sheet */
130
+ .sheet h2 { font-size: 18px; margin-bottom: 14px; }
131
+ .sheet dl { margin: 0; }
132
+ .sheet .line { display: grid; grid-template-columns: 96px 1fr; gap: 10px; padding: 9px 0; border-top: 1px solid var(--rule); font-size: 14.5px; }
133
+ .sheet dt { color: var(--slate); }
134
+ .sheet dd { margin: 0; overflow-wrap: anywhere; }
135
+ .sheet .est { margin-top: 18px; font-size: 14.5px; color: var(--slate); }
136
+ .sheet .est b { color: var(--ink); }
137
+
138
+ /* run: the printed deck */
139
+ .count { display: flex; align-items: baseline; gap: 10px; margin-top: 24px; }
140
+ .count .big { font-size: clamp(54px, 9vw, 92px); line-height: 1; font-weight: 700; letter-spacing: -0.04em; font-variant-numeric: tabular-nums; }
141
+ .count .of { font-size: 22px; color: var(--slate); }
142
+ .bar { height: 6px; background: var(--rule); margin-top: 14px; border-radius: 3px; overflow: hidden; }
143
+ .bar > i { display: block; height: 100%; width: 0; background: var(--ink); transition: width .5s ease; }
144
+ .state { margin-top: 10px; color: var(--slate); }
145
+ .press { position: relative; height: 230px; margin: 30px 0 8px; }
146
+ .card {
147
+ position: absolute; left: 0; top: 0; width: min(100%, 640px); padding: 18px 22px 20px; background: #fbfbfc; border: 1px solid #aeb2bf; border-radius: 2px;
148
+ font-family: var(--doc); font-size: 15px; line-height: 1.5; overflow: hidden; height: 190px;
149
+ }
150
+ .card::before, .card::after { content: ""; position: absolute; inset: -1px; border-radius: 2px; pointer-events: none; mix-blend-mode: multiply; }
151
+ .card::before { box-shadow: 4px 3px 0 0 var(--pink); z-index: -1; }
152
+ .card::after { box-shadow: -3px -2px 0 0 var(--blue); z-index: -1; }
153
+ .card em { display: block; font-style: italic; color: var(--slate); font-size: 13px; margin-bottom: 3px; font-family: var(--ui); }
154
+ .card .a { margin-top: 10px; color: #3a3f5a; }
155
+ .card .clip { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
156
+ .card.print { animation: print .45s cubic-bezier(.2,.8,.2,1) both; }
157
+ @keyframes print { from { transform: translate(14px, -10px); opacity: 0; } to { opacity: 1; } }
158
+ @media (prefers-reduced-motion: reduce) { .card.print { animation: none; } .bar > i { transition: none; } }
159
+
160
+ .table { margin-top: 28px; border-top: 1px solid var(--ink); }
161
+ .trow { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--rule); }
162
+ .trow .id { color: var(--slate); font-variant-numeric: tabular-nums; }
163
+ .trow .p { font-family: var(--doc); }
164
+ .trow .meta { color: var(--slate); font-size: 13px; margin-top: 2px; }
165
+ .trow details { margin-top: 6px; }
166
+ .trow summary { cursor: pointer; color: var(--blue); font-weight: 600; font-size: 14px; }
167
+ .trow .full { font-family: var(--doc); white-space: pre-wrap; margin-top: 8px; font-size: 15px; line-height: 1.55; }
168
+ .stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; margin-top: 26px; border-top: 1px solid var(--ink); }
169
+ .stats div { padding: 14px 14px 14px 0; border-bottom: 1px solid var(--rule); }
170
+ .stats b { display: block; font-size: 24px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
171
+ .stats span { color: var(--slate); font-size: 13.5px; }
172
+
173
+ .foot { display: flex; justify-content: space-between; gap: 20px; padding: 18px 36px 26px; border-top: 1px solid var(--rule); color: var(--slate); font-size: 13.5px; flex-wrap: wrap; }
174
+
175
+ @media (max-width: 1100px) {
176
+ .shell { grid-template-columns: 1fr; }
177
+ .ledger { border: 0; border-bottom: 1px solid var(--rule); padding: 10px 20px; overflow-x: auto; }
178
+ #steps { display: flex; gap: 4px; }
179
+ #steps button { grid-template-columns: auto auto; white-space: nowrap; padding: 8px 12px; }
180
+ #steps li.on::before { left: 0; right: 0; top: auto; bottom: 0; height: 3px; width: auto; }
181
+ .sheet { position: static; border: 0; border-top: 1px solid var(--rule); max-height: none; }
182
+ .panel { padding: 24px 20px 40px; }
183
+ .top { padding: 18px 20px 14px; flex-wrap: wrap; gap: 8px 18px; }
184
+ }
185
+ @media (max-width: 640px) {
186
+ .specimens, .row2 { grid-template-columns: 1fr; }
187
+ .bp-row { grid-template-columns: 1fr; gap: 6px; }
188
+ .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
189
+ .press { height: 250px; }
190
+ }
static/app.js ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ const STEPS = ["Format", "Domains", "Subdomains", "Languages", "Summary", "Engine", "Run"];
3
+ const LANGS = ["English", "Spanish", "French", "German", "Portuguese", "Italian", "Dutch", "Hindi", "Bengali", "Arabic", "Turkish", "Russian", "Polish", "Ukrainian",
4
+ "Chinese", "Japanese", "Korean", "Vietnamese", "Indonesian", "Thai", "Swahili", "Serbian", "Greek", "Hebrew"];
5
+ const PRESETS = {
6
+ hf: { label: "Hugging Face, free", base_url: "https://router.huggingface.co/v1", model: "prism-ml/Ternary-Bonsai-27B-AWQ-4bit:together", disable_thinking: true },
7
+ openai: { label: "OpenAI", base_url: "https://api.openai.com/v1", model: "gpt-4o-mini", disable_thinking: false },
8
+ local: { label: "Local server", base_url: "http://localhost:11434/v1", model: "llama3.1", disable_thinking: false },
9
+ };
10
+ const FORMATS = {
11
+ instruction: { name: "Instruction pairs", blurb: "A prompt and the reply you want a model to learn. Use it for supervised fine-tuning.",
12
+ eg: [["Prompt", "My hardcover arrived with a bent corner. Can I swap it?"], ["Completion", "Sorry about that. Yes, send a photo of the corner and we will ship a replacement today, no need to return the damaged copy."]] },
13
+ preference: { name: "Preference pairs", blurb: "A prompt with a chosen and a rejected reply, ranked by the model. Use it for preference tuning.",
14
+ eg: [["Prompt", "Where is order 8824? It was due Monday."], ["Chosen", "It left our warehouse Saturday and is with the courier. Tracking shows delivery Thursday."], ["Rejected", "Orders usually take a while. Please be patient."]] },
15
+ };
16
+
17
+ const S = {
18
+ step: 0, maxStep: 0, format: "instruction", domains: [], subs: {}, languages: ["English"], request: "", constraints: "", rows: 100,
19
+ blueprint: null, bpBusy: false, bpError: "", engine: { base_url: PRESETS.hf.base_url, model: PRESETS.hf.model, api_key: "", disable_thinking: true, workers: 4 },
20
+ preset: "hf", ping: null, pinging: false, job: null, jobRows: [], jobState: null, drawn: 0, printed: 0, hosted: false, maxRows: 1000, domainData: [], runError: "",
21
+ };
22
+ const $ = (s, el = document) => el.querySelector(s);
23
+ const esc = (t) => String(t ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
24
+ const sentence = (t) => (t ? t.charAt(0).toUpperCase() + t.slice(1) : t);
25
+
26
+ async function api(path, body, method) {
27
+ const r = await fetch(path, { method: method || (body ? "POST" : "GET"), headers: body ? { "Content-Type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined });
28
+ let data = null; try { data = await r.json(); } catch (e) { /* not json */ }
29
+ if (!r.ok) throw new Error((data && (data.detail || data.message)) || `Request failed (${r.status})`);
30
+ return data;
31
+ }
32
+ const spec = () => ({ engine: S.engine, format: S.format, request: S.request.trim(), domains: S.domains, subdomains: Object.values(S.subs).flat(),
33
+ languages: S.languages, constraints: S.constraints, rows: S.rows, blueprint: S.blueprint });
34
+
35
+ /* ------------------------------------------------------------ chrome */
36
+ function renderLedger() {
37
+ $("#steps").innerHTML = STEPS.map((n, i) => {
38
+ const cls = i === S.step ? "on" : i < S.step ? "done" : "";
39
+ return `<li class="${cls}"><button data-go="${i}" ${i > S.maxStep ? "disabled" : ""} ${i === S.step ? 'aria-current="step"' : ""}><span class="n">${i + 1}</span><span>${n}</span></button></li>`;
40
+ }).join("");
41
+ }
42
+ function estimate() {
43
+ const per = S.format === "preference" ? 4.4 : 2.5, lat = (S.ping && S.ping.latency_ms) || 1800;
44
+ const calls = Math.round(S.rows * per + 2);
45
+ const secs = Math.max(10, Math.round((calls * lat) / 1000 / Math.max(1, S.engine.workers)));
46
+ const t = secs >= 3600 ? `${(secs / 3600).toFixed(1)} hours` : secs >= 90 ? `${Math.round(secs / 60)} minutes` : `${secs} seconds`;
47
+ return { calls, time: t };
48
+ }
49
+ function renderSheet() {
50
+ const e = estimate(), subs = Object.values(S.subs).flat();
51
+ const line = (k, v) => `<div class="line"><dt>${k}</dt><dd>${v}</dd></div>`;
52
+ $("#sheet").innerHTML = `<h2>Your dataset</h2><dl>
53
+ ${line("Format", esc(FORMATS[S.format].name))}
54
+ ${line("Domains", S.domains.length ? esc(S.domains.join(", ")) : '<span class="hint">None chosen. The description will guide it.</span>')}
55
+ ${line("Subdomains", subs.length ? esc(subs.slice(0, 6).join(", ")) + (subs.length > 6 ? ` and ${subs.length - 6} more` : "") : '<span class="hint">All</span>')}
56
+ ${line("Languages", esc(S.languages.join(", ")))}
57
+ ${line("Rows", S.rows.toLocaleString())}
58
+ ${line("Model", esc(S.engine.model))}
59
+ </dl><p class="est">About <b>${e.calls.toLocaleString()}</b> model calls and <b>${e.time}</b> on this setup.</p>`;
60
+ }
61
+ function go(i) { S.step = i; S.maxStep = Math.max(S.maxStep, i); render(); $("#panel").focus({ preventScroll: true }); window.scrollTo({ top: 0 }); }
62
+ function actions(back, nextLabel, nextOk = true, extra = "") {
63
+ return `<div class="actions">${back ? `<button class="btn" data-act="back">Back</button>` : ""}${nextLabel ? `<button class="btn go" data-act="next" ${nextOk ? "" : "disabled"}>${nextLabel}</button>` : ""}${extra}</div>`;
64
+ }
65
+
66
+ /* ------------------------------------------------------------ steps */
67
+ function stepFormat() {
68
+ return `<h1>What kind of rows do you need?</h1>
69
+ <p class="lede">Pick the shape of each example. You can describe the content in the steps that follow.</p>
70
+ <div class="specimens">${Object.entries(FORMATS).map(([k, f]) => `
71
+ <button class="spec" data-format="${k}" aria-pressed="${S.format === k}"><b>${f.name}</b><span class="hint">${f.blurb}</span>
72
+ <div class="eg">${f.eg.map(([l, t]) => `<p><u>${l}</u>${esc(t)}</p>`).join("")}</div></button>`).join("")}</div>
73
+ ${actions(false, "Continue")}`;
74
+ }
75
+ function stepDomains() {
76
+ return `<h2>Which areas should the data cover?</h2>
77
+ <p class="lede">Choose any number, or skip this and describe the data yourself in the summary.</p>
78
+ <ul class="choices">${S.domainData.map((d) => `<li><button class="choice" data-domain="${esc(d.name)}" aria-pressed="${S.domains.includes(d.name)}"><span class="box"></span><span>${esc(d.name)}</span></button></li>`).join("")}</ul>
79
+ ${actions(true, "Continue")}`;
80
+ }
81
+ function stepSubs() {
82
+ if (!S.domains.length) return `<h2>Subdomains</h2><p class="lede">No domains chosen, so there is nothing to narrow. Continue to the languages, or go back and pick a domain.</p>${actions(true, "Continue")}`;
83
+ return `<h2>Narrow it down</h2><p class="lede">Pick the subdomains you care about. Leave a domain empty to cover all of it.</p>
84
+ ${S.domains.map((name) => { const d = S.domainData.find((x) => x.name === name); const sel = S.subs[name] || [];
85
+ return `<div class="sub-block"><h3>${esc(name)}</h3><ul class="choices">${d.subdomains.map((s) => `<li><button class="choice" data-sub="${esc(name)}|${esc(s)}" aria-pressed="${sel.includes(s)}"><span class="box"></span><span>${esc(sentence(s))}</span></button></li>`).join("")}</ul></div>`; }).join("")}
86
+ ${actions(true, "Continue")}`;
87
+ }
88
+ function stepLangs() {
89
+ return `<h2>Which languages?</h2><p class="lede">Rows are written directly in each language you choose, split evenly. English is the default.</p>
90
+ <ul class="choices">${LANGS.map((l) => `<li><button class="choice" data-lang="${l}" aria-pressed="${S.languages.includes(l)}"><span class="box"></span><span>${l}</span></button></li>`).join("")}</ul>
91
+ ${actions(true, "Continue", S.languages.length > 0)}`;
92
+ }
93
+ function bpView() {
94
+ const bp = S.blueprint; if (!bp) return "";
95
+ return `<div class="bp">
96
+ <div class="bp-row"><b>Who answers</b><span>${esc(bp.assistant_role)}</span></div>
97
+ <div class="bp-row"><b>Topics</b><div class="tree">${bp.topics.map((a) => `<div><strong>${esc(a.area)}</strong> <small>${a.subtopics.map((s) => esc(s.subtopic)).join(", ")}</small></div>`).join("")}</div></div>
98
+ <div class="bp-row"><b>Who asks</b><ul>${bp.personas.map((p) => `<li>${esc(p)}</li>`).join("")}</ul></div>
99
+ <div class="bp-row"><b>Question shapes</b><ul>${bp.formats.map((p) => `<li>${esc(p)}</li>`).join("")}</ul></div>
100
+ </div>`;
101
+ }
102
+ function stepSummary() {
103
+ const suggest = () => { const d = S.domains.length ? ` covering ${S.domains.join(", ").toLowerCase()}` : ""; return `A dataset of ${S.format === "preference" ? "prompts with a better and a worse reply" : "prompts and ideal replies"}${d}.`; };
104
+ return `<h2>Describe the dataset</h2>
105
+ <p class="lede">Say what you want in your own words. The more specific you are about who asks and who answers, the better the rows.</p>
106
+ <label class="f"><span>What should the dataset contain?</span>
107
+ <textarea class="in" id="request" placeholder="Customer support conversations for a small online bookshop: order status, returns, damaged books and gift wrapping.">${esc(S.request)}</textarea></label>
108
+ <p style="margin-top:8px"><button class="linkish" data-act="suggest">Draft a description from my choices</button> <span class="hint" data-suggest="${esc(suggest())}"></span></p>
109
+ <label class="f"><span>Rules every row must follow <span class="hint">(optional)</span></span>
110
+ <textarea class="in" id="constraints" style="min-height:76px" placeholder="Replies must be under 120 words. Use plain text, no markdown.">${esc(S.constraints)}</textarea></label>
111
+ <div class="group"><h3>How many rows?</h3>
112
+ <div class="seg" role="group" aria-label="Rows">${[25, 100, 250, 500, 1000].filter((n) => n <= S.maxRows).map((n) => `<button data-rows="${n}" aria-pressed="${S.rows === n}">${n.toLocaleString()}</button>`).join("")}</div>
113
+ <p class="hint" style="margin-top:8px">Hosted runs are capped at ${S.maxRows.toLocaleString()} rows. Run it yourself for more.</p></div>
114
+ <div class="group"><h3>Blueprint</h3>
115
+ <p class="hint">A plan for coverage: topics, who asks, who answers, and the shapes questions take. Preview it before you spend model calls on rows.</p>
116
+ <div class="actions" style="margin-top:12px"><button class="btn" data-act="blueprint" ${S.bpBusy || !S.request.trim() ? "disabled" : ""}>${S.bpBusy ? "Drafting the blueprint" : S.blueprint ? "Draft it again" : "Preview the blueprint"}</button></div>
117
+ ${S.bpError ? `<p class="note bad">${esc(S.bpError)}</p>` : ""}${bpView()}</div>
118
+ ${actions(true, "Continue", !!S.request.trim())}`;
119
+ }
120
+ function stepEngine() {
121
+ const p = S.ping;
122
+ return `<h2>Choose the model that writes the rows</h2>
123
+ <p class="lede">DataInventor works with any OpenAI-compatible endpoint. The default is a free model on Hugging Face.</p>
124
+ <div class="seg" role="group" aria-label="Presets">${Object.entries(PRESETS).map(([k, v]) => `<button data-preset="${k}" aria-pressed="${S.preset === k}">${v.label}</button>`).join("")}</div>
125
+ <label class="f"><span>Base URL</span><input class="in" id="base_url" value="${esc(S.engine.base_url)}" spellcheck="false"></label>
126
+ <div class="row2"><label class="f"><span>Model</span><input class="in" id="model" value="${esc(S.engine.model)}" spellcheck="false"></label>
127
+ <label class="f"><span>API key</span><input class="in" id="api_key" type="password" autocomplete="off" placeholder="${S.preset === "hf" && S.hosted ? "Optional here. The host supplies one." : "Paste a key"}" value="${esc(S.engine.api_key)}"></label></div>
128
+ <p class="hint" style="margin-top:6px">The key is used for this run only. It is not stored.${S.preset === "local" ? " A hosted Space cannot reach localhost. Run DataInventor on your machine to use a local server." : ""}</p>
129
+ <label class="check"><input type="checkbox" id="think" ${S.engine.disable_thinking ? "checked" : ""}><span><b>Turn thinking off</b><br><span class="hint">For reasoning models that return empty text until they finish thinking. It makes each call several times faster. Leave it off for endpoints that reject the setting.</span></span></label>
130
+ <label class="f" style="max-width:240px"><span>Parallel calls</span><input class="in" id="workers" type="number" min="1" max="8" value="${S.engine.workers}"></label>
131
+ <div class="actions"><button class="btn" data-act="ping" ${S.pinging ? "disabled" : ""}>${S.pinging ? "Testing" : "Test the connection"}</button>
132
+ ${p ? (p.ok ? `<span class="note ok" style="margin:0">Connected. One call took ${p.latency_ms} ms.</span>` : `<span class="note bad" style="margin:0">${esc(p.error)}</span>`) : ""}</div>
133
+ ${actions(true, "Review and start")}`;
134
+ }
135
+ const isFinal = (j) => j && ["done", "stopped", "failed"].includes(j.state);
136
+ function rowHtml(r) {
137
+ const pref = S.format === "preference";
138
+ return `<div class="trow"><span class="id">${r.id}</span><div><div class="p">${esc(r.prompt)}</div><div class="meta">${esc(r.area)}, ${esc(r.persona)}${r.language && r.language !== "English" ? ", " + esc(r.language) : ""}</div>
139
+ <details><summary>${pref ? "Read both replies" : "Read the reply"}</summary><div class="full">${pref ? "Chosen\n" + esc(r.chosen) + "\n\nRejected\n" + esc(r.rejected) : esc(r.completion)}</div></details></div></div>`;
140
+ }
141
+ function deckHtml() {
142
+ const pref = S.format === "preference", latest = S.jobRows.slice(-3);
143
+ const fresh = latest.length ? latest[latest.length - 1].id : 0;
144
+ const out = latest.map((r, i) => `<div class="card ${i === latest.length - 1 && fresh > S.printed ? "print" : ""}" style="transform:translate(${i * 16}px, ${i * 14}px);z-index:${i}"><em>${esc(r.persona)}, ${esc(r.topic)}</em><div class="clip">${esc(r.prompt)}</div><div class="a clip">${esc(pref ? r.chosen : r.completion)}</div></div>`).join("");
145
+ S.printed = Math.max(S.printed, fresh);
146
+ return out;
147
+ }
148
+ function headParts() {
149
+ const j = S.jobState, n = j ? j.count : 0, t = j ? j.target || S.rows : S.rows, pct = t ? Math.min(100, Math.round((n / t) * 100)) : 0, fin = isFinal(j);
150
+ const st = j && j.stats && j.stats.rows ? j.stats : null;
151
+ return {
152
+ h2: fin ? (j.state === "failed" ? "The run stopped" : "Your dataset is ready") : "Writing your dataset",
153
+ count: `<span class="big">${n.toLocaleString()}</span><span class="of">of ${t.toLocaleString()} rows</span>`, pct,
154
+ state: `${j ? esc(j.message) : "Starting"}${j && !fin ? ` Elapsed ${Math.round(j.elapsed)} s.` : ""}`,
155
+ note: j && j.state === "failed" ? `<p class="note bad">${esc(j.message)}</p>` : j && j.state === "done" && j.count < j.target ? `<p class="note">${esc(j.message)}</p>` : "",
156
+ actions: fin ? `${j.count ? `<a class="btn go" href="/api/jobs/${j.id}/download?format=csv" style="text-decoration:none">Download CSV</a><a class="btn" href="/api/jobs/${j.id}/download?format=jsonl" style="text-decoration:none">Download JSONL</a>` : ""}<button class="btn" data-act="again">Start another</button>` : `<button class="btn" data-act="stop">Stop and keep what is written</button>`,
157
+ stats: st ? `<div class="stats"><div><b>${st.rows.toLocaleString()}</b><span>rows</span></div><div><b>${st.distinct3_prompts}</b><span>distinct 3-grams in prompts</span></div><div><b>${st.avg_prompt_words}</b><span>words per prompt</span></div><div><b>${st.avg_answer_words}</b><span>words per reply</span></div></div>` : "",
158
+ filtered: j && j.rejected && Object.keys(j.rejected).length ? `<p class="hint" style="margin-top:12px">Filtered out: ${Object.entries(j.rejected).map(([k, v]) => `${v} ${esc(k)}`).join(", ")}.</p>` : "",
159
+ };
160
+ }
161
+ function stepRun() {
162
+ if (S.runError && !S.jobState) return `<h2>The run did not start</h2><p class="note bad">${esc(S.runError)}</p>${actions(true, "Try again")}`;
163
+ const h = headParts(); S.drawn = S.jobRows.length;
164
+ return `<div id="runroot"><h2 id="run-h2">${h.h2}</h2>
165
+ <div class="count" id="run-count" aria-live="polite">${h.count}</div>
166
+ <div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${h.pct}"><i id="run-bar" style="width:${h.pct}%"></i></div>
167
+ <p class="state" id="run-state">${h.state}</p><div id="run-note">${h.note}</div>
168
+ <div class="actions" id="run-actions">${h.actions}</div><div id="run-stats">${h.stats}</div><div id="run-filtered">${h.filtered}</div>
169
+ <div class="press" id="run-deck" aria-hidden="true">${deckHtml()}</div>
170
+ <div class="table" id="run-rows">${S.jobRows.slice().reverse().map(rowHtml).join("")}</div></div>`;
171
+ }
172
+ /* patch the run view in place so open replies stay open and only a new card prints */
173
+ function updateRun() {
174
+ const h = headParts(), set = (id, html) => { const el = document.getElementById(id); if (el && el.innerHTML !== html) el.innerHTML = html; };
175
+ set("run-h2", h.h2); set("run-count", h.count); set("run-state", h.state); set("run-note", h.note); set("run-actions", h.actions); set("run-stats", h.stats); set("run-filtered", h.filtered);
176
+ const bar = document.getElementById("run-bar"); if (bar) { bar.style.width = h.pct + "%"; bar.parentElement.setAttribute("aria-valuenow", h.pct); }
177
+ const deck = document.getElementById("run-deck"); if (deck) deck.innerHTML = deckHtml();
178
+ const rows = document.getElementById("run-rows");
179
+ if (rows && S.jobRows.length > S.drawn) { rows.insertAdjacentHTML("afterbegin", S.jobRows.slice(S.drawn).reverse().map(rowHtml).join("")); S.drawn = S.jobRows.length; }
180
+ }
181
+
182
+ /* ------------------------------------------------------------ render + events */
183
+ function render() {
184
+ renderLedger(); renderSheet();
185
+ const fn = [stepFormat, stepDomains, stepSubs, stepLangs, stepSummary, stepEngine, stepRun][S.step];
186
+ const keep = document.activeElement && document.activeElement.id;
187
+ $("#panel").innerHTML = fn();
188
+ if (keep) { const el = document.getElementById(keep); if (el) { el.focus(); if (el.setSelectionRange && el.type !== "number") { const l = el.value.length; try { el.setSelectionRange(l, l); } catch (e) { /* ok */ } } } }
189
+ }
190
+ document.addEventListener("click", async (ev) => {
191
+ const t = ev.target.closest("button, a"); if (!t) return;
192
+ if (t.dataset.go !== undefined) return go(+t.dataset.go);
193
+ if (t.dataset.format) { S.format = t.dataset.format; S.blueprint = null; return render(); }
194
+ if (t.dataset.domain) { const d = t.dataset.domain; S.domains = S.domains.includes(d) ? S.domains.filter((x) => x !== d) : [...S.domains, d]; if (!S.domains.includes(d)) delete S.subs[d]; S.blueprint = null; return render(); }
195
+ if (t.dataset.sub) { const [d, s] = t.dataset.sub.split("|"); const cur = S.subs[d] || []; S.subs[d] = cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s]; S.blueprint = null; return render(); }
196
+ if (t.dataset.lang) { const l = t.dataset.lang; S.languages = S.languages.includes(l) ? S.languages.filter((x) => x !== l) : [...S.languages, l]; S.blueprint = null; return render(); }
197
+ if (t.dataset.rows) { S.rows = +t.dataset.rows; return render(); }
198
+ if (t.dataset.preset) { const p = PRESETS[t.dataset.preset]; S.preset = t.dataset.preset; S.engine = { ...S.engine, base_url: p.base_url, model: p.model, disable_thinking: p.disable_thinking }; S.ping = null; return render(); }
199
+ const act = t.dataset.act; if (!act) return;
200
+ if (act === "next") return void go(S.step + 1);
201
+ if (act === "back") return go(Math.max(0, S.step - 1));
202
+ if (act === "suggest") { S.request = S.request.trim() ? S.request : $("[data-suggest]").dataset.suggest; return render(); }
203
+ if (act === "blueprint") return doBlueprint();
204
+ if (act === "ping") return doPing();
205
+ if (act === "stop") { if (S.job) await api(`/api/jobs/${S.job}/stop`, {}); return; }
206
+ if (act === "again") { S.job = null; S.jobState = null; S.jobRows = []; S.drawn = 0; S.printed = 0; S.runError = ""; return go(4); }
207
+ });
208
+ document.addEventListener("input", (ev) => {
209
+ const id = ev.target.id;
210
+ if (id === "request") { S.request = ev.target.value; S.blueprint = null; const b = $('[data-act="next"]'); if (b) b.disabled = !S.request.trim(); const bp = $('[data-act="blueprint"]'); if (bp) bp.disabled = S.bpBusy || !S.request.trim(); }
211
+ else if (id === "constraints") { S.constraints = ev.target.value; S.blueprint = null; }
212
+ else if (id === "base_url") { S.engine.base_url = ev.target.value; S.preset = ""; }
213
+ else if (id === "model") S.engine.model = ev.target.value;
214
+ else if (id === "api_key") S.engine.api_key = ev.target.value;
215
+ else if (id === "workers") { S.engine.workers = Math.max(1, Math.min(8, +ev.target.value || 1)); renderSheet(); }
216
+ else if (id === "think") S.engine.disable_thinking = ev.target.checked;
217
+ if (id === "model") renderSheet();
218
+ });
219
+ document.addEventListener("change", (ev) => { if (ev.target.id === "think") S.engine.disable_thinking = ev.target.checked; });
220
+
221
+ async function doBlueprint() {
222
+ S.bpBusy = true; S.bpError = ""; render();
223
+ try { S.blueprint = await api("/api/blueprint", spec()); } catch (e) { S.bpError = e.message; }
224
+ S.bpBusy = false; render();
225
+ }
226
+ async function doPing() {
227
+ S.pinging = true; S.ping = null; render();
228
+ try { S.ping = await api("/api/ping", S.engine); } catch (e) { S.ping = { ok: false, error: e.message }; }
229
+ S.pinging = false; render();
230
+ }
231
+ async function startRun() {
232
+ S.runError = ""; S.jobRows = []; S.jobState = null; S.drawn = 0; S.printed = 0;
233
+ try { const r = await api("/api/jobs", spec()); S.job = r.id; poll(); } catch (e) { S.runError = e.message; }
234
+ render();
235
+ }
236
+ async function poll() {
237
+ const id = S.job; if (!id) return;
238
+ try {
239
+ const j = await api(`/api/jobs/${id}?since=${S.jobRows.length}`);
240
+ if (id !== S.job) return;
241
+ const wasFinal = isFinal(S.jobState); S.jobRows.push(...j.new_rows); S.jobState = j;
242
+ if (S.step === 6) { if (document.getElementById("runroot") && !wasFinal) updateRun(); else render(); }
243
+ if (!["done", "stopped", "failed"].includes(j.state) || j.count > S.jobRows.length) return void setTimeout(poll, j.count > S.jobRows.length ? 200 : 1500);
244
+ if (j.stats && j.stats.rows) S.maxStep = 6;
245
+ } catch (e) { console.error(e.stack); S.jobState = { state: "failed", message: e.message, count: S.jobRows.length, target: S.rows, elapsed: 0, id }; if (S.step === 6) render(); }
246
+ }
247
+ /* "Review and start" on the engine step starts the run */
248
+ const _go = go;
249
+ go = function (i) { const wasEngine = S.step === 5; _go(i); if (i === 6 && wasEngine && !S.jobState) startRun(); return true; };
250
+
251
+ (async function init() {
252
+ try { const d = await api("/api/defaults"); S.hosted = d.hosted_key; S.maxRows = d.max_rows; if (S.rows > S.maxRows) S.rows = S.maxRows; $("#hostnote").textContent = d.hosted_key ? "Hosted model key supplied by the Space." : ""; } catch (e) { /* offline */ }
253
+ try { S.domainData = await api("/api/domains"); } catch (e) { S.domainData = []; }
254
+ render();
255
+ })();
static/domains.json ADDED
@@ -0,0 +1,716 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "Academic education",
4
+ "subdomains": [
5
+ "online learning",
6
+ "virtual classrooms",
7
+ "open educational resources",
8
+ "blended learning",
9
+ "microlearning",
10
+ "flipped classroom",
11
+ "adaptive learning",
12
+ "peer tutoring",
13
+ "competency based education",
14
+ "digital literacy",
15
+ "collaborative learning",
16
+ "personalized education"
17
+ ]
18
+ },
19
+ {
20
+ "name": "Agriculture",
21
+ "subdomains": [
22
+ "Precision Farming",
23
+ "Sustainable Agriculture",
24
+ "Agroecology",
25
+ "Vertical Farming",
26
+ "Regenerative Agriculture",
27
+ "Organic Farming",
28
+ "Aquaculture",
29
+ "Permaculture",
30
+ "Agroforestry",
31
+ "Hydroponics",
32
+ "Biotechnology in Agriculture",
33
+ "Climate-Smart Agriculture"
34
+ ]
35
+ },
36
+ {
37
+ "name": "Animal and nature",
38
+ "subdomains": [
39
+ "Wildlife Conservation",
40
+ "Botanical Gardens",
41
+ "Marine Biology",
42
+ "Bird Watching",
43
+ "Insect Ecology",
44
+ "Forest Preservation",
45
+ "Zoo Management",
46
+ "Nature Photography",
47
+ "Habitat Restoration",
48
+ "Mammal Studies",
49
+ "Desert Ecosystems",
50
+ "Aquatic Life"
51
+ ]
52
+ },
53
+ {
54
+ "name": "Architecture and design",
55
+ "subdomains": [
56
+ "residential architecture",
57
+ "commercial design",
58
+ "sustainable building",
59
+ "urban planning",
60
+ "interior design",
61
+ "landscape architecture",
62
+ "industrial design",
63
+ "digital architecture",
64
+ "adaptive reuse",
65
+ "smart cities",
66
+ "biophilic design",
67
+ "modular construction"
68
+ ]
69
+ },
70
+ {
71
+ "name": "Art",
72
+ "subdomains": [
73
+ "Art History",
74
+ "Modern Art",
75
+ "Contemporary Art",
76
+ "Digital Art",
77
+ "Abstract Art",
78
+ "Sculpture Art",
79
+ "Photography Art",
80
+ "Street Art",
81
+ "Pop Art",
82
+ "Minimalist Art",
83
+ "Installation Art",
84
+ "Folk Art"
85
+ ]
86
+ },
87
+ {
88
+ "name": "Career and workplace",
89
+ "subdomains": [
90
+ "Resume Writing",
91
+ "Interview Skills",
92
+ "Career Planning",
93
+ "Workplace Communication",
94
+ "Professional Development",
95
+ "Leadership Training",
96
+ "Time Management",
97
+ "Conflict Resolution",
98
+ "Team Collaboration",
99
+ "Job Search Strategies",
100
+ "Salary Negotiation",
101
+ "Remote Work Etiquette"
102
+ ]
103
+ },
104
+ {
105
+ "name": "Code",
106
+ "subdomains": [
107
+ "Code Review",
108
+ "Code Analysis",
109
+ "Code Optimization",
110
+ "Code Refactoring",
111
+ "Code Documentation",
112
+ "Code Testing",
113
+ "Code Security",
114
+ "Code Generation",
115
+ "Code Quality",
116
+ "Code Architecture",
117
+ "Code Maintenance",
118
+ "Code Education"
119
+ ]
120
+ },
121
+ {
122
+ "name": "Cooking",
123
+ "subdomains": [
124
+ "quick recipes",
125
+ "healthy meals",
126
+ "baking basics",
127
+ "vegan cooking",
128
+ "meal prep",
129
+ "dessert ideas",
130
+ "kitchen tips",
131
+ "international cuisine",
132
+ "slow cooker",
133
+ "budget cooking",
134
+ "food safety",
135
+ "pasta dishes"
136
+ ]
137
+ },
138
+ {
139
+ "name": "Corporate and business",
140
+ "subdomains": [
141
+ "Corporate Governance",
142
+ "Business Strategy",
143
+ "Financial Management",
144
+ "Human Resources",
145
+ "Marketing Analytics",
146
+ "Supply Chain",
147
+ "Risk Management",
148
+ "Customer Service",
149
+ "Legal Compliance",
150
+ "Project Management",
151
+ "Leadership Development",
152
+ "Operational Efficiency"
153
+ ]
154
+ },
155
+ {
156
+ "name": "Culture",
157
+ "subdomains": [
158
+ "Visual Arts",
159
+ "Music History",
160
+ "Literary Traditions",
161
+ "Performing Arts",
162
+ "Cultural Anthropology",
163
+ "Digital Culture",
164
+ "Food Culture",
165
+ "Religious Practices",
166
+ "Fashion Trends",
167
+ "Architectural Heritage",
168
+ "Folklore Studies",
169
+ "Media Studies"
170
+ ]
171
+ },
172
+ {
173
+ "name": "Data analysis and visualization",
174
+ "subdomains": [
175
+ "Data Cleaning Techniques",
176
+ "Statistical Analysis Methods",
177
+ "Machine Learning Algorithms",
178
+ "Data Visualization Tools",
179
+ "Database Management Systems",
180
+ "Big Data Processing",
181
+ "Predictive Analytics Models",
182
+ "Exploratory Data Analysis",
183
+ "Data Mining Strategies",
184
+ "Information Visualization Design",
185
+ "Natural Language Processing",
186
+ "Real-time Data Streaming"
187
+ ]
188
+ },
189
+ {
190
+ "name": "Dating",
191
+ "subdomains": [
192
+ "dating apps",
193
+ "online dating",
194
+ "dating sites",
195
+ "dating advice",
196
+ "dating tips",
197
+ "dating culture",
198
+ "dating trends",
199
+ "dating psychology",
200
+ "dating safety",
201
+ "dating etiquette",
202
+ "dating apps",
203
+ "dating strategies"
204
+ ]
205
+ },
206
+ {
207
+ "name": "Entertainment",
208
+ "subdomains": [
209
+ "Movie Reviews",
210
+ "TV Series",
211
+ "Music Charts",
212
+ "Celebrity News",
213
+ "Streaming Platforms",
214
+ "Concert Tickets",
215
+ "Theme Parks",
216
+ "Podcast Shows",
217
+ "Video Games",
218
+ "Live Events",
219
+ "Book Adaptations",
220
+ "Comedy Specials"
221
+ ]
222
+ },
223
+ {
224
+ "name": "Fashion and beauty",
225
+ "subdomains": [
226
+ "Skincare Basics",
227
+ "Makeup Tutorials",
228
+ "Hairstyle Ideas",
229
+ "Fashion Trends",
230
+ "Beauty Products",
231
+ "Style Guides",
232
+ "Wellness Tips",
233
+ "Beauty Hacks",
234
+ "Fashion History",
235
+ "Skincare Routines",
236
+ "Makeup Reviews",
237
+ "Fashion Inspiration"
238
+ ]
239
+ },
240
+ {
241
+ "name": "Fitness and sports",
242
+ "subdomains": [
243
+ "home fitness",
244
+ "outdoor sports",
245
+ "team sports",
246
+ "individual training",
247
+ "strength training",
248
+ "cardiovascular fitness",
249
+ "yoga and pilates",
250
+ "martial arts",
251
+ "swimming fitness",
252
+ "cycling sports",
253
+ "running training",
254
+ "flexibility exercises"
255
+ ]
256
+ },
257
+ {
258
+ "name": "Games",
259
+ "subdomains": [
260
+ "mobile games",
261
+ "indie games",
262
+ "racing games",
263
+ "strategy games",
264
+ "role playing games",
265
+ "shooter games",
266
+ "puzzle games",
267
+ "sports games",
268
+ "simulation games",
269
+ "board games",
270
+ "card games",
271
+ "party games"
272
+ ]
273
+ },
274
+ {
275
+ "name": "Geography",
276
+ "subdomains": [
277
+ "Physical Geography",
278
+ "Human Geography",
279
+ "Cartography",
280
+ "Geomorphology",
281
+ "Biogeography",
282
+ "Economic Geography",
283
+ "Political Geography",
284
+ "Cultural Geography",
285
+ "Urban Geography",
286
+ "Environmental Geography",
287
+ "Social Geography",
288
+ "Historical Geography"
289
+ ]
290
+ },
291
+ {
292
+ "name": "Governance",
293
+ "subdomains": [
294
+ "Public Administration",
295
+ "Corporate Governance",
296
+ "Government Accountability",
297
+ "Policy Implementation",
298
+ "Regulatory Compliance",
299
+ "Ethical Leadership",
300
+ "Stakeholder Engagement",
301
+ "Decision Making",
302
+ "Organizational Strategy",
303
+ "Transparency Practices",
304
+ "Governance Frameworks",
305
+ "Accountability Mechanisms"
306
+ ]
307
+ },
308
+ {
309
+ "name": "History",
310
+ "subdomains": [
311
+ "Ancient Civilizations",
312
+ "Medieval Europe",
313
+ "World War II",
314
+ "Cold War Era",
315
+ "American Revolution",
316
+ "Renaissance Period",
317
+ "Industrial Revolution",
318
+ "Civil Rights Movement",
319
+ "Ancient Egypt",
320
+ "Classical Greece",
321
+ "Cold War Diplomacy",
322
+ "Colonial America"
323
+ ]
324
+ },
325
+ {
326
+ "name": "How-to",
327
+ "subdomains": [
328
+ "how-to-fix",
329
+ "how-to-cook",
330
+ "how-to-program",
331
+ "how-to-travel",
332
+ "how-to-clean",
333
+ "how-to-build",
334
+ "how-to-save",
335
+ "how-to-learn",
336
+ "how-to-organize",
337
+ "how-to-design",
338
+ "how-to-market",
339
+ "how-to-lead"
340
+ ]
341
+ },
342
+ {
343
+ "name": "HR",
344
+ "subdomains": [
345
+ "Human Resources",
346
+ "Recruitment Services",
347
+ "Employee Relations",
348
+ "Performance Management",
349
+ "Compensation Benefits",
350
+ "Training Development",
351
+ "Organizational Development",
352
+ "Workforce Planning",
353
+ "Employee Engagement",
354
+ "HR Analytics",
355
+ "Compliance Legal",
356
+ "Diversity Inclusion"
357
+ ]
358
+ },
359
+ {
360
+ "name": "Language",
361
+ "subdomains": [
362
+ "Natural Language Processing",
363
+ "Computational Linguistics",
364
+ "Machine Translation",
365
+ "Speech Recognition",
366
+ "Text Generation",
367
+ "Sentiment Analysis",
368
+ "Machine Learning",
369
+ "Deep Learning",
370
+ "Neural Networks",
371
+ "Data Annotation",
372
+ "Language Modeling",
373
+ "Natural Language Inference"
374
+ ]
375
+ },
376
+ {
377
+ "name": "Legal",
378
+ "subdomains": [
379
+ "legal research",
380
+ "case law",
381
+ "statutory interpretation",
382
+ "legal ethics",
383
+ "contract law",
384
+ "tort law",
385
+ "criminal procedure",
386
+ "civil procedure",
387
+ "legal writing",
388
+ "legal argumentation",
389
+ "legal reasoning",
390
+ "legal ethics"
391
+ ]
392
+ },
393
+ {
394
+ "name": "Market analysis",
395
+ "subdomains": [
396
+ "Market Trends Analysis",
397
+ "Consumer Behavior Study",
398
+ "Competitive Intelligence Report",
399
+ "Industry Growth Forecast",
400
+ "Product Market Fit",
401
+ "Market Segmentation Strategy",
402
+ "Demand Supply Analysis",
403
+ "Pricing Strategy Review",
404
+ "Brand Positioning Analysis",
405
+ "Customer Acquisition Cost",
406
+ "Market Share Evaluation",
407
+ "Economic Impact Study"
408
+ ]
409
+ },
410
+ {
411
+ "name": "Marketing",
412
+ "subdomains": [
413
+ "Digital Marketing",
414
+ "Content Marketing",
415
+ "Social Media Marketing",
416
+ "Email Marketing",
417
+ "Search Engine Marketing",
418
+ "Performance Marketing",
419
+ "Brand Marketing",
420
+ "Growth Marketing",
421
+ "Content Strategy",
422
+ "Marketing Automation",
423
+ "Marketing Analytics",
424
+ "Influencer Marketing"
425
+ ]
426
+ },
427
+ {
428
+ "name": "Math",
429
+ "subdomains": [
430
+ "Linear Algebra",
431
+ "Calculus",
432
+ "Statistics",
433
+ "Number Theory",
434
+ "Topology",
435
+ "Probability",
436
+ "Differential Equations",
437
+ "Abstract Algebra",
438
+ "Geometry",
439
+ "Combinatorics",
440
+ "Analysis",
441
+ "Graph Theory"
442
+ ]
443
+ },
444
+ {
445
+ "name": "Medical",
446
+ "subdomains": [
447
+ "medical research",
448
+ "clinical trials",
449
+ "medical education",
450
+ "medical devices",
451
+ "medical imaging",
452
+ "medical diagnostics",
453
+ "medical treatments",
454
+ "medical ethics",
455
+ "medical statistics",
456
+ "medical informatics",
457
+ "medical genetics",
458
+ "medical psychology"
459
+ ]
460
+ },
461
+ {
462
+ "name": "Music",
463
+ "subdomains": [
464
+ "Music Theory Basics",
465
+ "Jazz Harmony Guide",
466
+ "Classical Music History",
467
+ "Electronic Music Production",
468
+ "Songwriting Techniques",
469
+ "Music Performance Skills",
470
+ "Music Technology Tools",
471
+ "Music Education Methods",
472
+ "Music Psychology Insights",
473
+ "Music Marketing Strategies",
474
+ "Music Copyright Laws",
475
+ "Music Streaming Platforms"
476
+ ]
477
+ },
478
+ {
479
+ "name": "News",
480
+ "subdomains": [
481
+ "Breaking News",
482
+ "World News",
483
+ "Sports News",
484
+ "Tech News",
485
+ "Business News",
486
+ "Health News",
487
+ "Entertainment News",
488
+ "Local News",
489
+ "Science News",
490
+ "Political News",
491
+ "Climate News",
492
+ "Opinion News"
493
+ ]
494
+ },
495
+ {
496
+ "name": "Parenting and family",
497
+ "subdomains": [
498
+ "Child Development Stages",
499
+ "Family Communication Skills",
500
+ "Parenting Styles Overview",
501
+ "Emotional Intelligence Training",
502
+ "Conflict Resolution Techniques",
503
+ "Positive Discipline Methods",
504
+ "Teen Parenting Challenges",
505
+ "Single Parent Support",
506
+ "Blended Family Dynamics",
507
+ "Early Childhood Education",
508
+ "Family Rituals and Traditions",
509
+ "Parental Self-Care Practices"
510
+ ]
511
+ },
512
+ {
513
+ "name": "Personal finance",
514
+ "subdomains": [
515
+ "budgeting basics",
516
+ "debt management",
517
+ "investment strategies",
518
+ "retirement planning",
519
+ "tax optimization",
520
+ "credit score improvement",
521
+ "financial literacy",
522
+ "emergency fund building",
523
+ "real estate investing",
524
+ "cryptocurrency basics",
525
+ "insurance coverage",
526
+ "financial independence"
527
+ ]
528
+ },
529
+ {
530
+ "name": "Personal growth",
531
+ "subdomains": [
532
+ "Mindset Mastery",
533
+ "Habit Formation",
534
+ "Emotional Intelligence",
535
+ "Goal Setting",
536
+ "Self Awareness",
537
+ "Productivity Systems",
538
+ "Communication Skills",
539
+ "Resilience Building",
540
+ "Time Management",
541
+ "Confidence Development",
542
+ "Critical Thinking",
543
+ "Work Life Balance"
544
+ ]
545
+ },
546
+ {
547
+ "name": "Product advice",
548
+ "subdomains": [
549
+ "Product Advice",
550
+ "Product Reviews",
551
+ "Product Comparison",
552
+ "Product Ratings",
553
+ "Product Specs",
554
+ "Product Deals",
555
+ "Product Tips",
556
+ "Product Guides",
557
+ "Product Q&A",
558
+ "Product News",
559
+ "Product Updates",
560
+ "Product Support"
561
+ ]
562
+ },
563
+ {
564
+ "name": "Religion",
565
+ "subdomains": [
566
+ "Christian Theology",
567
+ "Islamic Jurisprudence",
568
+ "Buddhist Philosophy",
569
+ "Jewish Law",
570
+ "Hindu Mythology",
571
+ "Sikh Beliefs",
572
+ "Daoist Practices",
573
+ "Zoroastrianism",
574
+ "Jainism",
575
+ "Secular Humanism",
576
+ "Comparative Religion",
577
+ "Religious Ethics"
578
+ ]
579
+ },
580
+ {
581
+ "name": "Roleplay",
582
+ "subdomains": [
583
+ "Creative Writing Roleplay",
584
+ "Fantasy Roleplay Community",
585
+ "Sci-Fi Roleplay Hub",
586
+ "Historical Roleplay Forum",
587
+ "Romance Roleplay Network",
588
+ "Horror Roleplay Group",
589
+ "Mystery Roleplay Space",
590
+ "Adventure Roleplay Zone",
591
+ "Daily Roleplay Challenges",
592
+ "Roleplay Tips and Tricks",
593
+ "Beginner Roleplay Guide",
594
+ "Advanced Roleplay Techniques"
595
+ ]
596
+ },
597
+ {
598
+ "name": "Science",
599
+ "subdomains": [
600
+ "Space Exploration",
601
+ "Quantum Physics",
602
+ "Marine Biology",
603
+ "Climate Science",
604
+ "Neuroscience Research",
605
+ "Astrophysics Studies",
606
+ "Genetics Analysis",
607
+ "Chemistry Education",
608
+ "Environmental Science",
609
+ "Mathematics Theory",
610
+ "Robotics Development",
611
+ "Virology Studies"
612
+ ]
613
+ },
614
+ {
615
+ "name": "Social media",
616
+ "subdomains": [
617
+ "Social Networking",
618
+ "Content Sharing",
619
+ "Video Streaming",
620
+ "Micro Blogging",
621
+ "Photo Sharing",
622
+ "Audio Podcasting",
623
+ "Professional Networking",
624
+ "Community Forums",
625
+ "Live Streaming",
626
+ "Text Messaging",
627
+ "Photo Editing",
628
+ "Social Commerce"
629
+ ]
630
+ },
631
+ {
632
+ "name": "Sports",
633
+ "subdomains": [
634
+ "Olympic Games",
635
+ "Professional Leagues",
636
+ "College Athletics",
637
+ "Youth Sports",
638
+ "Extreme Sports",
639
+ "Adaptive Sports",
640
+ "Esports",
641
+ "Martial Arts",
642
+ "Winter Sports",
643
+ "Outdoor Adventures",
644
+ "Fitness Training",
645
+ "Sports Psychology"
646
+ ]
647
+ },
648
+ {
649
+ "name": "Technology",
650
+ "subdomains": [
651
+ "Artificial Intelligence",
652
+ "Cybersecurity",
653
+ "Cloud Computing",
654
+ "Data Science",
655
+ "Internet of Things",
656
+ "Machine Learning",
657
+ "Mobile Development",
658
+ "Robotics",
659
+ "Software Engineering",
660
+ "Virtual Reality",
661
+ "Web Development",
662
+ "Blockchain Technology"
663
+ ]
664
+ },
665
+ {
666
+ "name": "Transportation",
667
+ "subdomains": [
668
+ "Public Transit Systems",
669
+ "Freight Logistics Management",
670
+ "Urban Mobility Solutions",
671
+ "Railway Network Operations",
672
+ "Aviation Safety Protocols",
673
+ "Maritime Shipping Routes",
674
+ "Electric Vehicle Infrastructure",
675
+ "Traffic Flow Optimization",
676
+ "Autonomous Vehicle Testing",
677
+ "Pedestrian Safety Measures",
678
+ "Cycling Infrastructure Development",
679
+ "Supply Chain Coordination"
680
+ ]
681
+ },
682
+ {
683
+ "name": "Travel",
684
+ "subdomains": [
685
+ "budget travel",
686
+ "luxury travel",
687
+ "adventure travel",
688
+ "eco travel",
689
+ "solo travel",
690
+ "family travel",
691
+ "cruise travel",
692
+ "road trip",
693
+ "backpacking travel",
694
+ "cultural travel",
695
+ "business travel",
696
+ "beach travel"
697
+ ]
698
+ },
699
+ {
700
+ "name": "Writing and editing",
701
+ "subdomains": [
702
+ "style guides",
703
+ "grammar rules",
704
+ "punctuation usage",
705
+ "sentence structure",
706
+ "paragraph organization",
707
+ "narrative techniques",
708
+ "academic writing",
709
+ "business correspondence",
710
+ "creative storytelling",
711
+ "technical documentation",
712
+ "editing strategies",
713
+ "proofreading methods"
714
+ ]
715
+ }
716
+ ]
static/index.html ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>DataInventor</title>
7
+ <meta name="description" content="Describe a dataset in plain words and get training data back. Open source, works with any OpenAI-compatible model.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:ital,wght@0,400..700;1,400..700&family=Literata:ital,opsz,wght@0,7..72,400..600;1,7..72,400..600&display=swap" rel="stylesheet">
11
+ <link rel="stylesheet" href="/static/app.css">
12
+ </head>
13
+ <body>
14
+ <a class="skip" href="#panel">Skip to the step</a>
15
+ <header class="top">
16
+ <div class="brand">
17
+ <span class="mark" aria-hidden="true"><i></i><i></i></span>
18
+ <span class="name">DataInventor</span>
19
+ </div>
20
+ <p class="tag">Describe the data you wish existed. Get training rows back. Bring any OpenAI-compatible model.</p>
21
+ </header>
22
+
23
+ <main class="shell">
24
+ <nav class="ledger" aria-label="Steps"><ol id="steps"></ol></nav>
25
+ <section class="panel" id="panel" tabindex="-1" aria-live="polite"></section>
26
+ <aside class="sheet" id="sheet" aria-label="Your dataset so far"></aside>
27
+ </main>
28
+
29
+ <footer class="foot">
30
+ <span>Open source recreation of the Invent a Dataset workflow. Not affiliated with Adaption Labs.</span>
31
+ <span id="hostnote"></span>
32
+ </footer>
33
+ <script src="/static/app.js"></script>
34
+ </body>
35
+ </html>