evals-lab 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -139,9 +139,9 @@ function verdictOf(said )
139
139
  can hold one. A reply that still cannot be read is asked for once more;
140
140
  a second, or a grader that failed, is the grader's error -- said as one,
141
141
  so it is not read as the reply failing. [ok] says whether a verdict read
142
- is one this metric can use. */
142
+ is one this metric can use: true, or why not. */
143
143
  async function graded(ctx , prompt , schema ,
144
- ok = () => true) {
144
+ ok = () => true) {
145
145
  if (!ctx.ask) throw new Error("a model-graded metric needs a grader, and this run has none");
146
146
  let last = "";
147
147
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -149,14 +149,21 @@ async function graded(ctx , prompt , schema
149
149
  try { said = await ctx.ask(prompt, schema); } catch (e) { throw new Error(`grader error: ${e instanceof Error ? e.message : String(e)}`); }
150
150
  try {
151
151
  const v = verdictOf(said);
152
- if (ok(v)) return v;
153
- last = `the grader said ${short(JSON.stringify(v))}`;
152
+ const usable = ok(v);
153
+ if (usable === true) return v;
154
+ last = typeof usable === "string" ? usable : `the grader said ${short(JSON.stringify(v))}`;
154
155
  } catch (e) { last = e instanceof Error ? e.message : String(e); }
155
156
  }
156
157
  throw new Error(`grader error: ${last}`);
157
158
  }
159
+ /** Whether a pass-or-fail verdict's score is on the scale it was asked for.
160
+ One off it -- a 4 echoing a 1-to-5 rating -- is not clamped to a score the
161
+ grader never gave: it is read as unreadable, asked for again, then the
162
+ grader's error (#514). */
163
+ const onScale = (v ) =>
164
+ typeof v.score !== "number" || (v.score >= 0 && v.score <= 1) || `the grader scored ${v.score}, off the 0 to 1 scale`;
158
165
  const gradedPass = (v , threshold ) => {
159
- const score = typeof v.score === "number" ? Math.max(0, Math.min(1, v.score)) : v.pass === true ? 1 : 0;
166
+ const score = typeof v.score === "number" ? v.score : v.pass === true ? 1 : 0;
160
167
  const pass = typeof v.pass === "boolean" ? v.pass && score >= threshold : score >= threshold;
161
168
  return { pass, score, reason: str(v.reason) || (pass ? "the grader passed it" : "the grader failed it") };
162
169
  };
@@ -339,6 +346,7 @@ const metrics = {
339
346
  // rules discarded is the finding.
340
347
  discarded: {
341
348
  family: RESULT,
349
+ readsResult: true,
342
350
  label: "Discarded",
343
351
  description: "Passes only when the job's rules discarded the reply.",
344
352
  options: [],
@@ -348,8 +356,26 @@ const metrics = {
348
356
  return ok(thrown, thrown ? "discarded" : input.error ? input.error : `kept ${input.terms.length} items`);
349
357
  },
350
358
  },
359
+ // A case that expects no answer at all: the connection answering an error
360
+ // -- a 400 for a file it can't read -- is the finding. A discard is
361
+ // Discarded's, at whichever stage it was.
362
+ refused: {
363
+ family: RESULT,
364
+ readsResult: true,
365
+ readsRefusal: true,
366
+ label: "Refused",
367
+ description: "Passes only when the target's call was refused: an error from the connection, not a reply.",
368
+ options: [],
369
+ defaults: () => ({}),
370
+ score: (input) => {
371
+ if (!input.error) return ok(false, "replied");
372
+ const refused = !/^(stage \d+: )?discarded: /.test(input.error);
373
+ return ok(refused, refused ? `refused: ${input.error}` : input.error);
374
+ },
375
+ },
351
376
  "item-count": {
352
377
  family: RESULT,
378
+ readsResult: true,
353
379
  label: "Item count",
354
380
  description: "Passes when the number of items is within the bounds.",
355
381
  options: [{ key: "min", label: "At least", type: "number" }, { key: "max", label: "At most", type: "number" }],
@@ -385,6 +411,7 @@ const metrics = {
385
411
  },
386
412
  latency: {
387
413
  family: RESULT,
414
+ readsResult: true,
388
415
  label: "Latency",
389
416
  description: "Passes when the reply came back within the time set.",
390
417
  options: [{ key: "max", label: "At most (ms)", type: "number" }],
@@ -472,7 +499,7 @@ const metrics = {
472
499
  validate: (m, at, bad) => needs(m, ["rubric"], at, bad),
473
500
  score: async (input, m, ctx) => gradedPass(await graded(ctx,
474
501
  `You are grading an output against a rubric.\n\n${taskOf(input, "")}Rubric:\n${str(m.rubric)}\n\nOutput:\n${input.text}\n\n${REPLY}`,
475
- VERDICT_SCHEMA), num(m.threshold) ?? 0.5),
502
+ VERDICT_SCHEMA, onScale), num(m.threshold) ?? 0.5),
476
503
  },
477
504
  factuality: {
478
505
  family: GRADED,
@@ -488,7 +515,7 @@ const metrics = {
488
515
  score: async (input, m, ctx) => gradedPass(await graded(ctx,
489
516
  `You are checking an output for factual consistency with a reference. Differences in wording or detail are `
490
517
  + `fine; a contradiction, or a fact the reference does not support, is not.\n\n${taskOf(input, "")}Reference:\n`
491
- + `${str(m.reference).trim() || recorded(input)}\n\nOutput:\n${input.text}\n\n${REPLY}`, VERDICT_SCHEMA), num(m.threshold) ?? 0.5),
518
+ + `${str(m.reference).trim() || recorded(input)}\n\nOutput:\n${input.text}\n\n${REPLY}`, VERDICT_SCHEMA, onScale), num(m.threshold) ?? 0.5),
492
519
  },
493
520
  "judge-vs-production": {
494
521
  family: GRADED,
package/lab/run-evals.js CHANGED
@@ -479,20 +479,24 @@ function prepare(bin, file, c) {
479
479
 
480
480
  // ---- The type → handler registry ------------------------------------------
481
481
  // Design §4. What a file *is* to a run is decided here and only here, so the
482
- // queue and the page never learn about file types, and pdf and xlsx are
483
- // later entries that touch nothing but this map.
482
+ // queue and the page never learn about file types, and xlsx is a later
483
+ // entry that touches nothing but this map.
484
484
  //
485
485
  // A file with no mapper is passed through anyway -- decoded as UTF-8 with
486
486
  // replacement and joined at {text} like the text handler, and flagged in the
487
- // transcript. Stating the failure mode beats hiding it: a PDF's text layer
488
- // is not extracted, so its streams arrive as arbitrary bytes, and a run
489
- // that read them "well" would be a finding about nothing.
487
+ // transcript. Stating the failure mode beats hiding it: a binary format's
488
+ // bytes arrive as mojibake, and a run that read them "well" would be a
489
+ // finding about nothing.
490
490
  const HANDLERS = new Map();
491
491
  [
492
492
  // Images: prepared to the profile's pixel budget by ImageMagick and sent
493
493
  // as the image content part. The formats are server.py's FILE_TYPES
494
494
  // image half, so nothing accepted for upload has no executor to run it.
495
495
  ["image", "jpg", "jpeg", "png", "heic", "heif", "dng", "tif", "tiff"],
496
+ // Documents: the file's bytes, unprocessed, handed to an API that reads
497
+ // them whole (core.DOCUMENT_TYPES); a connection that cannot is refused
498
+ // before the run, by validatePipeline.
499
+ ["document", ...Object.keys(core.DOCUMENT_TYPES).map(e => e.slice(1))],
496
500
  // Text-ish: decoded UTF-8, joining the stage at {text} -- the text-content
497
501
  // semantics the canvas already has (core.textPrompt).
498
502
  ["text", "txt", "md", "csv"],
@@ -511,8 +515,8 @@ const UNMAPPED = "the file has no handler, so it was passed through as raw "
511
515
 
512
516
  /**
513
517
  * One file of the Source made into what the job consumes: an image as the
514
- * dataUrl at [conn]'s budget, or text at {text}. A file with no mapper
515
- * still runs, and says so.
518
+ * dataUrl at [conn]'s budget, a document as its bytes, or text at {text}. A
519
+ * file with no mapper still runs, and says so.
516
520
  */
517
521
  function itemContent(bin, file, conn) {
518
522
  const kind = HANDLERS.get(extOf(file));
@@ -520,6 +524,7 @@ function itemContent(bin, file, conn) {
520
524
  const { dataUrl, dims } = prepare(bin, file, conn);
521
525
  return { kind, dataUrl, text: null, flags: [] };
522
526
  }
527
+ if (kind === "document") return { kind, dataUrl: documentOf(file), text: null, flags: [] };
523
528
  // TextDecoder with fatal unset replaces malformed bytes with U+FFFD --
524
529
  // what "decoded as UTF-8 with replacement" means, for a mapped-text file
525
530
  // and an unmapped one alike.
@@ -527,6 +532,13 @@ function itemContent(bin, file, conn) {
527
532
  return { kind: kind ?? "unmapped", dataUrl: null, text, flags: kind ? [] : [UNMAPPED] };
528
533
  }
529
534
 
535
+ /** A document file as the attachment a stage sends: its bytes, its media
536
+ type and its name. */
537
+ function documentOf(file) {
538
+ const name = path.basename(file);
539
+ return core.documentUrl(name, core.documentTypeOf(name).media, fs.readFileSync(file).toString("base64"));
540
+ }
541
+
530
542
  // ---- Reaching a model ----------------------------------------------------
531
543
 
532
544
  /**
@@ -737,9 +749,10 @@ async function ask(to, c, prompt, dataUrl, schema = null) {
737
749
  // this invocation did, so resume and re-run item agree with a fresh run.
738
750
 
739
751
  // What a file *is* to a run, the same registry §4 gives the graded set: an
740
- // image is prepared to the profile's pixel budget; a text file is decoded
741
- // UTF-8 and joined at {text}; anything else is passed through as UTF-8 with
742
- // replacement and flagged, so the failure mode is stated rather than hidden.
752
+ // image is prepared to the profile's pixel budget; a document is sent whole;
753
+ // a text file is decoded UTF-8 and joined at {text}; anything else is passed
754
+ // through as UTF-8 with replacement and flagged, so the failure mode is
755
+ // stated rather than hidden.
743
756
  const RUN_IMAGE_TYPES = new Set([".jpg", ".jpeg", ".png", ".heic", ".heif", ".dng", ".tif", ".tiff"]);
744
757
  const RUN_TEXT_TYPES = new Set([".txt", ".md", ".csv"]);
745
758
 
@@ -916,6 +929,7 @@ async function runSnapshot(o) {
916
929
  : files.map(name => {
917
930
  const ext = path.extname(name).toLowerCase();
918
931
  if (RUN_IMAGE_TYPES.has(ext)) return { name, kind: "image" };
932
+ if (core.documentTypeOf(name)) return { name, kind: "document" };
919
933
  // A record (flows/record.ts): one call of a Power Automate step, what
920
934
  // an HTTP Request is rebuilt from.
921
935
  if (ext === ".json") return { name, kind: "record" };
@@ -981,6 +995,11 @@ async function runSnapshot(o) {
981
995
  break;
982
996
  }
983
997
  }
998
+ if (item.kind === "document") {
999
+ const file = path.join(o.source, item.name);
1000
+ if (!fs.existsSync(file)) { failed = `the file ${item.name} is not in the source`; break; }
1001
+ dataUrl = documentOf(file);
1002
+ }
984
1003
  let record = null;
985
1004
  if (item.kind === "record") {
986
1005
  const file = path.join(o.source, item.name);
@@ -1450,7 +1469,7 @@ async function main() {
1450
1469
  found: s.found, missed: s.missed, invented: s.invented, count: s.count,
1451
1470
  metrics: s.metrics, watch: s.watch,
1452
1471
  terms: res.terms || [], ms: res.ms,
1453
- // What a file type was to this run -- "image", "text", "unmapped".
1472
+ // What a file type was to this run -- "image", "document", "text", "unmapped".
1454
1473
  type: item?.kind ?? null,
1455
1474
  // What the model actually said. #248 wants a failure written out as a
1456
1475
  // task an agent can act on, and "missed water" without the reply that
package/lab/server.py CHANGED
@@ -50,6 +50,7 @@ from pathlib import Path
50
50
  # Ollama's own address on this machine, unless OLLAMA_URL names another: the
51
51
  # deployed lab's is set in the Dockerfile, and no default names a host of its.
52
52
  OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
53
+
53
54
  # The password in front of everything but /healthz (#463). Unset means open,
54
55
  # as a lab run on a workstation always has been; the deployed one sets it,
55
56
  # because it keeps setups and API keys and can spend GPU time.
@@ -145,6 +146,16 @@ def host_of(url: str) -> str:
145
146
  return (urllib.parse.urlsplit(url).hostname or "").lower()
146
147
 
147
148
 
149
+ def ollama_shown():
150
+ """OLLAMA as the page may show it, where a connection with no address
151
+ goes: no user or password, should OLLAMA_URL carry one."""
152
+ try:
153
+ u = urllib.parse.urlsplit(OLLAMA)
154
+ return urllib.parse.urlunsplit(u._replace(netloc=u.netloc.rpartition("@")[2]))
155
+ except ValueError:
156
+ return ""
157
+
158
+
148
159
  # The chat endpoint each connection type's request reaches, off the /v1
149
160
  # base -- evals-core.ts's CONNECTION_TYPES, mirrored because the server is
150
161
  # stdlib-only and cannot load it. Ollama's native /api/chat sits at the root;
@@ -311,6 +322,8 @@ FILE_TYPES = {
311
322
  ".dng": "image/x-adobe-dng",
312
323
  ".tif": "image/tiff",
313
324
  ".tiff": "image/tiff",
325
+ # Handed to the model whole, as a document (evals-core.ts DOCUMENT_TYPES).
326
+ ".pdf": "application/pdf",
314
327
  ".txt": "text/plain; charset=utf-8",
315
328
  ".md": "text/markdown; charset=utf-8",
316
329
  ".csv": "text/csv; charset=utf-8",
@@ -1212,9 +1225,20 @@ def workspace_of(store) -> str:
1212
1225
 
1213
1226
 
1214
1227
  # The `connection_shares` sentinels (phase 4 fills and enforces the table;
1215
- # phase 1 only creates it empty): every workspace, and future ones.
1228
+ # phase 1 only creates it empty): every workspace, future ones, and none.
1229
+ # No rows is All, so a set that loses its last workspace (a purge, the Share
1230
+ # link's Undo, a stale id) holds SHARE_NONE rather than nothing (#506).
1216
1231
  SHARE_ALL = "*all"
1217
1232
  SHARE_NEW = "*new"
1233
+ SHARE_NONE = "*none"
1234
+
1235
+ def hold_nowhere(db, conns) -> None:
1236
+ """SHARE_NONE for each (kind, id) in [conns] left with no share row, which
1237
+ would otherwise read as shared with every workspace."""
1238
+ db.executemany("INSERT INTO connection_shares (kind, id, workspace) SELECT ?, ?, ? "
1239
+ "WHERE NOT EXISTS (SELECT 1 FROM connection_shares WHERE kind = ? AND id = ?)",
1240
+ [(k, i, SHARE_NONE, k, i) for k, i in conns])
1241
+
1218
1242
 
1219
1243
  # How the `docs` table scopes by key (docs/workspaces.md): workflows are
1220
1244
  # per-workspace, profiles/tokens global, and promptlab.versions is split --
@@ -1519,7 +1543,8 @@ class Store:
1519
1543
  # An AI Connection or an Account (kind "profile"/"account", id the
1520
1544
  # connection's -- the Target profile's it kept -- or the account's) is
1521
1545
  # usable in a workspace by its share set in `connection_shares`: a row for
1522
- # that workspace, or the SHARE_ALL sentinel.
1546
+ # that workspace, or the SHARE_ALL sentinel. SHARE_NONE is a row that
1547
+ # matches no workspace, so a set holding only it is usable nowhere.
1523
1548
  # No row at all is the migration and new-connection default -- shared with
1524
1549
  # every workspace, so nothing stops running the moment workspaces exist
1525
1550
  # (open question 2, resolved All); narrowing a connection is adding the
@@ -1540,7 +1565,7 @@ class Store:
1540
1565
  """Every connection's share set, for the Connections menus: keyed by
1541
1566
  "<kind>:<id>", the workspace values as stored (workspace ids and the
1542
1567
  sentinels). A connection with no row is absent, which the page reads as
1543
- shared with All. No key is anywhere in this (#258)."""
1568
+ shared with All; one with SHARE_NONE is shared with no workspace. No key is anywhere in this (#258)."""
1544
1569
  out: dict = {}
1545
1570
  with self.lock, closing(sqlite3.connect(self.path)) as db:
1546
1571
  for kind, cid, ws in db.execute("SELECT kind, id, workspace FROM connection_shares"):
@@ -1548,15 +1573,20 @@ class Store:
1548
1573
  return out
1549
1574
 
1550
1575
  def set_shares(self, kind: str, cid: str, workspaces) -> None:
1551
- """Replace a connection's share set. SHARE_ALL means every workspace,
1552
- so it is kept alone; only the sentinels and live workspace ids are let
1553
- in, so a stale id cannot linger in the table."""
1576
+ """Replace a connection's share set. SHARE_ALL means every workspace
1577
+ and SHARE_NONE none, so each is kept alone; only the sentinels and live
1578
+ workspace ids are let in, so a stale id cannot linger in the table. A
1579
+ set named but left empty by that is SHARE_NONE, not the All an empty
1580
+ set reads as; an empty set asked for is a plain delete, so a failed
1581
+ lab import's rollback leaves no row for a connection it never wrote."""
1554
1582
  with self.lock, closing(sqlite3.connect(self.path)) as db, db:
1555
1583
  valid = {r[0] for r in db.execute("SELECT id FROM workspaces WHERE trash IS NULL")}
1556
1584
  chosen = [w for w in dict.fromkeys(workspaces or [])
1557
- if w in (SHARE_ALL, SHARE_NEW) or w in valid]
1585
+ if w in (SHARE_ALL, SHARE_NEW, SHARE_NONE) or w in valid]
1558
1586
  if SHARE_ALL in chosen:
1559
1587
  chosen = [SHARE_ALL]
1588
+ elif SHARE_NONE in chosen or (workspaces and not chosen):
1589
+ chosen = [SHARE_NONE]
1560
1590
  db.execute("DELETE FROM connection_shares WHERE kind = ? AND id = ?", (kind, cid))
1561
1591
  db.executemany("INSERT INTO connection_shares (kind, id, workspace) VALUES (?, ?, ?)",
1562
1592
  [(kind, cid, w) for w in chosen])
@@ -1565,14 +1595,18 @@ class Store:
1565
1595
  """Add or remove one workspace from a connection's share set -- the Run
1566
1596
  bar's Share link and its Undo (docs/workspaces.md). The link is offered
1567
1597
  only where the connection is actually narrowed, so adding a row is what
1568
- grants the blocked workspace its use and Undo takes it back."""
1598
+ grants the blocked workspace its use and Undo takes it back -- to
1599
+ SHARE_NONE when it was the last, as a set shared nowhere was."""
1569
1600
  with self.lock, closing(sqlite3.connect(self.path)) as db, db:
1570
1601
  if on:
1571
1602
  db.execute("INSERT OR IGNORE INTO connection_shares (kind, id, workspace) "
1572
1603
  "VALUES (?, ?, ?)", (kind, cid, ws))
1604
+ db.execute("DELETE FROM connection_shares WHERE kind = ? AND id = ? AND workspace = ?",
1605
+ (kind, cid, SHARE_NONE))
1573
1606
  else:
1574
1607
  db.execute("DELETE FROM connection_shares WHERE kind = ? AND id = ? AND workspace = ?",
1575
1608
  (kind, cid, ws))
1609
+ hold_nowhere(db, [(kind, cid)])
1576
1610
 
1577
1611
  def workspace_name(self, ws) -> str:
1578
1612
  """A workspace's name, for the refusal sentence the Run bar shows."""
@@ -1702,8 +1736,11 @@ class Workspaces:
1702
1736
  if isinstance(shares, list)
1703
1737
  else list(db.execute("SELECT kind, id FROM connection_shares WHERE workspace = ?",
1704
1738
  (SHARE_NEW,))))
1739
+ grant = [(k, i) for k, i in grant if k and i]
1705
1740
  db.executemany("INSERT OR IGNORE INTO connection_shares (kind, id, workspace) VALUES (?, ?, ?)",
1706
- [(k, i, wid) for k, i in grant if k and i])
1741
+ [(k, i, wid) for k, i in grant])
1742
+ db.executemany("DELETE FROM connection_shares WHERE kind = ? AND id = ? AND workspace = ?",
1743
+ [(k, i, SHARE_NONE) for k, i in grant])
1707
1744
  return wid, None
1708
1745
 
1709
1746
  def rename(self, wid, name) -> tuple:
@@ -1843,7 +1880,11 @@ class Workspaces:
1843
1880
  db.execute("DELETE FROM queue WHERE workspace = ?", (ws,))
1844
1881
  db.execute("DELETE FROM runs WHERE workspace = ?", (ws,))
1845
1882
  db.execute("DELETE FROM docs WHERE workspace = ?", (ws,))
1883
+ # A connection shared only here is shared nowhere after, not with
1884
+ # All, which is what no rows would read as (#506).
1885
+ held = list(db.execute("SELECT kind, id FROM connection_shares WHERE workspace = ?", (ws,)))
1846
1886
  db.execute("DELETE FROM connection_shares WHERE workspace = ?", (ws,))
1887
+ hold_nowhere(db, held)
1847
1888
  db.execute("DELETE FROM workspaces WHERE id = ?", (ws,))
1848
1889
  return sids, rids
1849
1890
 
@@ -4411,8 +4452,8 @@ class Datasets:
4411
4452
 
4412
4453
  def save(self, did, version, body):
4413
4454
  """The body, written at [version] -- the one it began from. Returns
4414
- ({version}, None), or (None, error); a stale version's error carries
4415
- the current row."""
4455
+ ({version, versions}, None), or (None, error); a stale version's error
4456
+ carries the current row."""
4416
4457
  if type(version) is not int:
4417
4458
  return None, (400, "a save names the version it began from")
4418
4459
  # A body of an earlier version -- from a page loaded before this one --
@@ -4431,7 +4472,9 @@ class Datasets:
4431
4472
  self._keep(db, r, text)
4432
4473
  db.execute("UPDATE datasets SET body = ?, version = ?, updated_at = ? WHERE id = ?",
4433
4474
  (text, version + 1, self._now(), did))
4434
- return {"version": version + 1}, None
4475
+ # The save may have cut a version: a link pins the newest (#547).
4476
+ newest = self._head(db, did)["n"]
4477
+ return {"version": version + 1, "versions": newest}, None
4435
4478
 
4436
4479
  def remove(self, did):
4437
4480
  """Into the trash, at once; Undo restores it. Returns (token, None)."""
@@ -4863,8 +4906,8 @@ class FilterSets:
4863
4906
 
4864
4907
  def save(self, fid, version, body):
4865
4908
  """The body, written at [version] -- the one it began from. Returns
4866
- ({version}, None), or (None, error); a stale version's error carries
4867
- the current row."""
4909
+ ({version, versions}, None), or (None, error); a stale version's error
4910
+ carries the current row."""
4868
4911
  if type(version) is not int:
4869
4912
  return None, (400, "a save names the version it began from")
4870
4913
  body = upgrade_filter_set_body(body)
@@ -4881,7 +4924,9 @@ class FilterSets:
4881
4924
  self._keep(db, r, text)
4882
4925
  db.execute("UPDATE filter_sets SET body = ?, version = ?, updated_at = ? WHERE id = ?",
4883
4926
  (text, version + 1, self._now(), fid))
4884
- return {"version": version + 1}, None
4927
+ # The save may have cut a version: a link pins the newest (#547).
4928
+ newest = self._head(db, fid)["n"]
4929
+ return {"version": version + 1, "versions": newest}, None
4885
4930
 
4886
4931
  def remove(self, fid):
4887
4932
  """Into the trash, at once; Undo restores it. Returns (token, None)."""
@@ -5141,6 +5186,43 @@ def demo_pack() -> bytes:
5141
5186
  return buf.getvalue()
5142
5187
 
5143
5188
 
5189
+ # The connection types a prompt cannot be asked of (evals-core.ts: their
5190
+ # `answers` lacks "prompt", or they answer from the item and are no model).
5191
+ NO_WORDS = {"echo", "recorded", "http"}
5192
+
5193
+
5194
+ def pack_connection(ws):
5195
+ """The AI Connection a pack's target answers on where it names one this
5196
+ lab lacks (#550): the lab's grader, or else the first connection that is
5197
+ asked words, either shared with workspace [ws]; None where there is none,
5198
+ so the target keeps its placeholder and the Run bar says to add one."""
5199
+ body = ((STORE.all().get("promptlab.connections") or {}).get("body") or {}) if STORE is not None else {}
5200
+ words = [c for c in body.get("list") or []
5201
+ if isinstance(c, dict) and isinstance(c.get("id"), str) and c["id"]
5202
+ and c.get("type") not in NO_WORDS and STORE.shared("profile", c["id"], ws)]
5203
+ return next((c for c in words if c["id"] == body.get("grader")), words[0] if words else None)
5204
+
5205
+
5206
+ def repoint_targets(doc: dict, ws) -> None:
5207
+ """A pack pipeline's targets that name a connection this lab does not have,
5208
+ pointed at the one pack_connection picks, its profile dropped with it."""
5209
+ targets = doc.get("targets")
5210
+ if not isinstance(targets, list) or STORE is None:
5211
+ return
5212
+ have = {c.get("id") for c in ((STORE.all().get("promptlab.connections") or {}).get("body") or {}).get("list") or []
5213
+ if isinstance(c, dict)}
5214
+ pick = None
5215
+ for t in targets:
5216
+ ref = t.get("connection") if isinstance(t, dict) else None
5217
+ if not isinstance(ref, dict) or ref.get("id") in have:
5218
+ continue
5219
+ pick = pick or pack_connection(ws)
5220
+ if pick is None:
5221
+ return
5222
+ t["connection"] = {"id": pick["id"], "name": pick.get("name") or pick["id"]}
5223
+ t["profile"] = None
5224
+
5225
+
5144
5226
  def pack_requires_problem(manifest: dict, plugins: set) -> str:
5145
5227
  """Why this lab cannot take a pack its manifest describes, or ""."""
5146
5228
  req = manifest.get("requires") or {}
@@ -5533,6 +5615,7 @@ class Packs:
5533
5615
  sid = src_ids.get(content["ref"].get("id")) or src_ids.get(content["ref"].get("name"))
5534
5616
  if sid:
5535
5617
  content["ref"] = {"id": sid, "name": SOURCES.get(sid)["name"]}
5618
+ repoint_targets(doc, ws)
5536
5619
  new_work.append((key, doc))
5537
5620
  def place(body):
5538
5621
  body.setdefault("list", [])
@@ -7518,10 +7601,12 @@ CONNECTION_TYPES = {
7518
7601
  "presencePenalty", "dryMultiplier", "dryBase", "dryAllowedLength", "grammar",
7519
7602
  "imagePosition", "cachePrompt", "thinking", "rejectLoops"),
7520
7603
  }
7521
- # Types that answer from the item and send no request (Echo, #97): no
7522
- # address to reach, no key to carry, so the destination rules have nothing to
7523
- # say about them. A plugin's type joins with `"local": true`.
7524
- LOCAL_CONNECTIONS = {"echo"}
7604
+ # Types that answer from the item and send no request (Echo, #97; Recorded,
7605
+ # #299): no address to reach, no key to carry, so the destination rules have
7606
+ # nothing to say about them. A target step stands in for each now, so a stored
7607
+ # row of one is never exported (lab_export). A plugin's type joins with
7608
+ # `"local": true`.
7609
+ LOCAL_CONNECTIONS = {"echo", "recorded"}
7525
7610
  # The built-in types, kept apart so a plugin's can be taken away again.
7526
7611
  BUILTIN_CONNECTION_TYPES = dict(CONNECTION_TYPES)
7527
7612
  BUILTIN_LOCAL = set(LOCAL_CONNECTIONS)
@@ -7967,6 +8052,11 @@ class NoRedirects(urllib.request.HTTPRedirectHandler):
7967
8052
  # -- a host nobody put on the list -- and the relay reported 200. An
7968
8053
  # allow-list that an environment variable can route around is not one.
7969
8054
  OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirects)
8055
+ # urllib's own "Python-urllib/3.x" is refused outright by many public APIs
8056
+ # behind Cloudflare (#512), so every outbound request names the lab instead --
8057
+ # honestly, never a browser. A request's own User-Agent, such as an API call
8058
+ # Source's Headers, wins: urllib adds this only where none is set.
8059
+ OPENER.addheaders = [("User-Agent", "evals-lab")]
7970
8060
 
7971
8061
 
7972
8062
  class Handler(BaseHTTPRequestHandler):
@@ -8158,7 +8248,8 @@ class Handler(BaseHTTPRequestHandler):
8158
8248
  # its own, and only in a lab with a store to keep it in.
8159
8249
  return self._json(200, {"targetCap": TARGET_CAP, "microsoft": microsoft_config(),
8160
8250
  "microsoftEditable": MICROSOFT is not None and not M365_CLIENT_ID,
8161
- "dynamicProviders": dynamic_providers_config()})
8251
+ "dynamicProviders": dynamic_providers_config(),
8252
+ "ollama": ollama_shown()})
8162
8253
  if path == "/api/state":
8163
8254
  if STORE is None:
8164
8255
  return self._send(404, b"not found", "text/plain")
@@ -8970,7 +9061,11 @@ class Handler(BaseHTTPRequestHandler):
8970
9061
  if err:
8971
9062
  return self._json(err[0], {"error": err[1]})
8972
9063
  content["live"] = True
8973
- content["files"] = [name]
9064
+ # The one capture, looped as a snapshot's files are
9065
+ # (evals-core.ts expand): Run first cannot cut below
9066
+ # one item, and Loop repeats it for reply variance (#513).
9067
+ loops = content.get("loops")
9068
+ content["files"] = [name] * (loops if type(loops) is int and loops >= 1 else 1)
8974
9069
  content["revs"] = {name: "live:" + hashlib.sha256(
8975
9070
  produced[name].encode("utf-8")).hexdigest()}
8976
9071
  else:
@@ -9702,8 +9797,10 @@ def lab_export(only=None) -> dict:
9702
9797
  the profiles over them (or over none) and the grader if it is one of them,
9703
9798
  each share set narrowed to that workspace, the one the file names, and
9704
9799
  *new kept where the connection has it. A connection shared only with
9705
- workspaces in the trash is usable nowhere, and an empty share set would
9706
- import as All, so it is left out, and out as the grader too."""
9800
+ workspaces in the trash, or with none, is usable nowhere, and an empty share set would
9801
+ import as All, so it is left out, and out as the grader too. So is a
9802
+ connection of a local type (Echo): a target step stands in for it, and the
9803
+ lab it lands in offers that step already."""
9707
9804
  body = (STORE.all().get("promptlab.connections") or {}).get("body") or {}
9708
9805
  profiles = (STORE.all().get("promptlab.connectionProfiles") or {}).get("body") or {}
9709
9806
  every = WORKSPACES.list()
@@ -9716,6 +9813,9 @@ def lab_export(only=None) -> dict:
9716
9813
  for c in body.get("list") or []:
9717
9814
  if not isinstance(c, dict) or (only and not STORE.shared("profile", str(c.get("id") or ""), spaces[0]["id"])):
9718
9815
  continue
9816
+ if c.get("type") in LOCAL_CONNECTIONS:
9817
+ nowhere.add(c.get("id"))
9818
+ continue
9719
9819
  entry = {k: v for k, v in c.items() if k not in ("key", "shares")}
9720
9820
  held = rows.get(f"profile:{c.get('id')}")
9721
9821
  if held and SHARE_ALL not in held:
@@ -9827,6 +9927,11 @@ def lab_problems(doc) -> str:
9827
9927
  if not c["shares"]:
9828
9928
  return (f"connection {c['id']}'s shares is empty; leave shares out for All, "
9829
9929
  "or name a workspace or *new")
9930
+ # *none is the store's own mark for a set that lost its last workspace;
9931
+ # an export leaves such a connection out, so a file never carries it.
9932
+ if SHARE_NONE in c["shares"]:
9933
+ return (f"connection {c['id']}'s shares names *none; a lab file holds no connection "
9934
+ "shared with no workspace -- leave it out, or name a workspace or *new")
9830
9935
  lost = [s for s in c["shares"] if s not in (SHARE_ALL, SHARE_NEW) and s not in known]
9831
9936
  if lost:
9832
9937
  return f"connection {c['id']} is shared with {', '.join(lost)}, which neither the file nor this lab has"
@@ -9893,9 +9998,7 @@ def lab_import(doc, keys) -> tuple:
9893
9998
  lines.append(f"grader set to {doc['grader']}")
9894
9999
  # Only an added connection takes the file's share set: one the lab had
9895
10000
  # keeps its own, as everything else it had does. Set before the write,
9896
- # so the connection is never there and shared with All in between; a
9897
- # failed write puts back what the rows were, since its ids never landed.
9898
- before = STORE.shares()
10001
+ # so the connection is never there and shared with All in between.
9899
10002
  for c in added:
9900
10003
  if "shares" in c:
9901
10004
  STORE.set_shares("profile", c["id"], [s if s in (SHARE_ALL, SHARE_NEW) else wid_of[s]
@@ -9912,9 +10015,15 @@ def lab_import(doc, keys) -> tuple:
9912
10015
  ok, _ = STORE.write(writes, STORE.default_ws)
9913
10016
  if ok:
9914
10017
  break
10018
+ # A failed write takes back the rows of each id that still never
10019
+ # landed, as no rows at all (an empty set is a delete, never *none).
10020
+ # One another writer landed in between keeps the rows it has now:
10021
+ # taking them would leave it shared with All.
10022
+ landed = {x.get("id") for x in ((STORE.all().get("promptlab.connections") or {}).get("body")
10023
+ or {}).get("list") or [] if isinstance(x, dict)}
9915
10024
  for c in added:
9916
- if "shares" in c:
9917
- STORE.set_shares("profile", c["id"], before.get(f"profile:{c['id']}") or [])
10025
+ if "shares" in c and c["id"] not in landed:
10026
+ STORE.set_shares("profile", c["id"], [])
9918
10027
  else:
9919
10028
  return None, (409, "the lab's connections kept changing while they were imported; import again")
9920
10029
  said += lines
@@ -0,0 +1,5 @@
1
+ import{$t as e,An as t,Bt as n,Cn as r,Ct as i,Dt as a,En as o,Ft as s,Ht as c,It as l,Jt as u,Kt as d,Nt as f,Pt as p,Qt as m,St as ee,Tn as te,Tt as h,Ut as ne,Vt as g,Xt as _,Yt as v,_t as re,an as ie,at as ae,bn as oe,bt as se,cr as ce,fn as y,gn as le,gt as ue,kn as de,kt as b,l as fe,ln as pe,lt as x,mn as S,n as me,nn as C,on as he,pn as ge,qt as _e,s as w,sr as ve,tn as ye,vn as be,vt as T,wn as xe,wt as Se,xt as Ce,yn as we}from"./tokens-CrjDMCbz.js";var E=ce(),Te=ve(),D=[{id:`text`,label:`Plain text`,settings:[]},{id:`list`,label:`List`,settings:[{key:`parse`,label:`Parse as`,type:`select`,choices:[{value:`csv`,label:`CSV`},{value:`lines`,label:`One a line`},{value:`json`,label:`JSON array`}]}]}],O=[{id:`case`,label:`Case`,accepts:[`list`],options:[{key:`mode`,label:`Mode`,type:`select`,choices:[{value:`LOWER`,label:`lower`},{value:`SENTENCE`,label:`Sentence`},{value:`TITLE`,label:`Title`},{value:`UPPER`,label:`UPPER`},{value:`NONE`,label:`As replied`}]}],defaults:()=>({type:`case`,mode:`LOWER`})},{id:`join`,label:`Join`,accepts:[`list`],options:[{key:`with`,label:`Separator`,type:`text`}],defaults:()=>({type:`join`,with:`, `})}],Ee=[{name:`language`,type:`value`,value:`English UK`},{name:`geo-location`,type:`value`,value:`Hamilton, Waikato, New Zealand`},{name:`geo`,type:`block`,enabled:!1}],De=[{path:`group/open`,title:`Group, open`},{path:`group/closed`,title:`Group, closed with a note`},{path:`group/no-menu`,title:`Group without actions`},{path:`group/sub`,title:`Sub-group, level 3`},{path:`group/depth`,title:`Group, three levels deep`},{path:`group/renaming`,title:`Group, renaming`},{path:`menu/disabled`,title:`Menu with a disabled item`},{path:`row/button`,title:`Row actions, one action a button`},{path:`row/menu`,title:`Row actions, two or more in the menu`},{path:`actionbar/states`,title:`Action bar, docked under a header, in its four states`},{path:`tabs/main`,title:`Tab strip, the lab's tabs`},{path:`tabs/section`,title:`Tab strip, a section strip under the lab's`},{path:`header/pipeline`,title:`Context header, a Pipeline`},{path:`header/dataset`,title:`Context header, a Dataset`},{path:`header/source`,title:`Context header, a Source`},{path:`header/switcher`,title:`Switcher, recent and more`},{path:`header/switcher-detail`,title:`Switcher, with a detail`},{path:`picker/fields`,title:`Picker, a profile and a Source`},{path:`multi/closed`,title:`Multi-select, two of three ticked`},{path:`multi/open`,title:`Multi-select, its list open`},{path:`chiplist/words`,title:`Chip-list, words with the input to add more`},{path:`chiplist/empty`,title:`Chip-list, empty with its placeholder`},{path:`workspace/menu`,title:`Workspace trigger and its three-part menu`},{path:`copymove/choose`,title:`Copy or move, a destination chosen`},{path:`copymove/collision`,title:`Copy or move, the name is already taken`},{path:`copymove/confirm`,title:`Copy or move, a Move that breaks a dependency`},{path:`copymove/confirm-pipeline`,title:`Copy or move, a pipeline Move that leaves its data behind`},{path:`share/menu`,title:`Share menu, All / some / New workspaces`},{path:`header/long`,title:`Context header, a long name`},{path:`menu/busy`,title:`Menu with a busy item`},{path:`loading/group`,title:`Loading, inside a group`},{path:`loading/error`,title:`Loading failed, with Retry`},{path:`loading/busy-button`,title:`Busy buttons`},{path:`repeater/empty`,title:`Repeater, empty`},{path:`repeater/min`,title:`Repeater at min 1`},{path:`repeater/between`,title:`Repeater, two of four`},{path:`repeater/max`,title:`Repeater at max`},{path:`repeater/locked`,title:`Repeater, a locked row`},{path:`props/controls`,title:`Property table, every control`},{path:`props/required`,title:`Property table, required only`},{path:`props/long`,title:`Property table, long values`},{path:`props/cards`,title:`Property table, a kind chosen by card`},{path:`props/widths`,title:`Property table, a width per value`},{path:`props/affix`,title:`Property table, a fixed suffix and a field's error`},{path:`props/secret`,title:`Property table, a key held and not held`},{path:`props/code`,title:`Property table, an endpoint and a payload`},{path:`table/sortable`,title:`Table, sortable headers and a row menu`},{path:`table/paged`,title:`Table, paged ten at a time`},{path:`table/select`,title:`DataTable with selection`},{path:`table/select-on`,title:`Table, two rows selected`},{path:`table/filter`,title:`Table with a filter bar`},{path:`table/filter-on`,title:`Table, filtered, with a scope`},{path:`edit/form`,title:`Edit modal, a profile`},{path:`edit/plain`,title:`Edit modal, one section and no menu`},{path:`edit/long`,title:`Edit modal, a form that scrolls`},{path:`edit/status`,title:`Edit modal, a status that is a refusal`},{path:`view/tokens`,title:`Token mappings`},{path:`view/output`,title:`Output with Case`},{path:`view/output-text`,title:`Output, plain text`},{path:`view/output-two`,title:`Output with two modifiers`},{path:`view/output-twice`,title:`Output with the same modifier twice`},{path:`view/output-none`,title:`Responses, read as text`},{path:`view/output-flow`,title:`Responses with Read as`},{path:`view/field-chip`,title:`Field chips in a target's words, with a changed word`},{path:`view/field-edit`,title:`The Edited target's editable prompt, with Insert field and a highlighted diff`},{path:`view/insert-field`,title:`The Edited target's Insert field menu, open`},{path:`wizard/pill`,title:`Wizard pill, two of four steps done`},{path:`wizard/pill-open`,title:`Wizard pill, its checklist open on the current step`},{path:`switch/off`,title:`Switch, off`},{path:`switch/on`,title:`Switch, on`},{path:`switch/disabled`,title:`Switch, disabled`}],k=e(),A=()=>{},j=(...e)=>e.map(e=>({label:e,icon:e===`Remove`?m.remove:e===`Clone`?m.clone:m.rename,onSelect:A,destructive:e===`Remove`}));function M({start:e,min:t,max:n,locked:r}){let[i,a]=(0,E.useState)(e);return(0,k.jsx)(re,{noun:`scenario`,items:i,...t===void 0?{}:{min:t},...n===void 0?{}:{max:n},...r?{locked:r}:{},onAdd:()=>a([...i,`Target ${String.fromCharCode(65+i.length)}`]),onRemove:e=>a(i.filter((t,n)=>n!==e)),onClone:e=>a([...i.slice(0,e+1),`${i[e]} copy`,...i.slice(e+1)]),onRename:A,empty:`No targets yet.`,renderItem:(e,t,n)=>(0,k.jsx)(d,{id:`scenario-${t}`,title:e,level:3,actions:n,children:(0,k.jsx)(`p`,{className:`note`,children:`A connection, a profile and a prompt for each job.`})},`${e}-${t}`)})}function Oe(){let[e,t]=(0,E.useState)(Ee),n=(n,r)=>t(e.map(e=>e.name===n?{...e,...r}:e));return(0,k.jsx)(fe,{mappings:e,onValue:(e,t)=>n(e,{value:t}),onEnabled:(e,t)=>n(e,{enabled:t}),onEdit:A,onRemove:n=>t(e.filter(e=>e.name!==n)),onAdd:()=>t([...e,{name:`token${e.length+1}`,type:`value`,value:``}])})}function N({start:e,format:t=!0,readAs:n}){let[r,i]=(0,E.useState)(e),[a,o]=(0,E.useState)(t),[s,c]=(0,E.useState)(n);return(0,k.jsx)(ae,{id:`output`,out:r,format:a,readAs:s,outputKinds:D,modifiers:O,onFormat:e=>{o(e!=null);let t=e??`text`;i({kind:t,modifiers:r.modifiers.filter(e=>O.find(t=>t.id===e.type)?.accepts.includes(t))})},onReadAs:c,onModifier:(e,t)=>i({...r,modifiers:r.modifiers.map((n,r)=>r===e?t:n)}),onAddModifier:e=>i({...r,modifiers:[...r.modifiers,e]}),onRemoveModifier:e=>i({...r,modifiers:r.modifiers.filter((t,n)=>n!==e)})})}function ke(){let[e,t]=(0,E.useState)(`Job 1`),[n,r]=(0,E.useState)(!0);return(0,k.jsx)(d,{id:`renaming`,title:e,actions:[{label:`Rename…`,icon:m.rename,onSelect:()=>r(!0)}],...n?{rename:{value:e,onCommit:n=>{t(n||e),r(!1)},onCancel:()=>r(!1)}}:{},children:(0,k.jsx)(`p`,{className:`note`,children:`Enter or leaving keeps the name; Escape cancels.`})})}function Ae(){let[e,t]=(0,E.useState)(`system`);return(0,k.jsx)(`div`,{className:`gallery-row`,style:{justifyContent:`flex-end`},children:(0,k.jsx)(i,{current:{id:`w1`,slug:`work`,name:`Work`},workspaces:[{id:`w1`,slug:`work`,name:`Work`},{id:`w2`,slug:`side-project`,name:`Side project`},{id:`w3`,slug:`client-demos`,name:`Client demos`}],onSwitch:A,onNew:A,onManage:A,theme:e,onTheme:t})})}var je=[{id:`cur`,slug:`work`,name:`Work`,archived:!1,isDefault:!0,lastUsed:`2026-10-08T09:30:00Z`,createdAt:null,pipelines:4,runs:18},{id:`side`,slug:`side-project`,name:`Side project`,archived:!1,isDefault:!1,lastUsed:`2026-10-07T09:30:00Z`,createdAt:null,pipelines:2,runs:6},{id:`demos`,slug:`client-demos`,name:`Client demos`,archived:!1,isDefault:!1,lastUsed:`2026-10-06T09:30:00Z`,createdAt:null,pipelines:1,runs:0}],Me={id:`cur`,slug:`work`,name:`Work`},Ne={undo:null,offer:A,undoNow:A,dismiss:A},Pe=e=>({copyMovePreflight:async()=>({nameTaken:e,suggestedName:`Daily regressions (copy)`}),copyAsset:async()=>({id:`copy`}),moveAsset:async()=>({id:`same`}),forWorkspace:()=>({})});function P({nameTaken:e=!1,confirm:t=!1,subject:n,dependents:r=[]}){return(0,k.jsx)(ue,{api:Pe(e),subject:n??{kind:`filter-set`,id:`f1`,name:`Daily regressions`},workspaces:je,current:Me,dependents:r,undo:Ne,onClose:A,defaultDest:`side`,...t?{defaultConfirm:!0}:{}})}function Fe(){let e=[{id:`w1`,name:`Work`},{id:`w2`,name:`Side project`},{id:`w3`,name:`Client demos`}];return(0,k.jsx)(`div`,{style:{display:`grid`,gap:`var(--s3)`,maxWidth:`20rem`},children:[{label:`All workspaces`,shares:void 0},{label:`Some workspaces`,shares:[`w2`]},{label:`New workspaces`,shares:[se]},{label:`No workspaces`,shares:[Ce]}].map(t=>(0,k.jsx)(ee,{label:t.label,workspaces:e,shares:t.shares,onChange:A},t.label))})}function F({sections:e,menu:t,status:n,statusBad:r,outcome:i}){return(0,k.jsx)(f,{title:`Lab Ollama`,...n?{status:n}:{},...r?{statusBad:!0}:{},...t?{menu:t}:{},sections:e,actions:[{label:`Test connection`,icon:ge,onSelect:A,outcome:i?{label:i,onOpen:A}:void 0}],onClose:A})}var I=()=>[{key:`name`,label:`Name`,width:`full`,required:!0,control:{type:`text`,value:`Lab Ollama`,onChange:A}},{key:`slug`,label:`Slug`,width:`full`,required:!0,control:{type:`text`,value:`lab-ollama`,onChange:A}},{key:`type`,label:`Type`,required:!0,control:{type:`select`,value:`ollama`,onChange:A,options:[{value:`ollama`,label:`Ollama (Local)`},{value:`anthropic`,label:`Anthropic`}]}},{key:`url`,label:`Address`,width:`full`,control:{type:`text`,value:``,placeholder:`http://127.0.0.1:11434`,onChange:A}},{key:`key`,label:`Key`,control:{type:`secret`,value:``,onChange:A}},{key:`model`,label:`Model`,width:`full`,control:{type:`text`,value:`minicpm-v4.6:latest`,onChange:A}}],L=()=>[{key:`seed`,label:`Seed`,control:{type:`text`,value:``,placeholder:`1234`,onChange:A}},{key:`nPredict`,label:`Reply tokens`,width:`quarter`,control:{type:`text`,value:``,onChange:A}},{key:`temperature`,label:`Temperature`,control:{type:`text`,value:`0.3`,onChange:A}},{key:`repeatPenalty`,label:`Repeat penalty`,width:`quarter`,control:{type:`text`,value:``,onChange:A}}],R=()=>[{key:`endpoint`,label:`Endpoint`,required:!0,control:{type:`code`,text:`http://127.0.0.1:11434/api/chat`}},{key:`prompt`,label:`Prompt`,required:!0,control:{type:`static`,text:`(prompt)`}},{key:`model`,label:`model`,control:{type:`code`,text:`minicpm-v4.6:latest`}},{key:`options`,label:`options`,control:{type:`code`,text:`{"seed":1234,"temperature":0.3}`}}],z={key:`kind`,label:`Response Format`,required:!0,control:{type:`select`,value:`list`,onChange:A,options:D.map(e=>({value:e.id,label:e.label}))}};function B({actions:e}){return(0,k.jsx)(d,{id:`g`,title:`Actions`,children:(0,k.jsx)(b,{label:`Actions`,rows:[{name:`Ask_Claude`}],rowKey:e=>e.name,columns:[{key:`name`,header:`Name`,role:`title`,cell:e=>e.name},{key:`menu`,header:``,role:`menu`,sort:null,cell:()=>(0,k.jsx)(_e,{label:`Ask_Claude actions`,actions:e})}]})})}var Ie=[[`home`,`Home`,we],[`runs`,`Runs`,te],[`history`,`History`,oe],[`library`,`Library`,be],[`setup`,`Setup`,pe]],Le=[[`datasets`,`Datasets`,t],[`prompts`,`Prompts`,le],[`sources`,`Sources`,xe]],V=e=>(0,k.jsx)(g,{label:`Lab`,current:e,items:Ie.map(([e,t,n])=>({key:e,label:t,icon:n,href:`#/${e}`}))}),H=e=>(0,k.jsx)(g,{label:`Library`,level:`section`,current:e,items:Le.map(([e,t,n])=>({key:e,label:t,icon:n,href:`#/library/${e}`}))}),U=({section:e,children:t})=>(0,k.jsxs)(`div`,{children:[V(e?`library`:`runs`),e&&H(e),t]}),W=(...e)=>e.map(e=>({label:e,icon:e===`Delete`?m.remove:e===`Clone`?m.clone:m.rename,onSelect:A,destructive:e===`Delete`})),Re={"header/pipeline":()=>(0,k.jsx)(U,{children:(0,k.jsx)(n,{kind:`Pipeline`,name:`Pipeline 1`,onPick:A,meta:`2 jobs, 3 targets`,primary:{label:`New pipeline`,icon:y,onSelect:A},actions:W(`Rename…`,`Clone`,`Export YAML`,`Import YAML…`,`Restore…`,`Delete`)})}),"header/dataset":()=>(0,k.jsx)(U,{section:`datasets`,children:(0,k.jsx)(n,{kind:`Dataset`,name:`Demo 1`,onPick:A,meta:`62 cases, 19 graded`,primary:{label:`New dataset`,icon:y,onSelect:A},actions:W(`Rename…`,`Clone`,`Import JSON…`,`Export JSON`,`Restore…`,`Delete`)})}),"header/switcher":()=>(0,k.jsx)(h,{kind:`Pipeline`,value:`p5`,onPick:A,onClose:A,recent:[`p7`,`p6`,`p5`,`p4`,`p3`],options:Array.from({length:13},(e,t)=>({id:`p${t}`,label:[`Demo 1`,`Demo 2`,`Receipts`,`Menus`,`Street signs`,`Labels`,`Plates`,`Posters`,`Packaging`,`Whiteboards`,`Book spines`,`Shop fronts`,`Timetables`][t],used:t<8?{at:`2026-10-0${t+1}T09:30:00Z`,label:x(`2026-10-0${t+1}T09:30:00Z`)}:null}))}),"header/switcher-detail":()=>(0,k.jsx)(h,{kind:`Connection`,value:`c1`,onPick:A,onClose:A,detailLabel:`Model`,recent:[`c3`,`c1`],options:[[`Lab Ollama gemma3 4b`,`gemma3:4b`],[`Lab Ollama minicpm`,`minicpm-v`],[`Anthropic`,`claude-sonnet-4-5`],[`Dell llama`,`qwen2.5vl:7b`],[`OpenAI`,`gpt-5`],[`Docker llama`,`qwen2.5vl:7b`]].map(([e,t],n)=>({id:`c${n}`,label:e,detail:t,used:n<4?{at:`2026-10-1${n}T09:30:00Z`,label:x(`2026-10-1${n}T09:30:00Z`)}:null}))}),"picker/fields":()=>(0,k.jsx)(d,{id:`g`,title:`Target A`,children:(0,k.jsx)(p,{label:`Target A setup`,rows:[{key:`profile`,label:`Profile`,control:{type:`picker`,kind:`Profile`,value:`p2`,usedLabel:`Last changed`,onChange:A,recent:[`p5`,`p4`,`p3`,`p2`,`p1`],options:[{id:`#echo`,label:`Echo`,fixed:!0},...[`Lab Ollama`,`Anthropic`,`Cloud vision`,`Local llama`,`OpenRouter`,`Bench`,`Vision`].map((e,t)=>({id:`p${t}`,label:e,used:t<6?{at:`2026-10-0${t+1}T09:30:00Z`,label:x(`2026-10-0${t+1}T09:30:00Z`)}:null}))]}},{key:`source`,label:`Source`,control:{type:`picker`,kind:`Source`,value:`gone`,usedLabel:`Last changed`,onChange:A,options:[{id:``,label:`Any`,fixed:!0},{id:`gone`,label:`Shoot 3 (missing)`,missing:!0},{id:`s1`,label:`Receipts`}]}}]})}),"header/source":()=>(0,k.jsx)(U,{section:`sources`,children:(0,k.jsx)(n,{kind:`Source`,name:`Demo 1 images`,onPick:A,meta:`File Library. 63 files, 48 MB`,primary:{label:`Upload…`,icon:C,onSelect:A},actions:W(`Rename…`,`Delete`)})}),"workspace/menu":()=>(0,k.jsx)(Ae,{}),"copymove/choose":()=>(0,k.jsx)(P,{}),"copymove/collision":()=>(0,k.jsx)(P,{nameTaken:!0}),"copymove/confirm":()=>(0,k.jsx)(P,{confirm:!0,dependents:[{kind:`pipeline`,id:`w1`,name:`Nightly regression`,href:`#/runs`}]}),"copymove/confirm-pipeline":()=>(0,k.jsx)(P,{confirm:!0,subject:{kind:`pipeline`,id:`w1`,name:`Nightly regression`},dependents:[{kind:`eval-group`,id:`g1`,name:`Receipts, 62 cases`,href:`#/library/datasets/g1`},{kind:`source`,id:`s1`,name:`Receipt images`,href:`#/library/sources`}]}),"share/menu":()=>(0,k.jsx)(Fe,{}),"header/long":()=>(0,k.jsx)(U,{children:(0,k.jsx)(n,{kind:`Pipeline`,name:`Receipts and handwritten menus, second pass with the tighter prompt and a Contains metric`,onPick:A,meta:`12 jobs, 8 targets`,primary:{label:`New pipeline`,icon:y,onSelect:A},actions:W(`Rename…`,`Clone`,`Delete`)})}),"actionbar/states":()=>(0,k.jsxs)(`div`,{children:[V(`runs`),(0,k.jsx)(n,{kind:`Pipeline`,name:`Demo 1`,onPick:A,meta:`1 job, 2 targets`,docked:!0,primary:{label:`New pipeline`,icon:y,onSelect:A},actions:W(`Rename…`,`Delete`)}),(0,k.jsx)(T,{primary:{label:`Run`,icon:S,onSelect:A},status:(0,k.jsx)(`b`,{children:`Ready to run`})}),(0,k.jsxs)(`div`,{style:{display:`grid`,gap:`var(--s4)`,marginTop:`var(--s5)`},children:[(0,k.jsx)(T,{primary:{label:`Run`,icon:S,onSelect:A,disabled:!0},status:(0,k.jsxs)(k.Fragment,{children:[`Connection "Vision" not found. `,(0,k.jsxs)(`a`,{href:`#/setup`,style:{whiteSpace:`nowrap`},children:[`Open`,`\xA0`,`Setup`]})]})}),(0,k.jsx)(T,{primary:{label:`Run`,icon:S,onSelect:A,disabled:!0},status:(0,k.jsxs)(k.Fragment,{children:[(0,k.jsx)(`b`,{children:`Running`}),` 12 of 42`]}),secondary:{label:`Stop`,icon:he,onSelect:A},progress:28,progressLabel:`Run progress`}),(0,k.jsx)(T,{primary:{label:`Run`,icon:S,onSelect:A},status:(0,k.jsxs)(k.Fragment,{children:[(0,k.jsx)(`b`,{children:`Done`}),` 42 in 2m`]})})]})]}),"tabs/main":()=>V(`runs`),"tabs/section":()=>(0,k.jsxs)(`div`,{children:[V(`library`),H(`datasets`)]}),"group/open":()=>(0,k.jsx)(d,{id:`g`,title:`Evals`,actions:j(`Change…`,`Remove`),children:(0,k.jsx)(`p`,{className:`note`,children:`The graded set.`})}),"group/closed":()=>(0,k.jsx)(d,{id:`g`,title:`Content`,open:!1,note:`Sample library, 63 files`,actions:j(`Change…`,`Remove`)}),"group/no-menu":()=>(0,k.jsx)(d,{id:`g`,title:`Mapping`,children:(0,k.jsx)(`p`,{className:`note`,children:`Nothing to do to it.`})}),"group/sub":()=>(0,k.jsx)(d,{id:`g`,title:`Job 1`,actions:j(`Rename…`,`Remove`),children:(0,k.jsx)(d,{id:`g-sub`,title:`Scenarios`,level:3,actions:j(`Clear`),children:(0,k.jsx)(`p`,{className:`note`,children:`Inside a pane.`})})}),"group/depth":()=>(0,k.jsxs)(d,{id:`g`,title:`Job 1`,actions:j(`Rename…`,`Remove`),children:[(0,k.jsxs)(d,{id:`g-scenarios`,title:`Scenarios`,level:3,children:[(0,k.jsx)(d,{id:`g-plain`,title:`Plain`,level:3,actions:j(`Rename…`,`Remove`),children:(0,k.jsx)(`p`,{className:`note`,children:`A pane, a section of it, and a thing with a name inside that section.`})}),(0,k.jsx)(d,{id:`g-strict`,title:`Strict`,level:3,actions:j(`Rename…`,`Remove`)})]}),(0,k.jsx)(d,{id:`g-output`,title:`Output`,level:3,children:(0,k.jsx)(`p`,{className:`note`,children:`The next section of the same pane.`})})]}),"group/renaming":()=>(0,k.jsx)(ke,{}),"menu/disabled":()=>(0,k.jsx)(d,{id:`g`,title:`Target D`,actions:[{label:`Rename…`,icon:m.rename,onSelect:A},{label:`Clone`,icon:m.clone,onSelect:A,disabled:!0},{label:`Remove`,icon:m.remove,onSelect:A,destructive:!0}]}),"row/button":()=>(0,k.jsx)(B,{actions:[{label:`New pipeline`,icon:S,onSelect:A}]}),"row/menu":()=>(0,k.jsx)(B,{actions:[{label:`View…`,icon:o,onSelect:A},{label:`Download`,icon:de,onSelect:A},{label:`Remove`,icon:ie,onSelect:A,destructive:!0}]}),"menu/busy":()=>(0,k.jsx)(d,{id:`g`,title:`History`,actions:[{label:`Export JSON`,icon:m.export,onSelect:A,busy:!0},{label:`Clear`,icon:m.clear,onSelect:A,destructive:!0}]}),"loading/group":()=>(0,k.jsx)(d,{id:`g`,title:`History`,actions:j(`Rename…`),children:(0,k.jsx)(v,{label:`Runs`,delay:0})}),"loading/error":()=>(0,k.jsx)(d,{id:`g`,title:`History`,children:(0,k.jsx)(v,{label:`Runs`,error:`Runs not loaded: server unreachable.`,onRetry:A})}),"loading/busy-button":()=>(0,k.jsx)(d,{id:`g`,title:`Run`,children:(0,k.jsxs)(`div`,{className:`gallery-row`,children:[(0,k.jsxs)(`button`,{type:`button`,className:`run`,disabled:!0,"aria-busy":!0,children:[(0,k.jsx)(_,{size:16}),`Run`]}),(0,k.jsxs)(`button`,{type:`button`,className:`ghost`,disabled:!0,"aria-busy":!0,children:[(0,k.jsx)(_,{}),`Older`]})]})}),"repeater/empty":()=>(0,k.jsx)(M,{start:[]}),"repeater/min":()=>(0,k.jsx)(M,{start:[`Target A`],min:1}),"repeater/between":()=>(0,k.jsx)(M,{start:[`Target A`,`Target B`],min:1,max:4}),"repeater/max":()=>(0,k.jsx)(M,{start:[`Target A`,`Target B`,`Target C`,`Target D`],min:1,max:4}),"repeater/locked":()=>(0,k.jsx)(M,{start:[`Reply`,`Target A`],locked:(e,t)=>t===0}),"props/controls":()=>(0,k.jsx)(p,{label:`Controls`,onRename:A,onRemove:A,onAdd:A,addLabel:`Add row`,rows:[z,{key:`language`,label:`{language}`,code:!0,control:{type:`text`,value:`English UK`,onChange:A}},{key:`geo`,label:`{geo}…{/geo}`,code:!0,control:{type:`static`,text:`Block`},enabled:{checked:!0,onChange:A}},{key:`image`,label:`Image`,control:{type:`checkbox`,checked:!0,label:`Send`,onChange:A}}]}),"props/required":()=>(0,k.jsx)(p,{label:`Output`,onRemove:A,rows:[z]}),"props/long":()=>(0,k.jsx)(p,{label:`Long`,onRemove:A,rows:[{key:`geo-location`,label:`{geo-location}`,code:!0,control:{type:`text`,value:`Hamilton, Waikato, New Zealand, Southern Hemisphere, Earth`,onChange:A}},{key:`averylongtokennamethatdoesnotbreak`,label:`{averylongtokennamethatdoesnotbreak}`,code:!0,control:{type:`text`,value:``,onChange:A}}]}),"props/cards":()=>(0,k.jsx)(He,{}),"props/widths":()=>(0,k.jsx)(p,{label:`Widths`,rows:[{key:`type`,label:`Type`,required:!0,control:{type:`select`,value:`ollama`,onChange:A,options:[{value:`ollama`,label:`Ollama (Local)`},{value:`anthropic`,label:`Anthropic`}]}},{key:`url`,label:`Address`,width:`full`,control:{type:`text`,value:``,placeholder:`http://127.0.0.1:11434`,onChange:A}},{key:`model`,label:`Model`,width:`full`,control:{type:`text`,value:`minicpm-v4.6:latest`,onChange:A}},{key:`seed`,label:`Seed`,control:{type:`text`,value:``,placeholder:`1234`,onChange:A}},{key:`nPredict`,label:`Reply tokens`,width:`quarter`,control:{type:`text`,value:``,onChange:A}},{key:`temperature`,label:`Temperature`,control:{type:`text`,value:`0.2`,onChange:A}},{key:`grammar`,label:`Grammar`,control:{type:`textarea`,value:`root ::= item (", " item)*
2
+ item ::= [a-z ]+`,onChange:A}}]}),"props/affix":()=>(0,k.jsx)(p,{label:`Call`,rows:[{key:`name`,label:`Name`,control:{type:`text`,value:`open-meteo-forecast`,suffix:`.txt`,onChange:A}},{key:`taken`,label:`Name`,error:`open-meteo-forecast-storm.txt is taken.`,control:{type:`text`,value:`open-meteo-forecast-storm`,suffix:`.txt`,onChange:A}},{key:`status`,label:`Status`,width:`quarter`,control:{type:`text`,value:`200`,numeric:!0,onChange:A}},{key:`body`,label:`Body`,control:{type:`textarea`,mono:!0,value:`{
3
+ "category": "utilities"
4
+ }`,onChange:A}}]}),"props/secret":()=>(0,k.jsx)(p,{label:`Keys`,rows:[{key:`held`,label:`Key`,control:{type:`secret`,value:`sk-not-a-real-key-0000`,onChange:A}},{key:`empty`,label:`Key`,control:{type:`secret`,value:``,onChange:A}},{key:`server`,label:`Key`,control:{type:`secret`,value:``,held:!0,onChange:A}}]}),"props/code":()=>(0,k.jsx)(p,{label:`Debug`,rows:[{key:`endpoint`,label:`Endpoint`,required:!0,control:{type:`code`,text:`https://api.anthropic.com/v1/messages`}},{key:`prompt`,label:`Prompt`,required:!0,control:{type:`static`,text:`(prompt)`}},{key:`model`,label:`model`,control:{type:`code`,text:`claude-sonnet-4-5-20250929`}},{key:`options`,label:`options`,control:{type:`code`,text:`{"seed":1234,"temperature":0}`}},{key:`stream`,label:`stream`,control:{type:`code`,text:`false`}}]}),"table/sortable":()=>(0,k.jsx)($,{count:4}),"table/paged":()=>(0,k.jsx)($,{count:35,pageSize:10}),"table/select":()=>(0,k.jsx)($,{count:5,picked:[]}),"table/select-on":()=>(0,k.jsx)($,{count:5,picked:[1,3]}),"table/filter":()=>(0,k.jsx)(Ve,{}),"table/filter-on":()=>(0,k.jsx)(Ve,{search:`IMG_40`,size:`large`}),"edit/form":()=>(0,k.jsx)(F,{outcome:`Connected`,menu:[{label:`Restore…`,icon:m.restore,onSelect:A},{label:`Apply preset: repeat 1.3`,icon:m.preset,onSelect:A}],sections:[{key:`identity`,rows:I()},{key:`settings`,title:`Settings`,rows:L()},{key:`debug`,title:`Debug`,rows:R(),folded:!0}]}),"edit/plain":()=>(0,k.jsx)(f,{title:`A mapping`,sections:[{key:`one`,rows:[z,{key:`with`,label:`Separator`,control:{type:`text`,value:`, `,onChange:A}}]}],onClose:A}),"edit/long":()=>(0,k.jsx)(F,{menu:[{label:`Restore…`,icon:m.restore,onSelect:A,disabled:!0}],sections:[{key:`identity`,rows:I()},{key:`settings`,title:`Settings`,rows:[...L(),{key:`grammar`,label:`Grammar`,control:{type:`textarea`,value:`root ::= item (", " item)*
5
+ item ::= [a-z ]+`,onChange:A}},{key:`imagePosition`,label:`Image position`,control:{type:`select`,value:`after`,onChange:A,options:[{value:`before`,label:`before the prompt`},{value:`after`,label:`after the prompt`}]}},{key:`cachePrompt`,label:`Cache prompt`,control:{type:`select`,value:`off`,onChange:A,options:[{value:`on`,label:`on`},{value:`off`,label:`off`}]}}]},{key:`debug`,title:`Debug`,rows:R(),folded:!0}]}),"edit/status":()=>(0,k.jsx)(F,{status:`the relay could not reach http://127.0.0.1:11434`,statusBad:!0,sections:[{key:`identity`,rows:I()}]}),"view/tokens":()=>(0,k.jsx)(d,{id:`g`,title:`Token mappings`,level:3,children:(0,k.jsx)(Oe,{})}),"view/output":()=>(0,k.jsx)(N,{start:{kind:`list`,parse:`csv`,modifiers:[{type:`case`,mode:`LOWER`}]}}),"view/output-text":()=>(0,k.jsx)(N,{start:{kind:`text`,modifiers:[]}}),"view/output-two":()=>(0,k.jsx)(N,{start:{kind:`list`,parse:`csv`,modifiers:[{type:`case`,mode:`TITLE`},{type:`join`,with:` | `}]}}),"view/output-twice":()=>(0,k.jsx)(N,{start:{kind:`list`,parse:`csv`,modifiers:[{type:`case`,mode:`TITLE`},{type:`case`,mode:`LOWER`}]}}),"view/output-none":()=>(0,k.jsx)(N,{start:{kind:`text`,modifiers:[]},format:!1}),"view/output-flow":()=>(0,k.jsx)(N,{start:{kind:`text`,modifiers:[]},readAs:`@body('Ask')?['content']?[0]?['text']`}),"view/field-chip":()=>(0,k.jsx)(d,{id:`g`,title:`Edited`,level:3,children:(0,k.jsxs)(`div`,{className:`prompt`,children:[(0,k.jsx)(`span`,{className:`prop-name`,children:`Prompt`}),(0,k.jsx)(w,{words:`Summarise @{triggerBody()?['text']} in two short sentences for @{variables('Audience')}.`,was:`Summarise @{triggerBody()?['text']} in one sentence for @{variables('Audience')}.`})]})}),"view/field-edit":()=>(0,k.jsx)(ze,{}),"view/insert-field":()=>(0,k.jsx)(d,{id:`g`,title:`Edited`,level:3,children:(0,k.jsxs)(`div`,{className:`prompt`,children:[(0,k.jsxs)(`div`,{className:`pe-head`,children:[(0,k.jsx)(`span`,{className:`prop-name`,children:`Prompt`}),(0,k.jsx)(ne,{fields:J,onInsert:A,label:`Edited`,defaultOpen:!0})]}),(0,k.jsx)(`textarea`,{"aria-label":`Edited, prompt`,rows:4,defaultValue:q}),(0,k.jsx)(w,{className:`pe-words`,words:q,was:K,showExprs:!1})]})}),"multi/closed":()=>(0,k.jsx)(Y,{}),"multi/open":()=>(0,k.jsx)(Y,{open:!0}),"chiplist/words":()=>(0,k.jsx)(X,{start:[`clearly`,`obviously`,`as an AI`]}),"chiplist/empty":()=>(0,k.jsx)(X,{start:[]}),"wizard/pill":()=>(0,k.jsx)(Z,{}),"wizard/pill-open":()=>(0,k.jsx)(Z,{open:!0}),"switch/off":()=>(0,k.jsx)(G,{}),"switch/on":()=>(0,k.jsx)(G,{start:!0}),"switch/disabled":()=>(0,k.jsx)(s,{checked:!0,disabled:!0,label:`Snapshot now`,onChange:A})};function G({start:e}){let[t,n]=(0,E.useState)(!!e);return(0,k.jsx)(s,{checked:t,onChange:n,label:`Snapshot now`})}var K=`Tell me about this location in 1 sentence: @{variables('LocationVar')}`,q=`Tell me about this location in 2 sentences, for someone visiting for the first time: @{variables('LocationVar')}`,J=[{name:`LocationVar`,expr:`@{variables('LocationVar')}`},{name:`text`,expr:`@{triggerBody()?['text']}`}];function ze(){let[e,t]=(0,E.useState)(q);return(0,k.jsx)(d,{id:`g`,title:`Edited`,level:3,children:(0,k.jsx)(me,{label:`Edited`,asks:[{key:`connection`,label:`Connection`,control:{type:`static`,text:`Anthropic Direct`}}],prompt:e,onPrompt:t,rows:[{key:`model`,label:`Model`,control:{type:`static`,text:`claude-haiku-4-5`}}],request:[{key:`request`,label:`Request`,control:{type:`code`,text:`POST /v1/messages`}},{key:`body`,label:`Body`,control:{type:`code`,text:JSON.stringify({model:`claude-haiku-4-5`,max_tokens:1024,messages:[{role:`user`,content:e}]},null,2)}}],requestId:`story-edited-request`,words:{text:e,was:K},fields:J})})}function Y({open:e=!1}){let[t,n]=(0,E.useState)([`better`,`same`]),r=[{value:`better`,label:`Better`},{value:`same`,label:`Same`},{value:`worse`,label:`Worse`}];return e?(0,k.jsx)(`div`,{style:{minHeight:180},children:(0,k.jsx)(l,{value:t,options:r,onChange:n,defaultOpen:!0})}):(0,k.jsx)(p,{label:`Judged against recorded`,rows:[{key:`expected`,label:`Expected`,required:!0,control:{type:`multi`,value:t,options:r,onChange:n}}]})}function X({start:e}){let[t,n]=(0,E.useState)(e);return(0,k.jsx)(`div`,{style:{maxWidth:360},children:(0,k.jsx)(Se,{value:t,onChange:n})})}function Z({open:e}){return(0,k.jsx)(`div`,{style:{display:`flex`,justifyContent:`flex-end`,padding:`var(--s3)`},children:(0,k.jsx)(c,{name:`Test a workflow`,done:2,total:4,current:2,steps:[{title:`Add a workflow Source`,done:!0},{title:`Test an action`,done:!0},{title:`Run the test`,done:!1},{title:`Compare the replies`,done:!1}],onOpen:A,onCancel:A,...e?{defaultOpen:!0}:{}})})}var Q=De.map(e=>({...e,render:Re[e.path]})),Be=e=>Array.from({length:e},(e,t)=>({name:`IMG_${String(4e3+t*7919%97).padStart(4,`0`)}.jpg`,kb:t*37%900+40}));function Ve({search:e=``,size:t=`all`}){let[n,r]=(0,E.useState)(e),[i,o]=(0,E.useState)(t),[s,c]=(0,E.useState)(`shoot`),l=Be(12),u=l.filter(e=>e.name.toLowerCase().includes(n.trim().toLowerCase())&&(i===`all`||i===`large`==e.kb>=400));return(0,k.jsxs)(d,{id:`files`,title:`Files`,children:[(0,k.jsx)(a,{noun:`files`,search:{label:`Name`,placeholder:`Search names`,value:n,onChange:r},count:{shown:u.length,total:l.length},filters:[{key:`source`,label:`Source`,value:s,neutral:null,onChange:c,options:[{value:`shoot`,label:`Tuesday shoot`},{value:`samples`,label:`Sample library`}]},{key:`size`,label:`Size`,value:i,onChange:o,options:[{value:`all`,label:`All`},{value:`small`,label:`Small`},{value:`large`,label:`Large`}]}]}),u.length?(0,k.jsx)(b,{label:`Files`,rows:u,rowKey:e=>e.name,columns:[{key:`name`,header:`Name`,cell:e=>e.name},{key:`size`,header:`Size`,sort:e=>e.kb,cell:e=>`${e.kb} KB`}]}):(0,k.jsx)(`p`,{className:`note`,children:`No files match these filters.`})]})}function $({count:e,pageSize:t,picked:n}){let r=Be(e),[i,a]=(0,E.useState)(r),[o,s]=(0,E.useState)(()=>new Set((n??[]).map(e=>r[e].name)));return(0,k.jsx)(d,{id:`files`,title:`Files`,children:(0,k.jsx)(b,{label:`Files`,cards:!0,rows:i,rowKey:e=>e.name,pageSize:t??null,select:n&&{noun:`file`,selected:o,onChange:s,actions:e=>[{label:`Move…`,icon:m.rename,onSelect:A},{label:`Remove`,icon:m.remove,destructive:!0,onSelect:()=>{a(t=>t.filter(t=>!e.includes(t))),s(new Set)}},{label:`Export`,icon:C,onSelect:A}]},columns:[{key:`name`,header:`Name`,role:`title`,cell:e=>e.name},{key:`size`,header:`Size`,className:`num`,sort:e=>e.kb,cell:e=>`${e.kb} KB`},{key:`menu`,header:``,role:`menu`,sort:null,cell:e=>(0,k.jsx)(u,{label:`${e.name} actions`,actions:[{label:`Rename…`,icon:m.rename,onSelect:A},{label:`Remove`,icon:m.remove,destructive:!0,onSelect:A}]})}]})})}function He(){let[e,t]=(0,E.useState)(`files`);return(0,k.jsx)(p,{label:`New Source`,rows:[{key:`type`,label:`Type`,control:{type:`cards`,value:e,onChange:t,options:[{value:`files`,label:`File Library`,icon:(0,k.jsx)(r,{size:16,fill:`currentColor`,"aria-hidden":`true`})},{value:`flow`,label:`Power Automate flow`,icon:(0,k.jsx)(ye,{size:16,fill:`currentColor`,"aria-hidden":`true`})}]}},{key:`name`,label:`Name`,control:{type:`text`,value:``,onChange:A}}]})}var Ue=()=>decodeURIComponent(location.hash.replace(/^#\/?/,``));function We(){let[e,t]=(0,E.useState)(Ue);(0,E.useEffect)(()=>{let e=()=>t(Ue());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]);let n=Q.find(t=>t.path===e);return n?(0,k.jsxs)(`main`,{className:`gallery-one`,"data-route":n.path,children:[(0,k.jsxs)(`nav`,{className:`gallery-nav`,children:[(0,k.jsx)(`a`,{href:`#/`,children:`All`}),(0,k.jsx)(`span`,{children:n.title})]}),n.render()]}):(0,k.jsxs)(`main`,{className:`gallery-index`,"data-route":``,children:[(0,k.jsx)(`h1`,{children:`Components`}),(0,k.jsx)(`ul`,{children:Q.map(e=>(0,k.jsxs)(`li`,{children:[(0,k.jsx)(`a`,{href:`#/${e.path}`,children:e.title}),` `,(0,k.jsx)(`code`,{children:e.path})]},e.path))})]})}(0,Te.createRoot)(document.getElementById(`root`)).render((0,k.jsx)(E.StrictMode,{children:(0,k.jsx)(We,{})}));