litevision 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +160 -111
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -24047,110 +24047,143 @@ function renderPage(nonce) {
24047
24047
  <style>
24048
24048
  :root {
24049
24049
  color-scheme: light dark;
24050
- --bg: #f7f7f8; --card: #fff; --text: #1d1d1f; --muted: #6b6b73; --border: #dcdce0;
24051
- --accent: #2f6fde; --danger: #c43d3d; --ok: #1f8a4c; --warn: #a86b00;
24050
+ --bg: #fafafa; --surface: #fff; --field: #fff; --hover: #f4f4f5; --text: #18181b; --muted: #71717a; --border: #e4e4e7;
24051
+ --accent: #2563eb; --focus: rgb(37 99 235 / 0.22); --danger: #dc2626; --ok: #15803d; --warn: #b45309;
24052
24052
  }
24053
24053
  @media (prefers-color-scheme: dark) {
24054
- :root { --bg: #151517; --card: #1f1f22; --text: #ececf0; --muted: #a0a0aa; --border: #35353b;
24055
- --accent: #6f9cf0; --danger: #ec6b6b; --ok: #4cc283; --warn: #e0a640; }
24054
+ :root { --bg: #0f0f11; --surface: #18181b; --field: #121214; --hover: #26262a; --text: #f4f4f5; --muted: #a1a1aa; --border: #2e2e33;
24055
+ --accent: #3b82f6; --focus: rgb(59 130 246 / 0.35); --danger: #f87171; --ok: #4ade80; --warn: #fbbf24; }
24056
24056
  }
24057
24057
  * { box-sizing: border-box; }
24058
24058
  body { margin: 0; background: var(--bg); color: var(--text);
24059
- font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
24060
- main { max-width: 760px; margin: 0 auto; padding: 24px 16px 64px; }
24061
- h1 { font-size: 22px; margin: 0 0 4px; }
24062
- h2 { font-size: 17px; margin: 32px 0 12px; }
24063
- h3 { font-size: 15px; margin: 0; }
24064
- p.lead, .muted { color: var(--muted); }
24065
- p.lead { margin: 0 0 8px; }
24066
- .card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 16px; margin-bottom: 12px; }
24067
- .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
24068
- .row + .row { margin-top: 10px; }
24069
- .grow { flex: 1 1 220px; min-width: 0; }
24070
- label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
24071
- input, select, textarea, button { font: inherit; border-radius: 7px; }
24072
- input, select, textarea { width: 100%; padding: 7px 9px; border: 1px solid var(--border); background: var(--bg); color: var(--text); }
24073
- textarea { resize: vertical; min-height: 60px; }
24074
- .badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: var(--accent); color: #fff; }
24075
- .counter { font-size: 12px; color: var(--muted); text-align: right; }
24076
- .notes-history { font-size: 13px; }
24077
- button { padding: 7px 12px; border: 1px solid var(--border); background: var(--card); color: var(--text); cursor: pointer; white-space: nowrap; }
24059
+ font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
24060
+ main { max-width: 720px; margin: 0 auto; padding: 48px 20px 96px; }
24061
+ h1 { font-size: 20px; font-weight: 600; margin: 0 0 6px; }
24062
+ h2 { font-size: 15px; font-weight: 600; margin: 0; }
24063
+ h3 { font-size: 15px; font-weight: 600; margin: 0; flex: 1; min-width: 0; overflow-wrap: anywhere; }
24064
+ p { margin: 0; }
24065
+ .muted { color: var(--muted); }
24066
+ header p { color: var(--muted); }
24067
+ section { margin-top: 44px; }
24068
+ .section-head { margin-bottom: 14px; }
24069
+ .section-head p { color: var(--muted); font-size: 13px; margin-top: 2px; }
24070
+
24071
+ .card { display: grid; gap: 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
24072
+ padding: 20px; margin-bottom: 12px; }
24073
+ .card-head { display: flex; align-items: center; gap: 10px; }
24074
+ .fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
24075
+ .field { min-width: 0; }
24076
+ .wide { grid-column: 1 / -1; }
24077
+ .inline { display: flex; flex-wrap: wrap; gap: 8px; }
24078
+ .inline > input, .inline > select { flex: 1 1 200px; width: auto; }
24079
+ .actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 16px; border-top: 1px solid var(--border); }
24080
+ .actions > select { width: auto; flex: 0 1 180px; }
24081
+ .spacer { flex: 1; }
24082
+ .empty { color: var(--muted); padding: 20px; border: 1px dashed var(--border); border-radius: 12px; text-align: center; margin-bottom: 12px; }
24083
+
24084
+ label { display: block; font-size: 12px; font-weight: 500; color: var(--muted); margin-bottom: 6px; }
24085
+ input, select, textarea, button { font: inherit; color: inherit; }
24086
+ input, select, textarea { width: 100%; height: 34px; padding: 0 10px; border: 1px solid var(--border); border-radius: 8px;
24087
+ background: var(--field); color: var(--text); }
24088
+ textarea { height: auto; min-height: 72px; padding: 8px 10px; resize: vertical; }
24089
+ input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus); }
24090
+ input::placeholder, textarea::placeholder { color: var(--muted); opacity: 0.8; }
24091
+ /* API keys are masked with CSS: a real key field would make the browser offer to save the key as a site login. */
24092
+ .secret { -webkit-text-security: disc; }
24093
+ .counter { font-size: 12px; color: var(--muted); text-align: right; margin-top: 4px; }
24094
+
24095
+ button { height: 34px; padding: 0 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
24096
+ font-weight: 500; cursor: pointer; white-space: nowrap; }
24097
+ button:hover:not(:disabled) { background: var(--hover); }
24098
+ button:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus); }
24078
24099
  button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
24079
- button.danger { color: var(--danger); }
24080
- button:disabled { opacity: 0.6; cursor: default; }
24081
- code { font-size: 13px; overflow-wrap: anywhere; }
24100
+ button.primary:hover:not(:disabled) { background: var(--accent); filter: brightness(1.08); }
24101
+ button.danger { border-color: transparent; background: transparent; color: var(--danger); }
24102
+ button.small { height: 28px; padding: 0 10px; font-size: 13px; }
24103
+ button:disabled { opacity: 0.5; cursor: default; }
24104
+
24105
+ .chip { flex: none; font-size: 12px; font-weight: 500; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
24106
+ .chip.ok { color: var(--ok); border-color: currentColor; }
24107
+ .chip.accent { color: #fff; background: var(--accent); border-color: var(--accent); }
24108
+ .chip.failed { color: var(--danger); border-color: currentColor; }
24109
+ .chip.running { color: var(--warn); border-color: currentColor; }
24082
24110
  .key { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
24083
- .result { font-size: 14px; margin-top: 10px; }
24111
+
24112
+ .result { font-size: 13px; }
24113
+ .result:empty { display: none; }
24114
+ .field > .result { margin-top: 8px; }
24084
24115
  .result.success { color: var(--ok); } .result.failure { color: var(--danger); } .result.unsupported { color: var(--warn); }
24085
- #status { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); max-width: calc(100% - 32px);
24086
- background: var(--text); color: var(--bg); padding: 8px 14px; border-radius: 8px; font-size: 14px; display: none; }
24087
- #status.error { background: var(--danger); color: #fff; }
24088
- .job-meta { font-size: 13px; color: var(--muted); }
24089
- .prompt { margin: 8px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
24090
- .thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
24091
- .thumb { width: 112px; height: 112px; object-fit: cover; border-radius: 7px; border: 1px solid var(--border); background: var(--bg); }
24116
+ .note { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; margin-top: 8px; }
24117
+
24118
+ .job-meta { font-size: 12px; color: var(--muted); margin-top: 2px; }
24119
+ .prompt { white-space: pre-wrap; overflow-wrap: anywhere; }
24120
+ .thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
24121
+ .thumb { width: 96px; height: 96px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); }
24092
24122
  .thumb.placeholder { display: flex; align-items: center; justify-content: center; text-align: center; padding: 6px;
24093
- font-size: 12px; color: var(--muted); border-style: dashed; }
24094
- .rating { font-size: 14px; }
24123
+ font-size: 11px; color: var(--muted); border-style: dashed; }
24124
+ .actions.rate { display: grid; gap: 10px; }
24125
+ .rating { font-size: 13px; }
24095
24126
  .rating.good { color: var(--ok); } .rating.bad { color: var(--danger); }
24096
- .outcome.failed { color: var(--danger); } .outcome.running { color: var(--warn); }
24127
+
24128
+ #status { position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); max-width: calc(100% - 32px);
24129
+ background: var(--text); color: var(--bg); padding: 8px 14px; border-radius: 8px; font-size: 13px; display: none;
24130
+ box-shadow: 0 6px 24px rgb(0 0 0 / 0.18); }
24131
+ #status.error { background: var(--danger); color: #fff; }
24097
24132
  </style>
24098
24133
  </head>
24099
24134
  <body>
24100
24135
  <main>
24101
- <h1>litevision settings</h1>
24102
- <p class="lead">Credentials you enter here are saved on this computer only, in a file only you can read. They never pass through your Agent.</p>
24136
+ <header>
24137
+ <h1>litevision settings</h1>
24138
+ <p>Credentials you enter here are saved on this computer only, in a file only you can read. They never pass through your Agent.</p>
24139
+ </header>
24103
24140
 
24104
24141
  <section id="providers">
24105
- <h2>Providers</h2>
24142
+ <div class="section-head"><h2>Providers</h2></div>
24106
24143
  <div id="provider-list"><p class="muted">Loading…</p></div>
24107
24144
 
24108
24145
  <form id="add-provider" class="card" autocomplete="off">
24109
24146
  <h3>Add a Provider</h3>
24110
- <div class="row">
24111
- <div class="grow"><label for="add-preset">Preset</label><select id="add-preset"></select></div>
24112
- <div class="grow"><label for="add-id">Name</label><input id="add-id" required spellcheck="false" placeholder="openai"></div>
24147
+ <div class="fields">
24148
+ <div class="field"><label for="add-preset">Preset</label><select id="add-preset"></select></div>
24149
+ <div class="field"><label for="add-id">Name</label><input id="add-id" required spellcheck="false" placeholder="openai"></div>
24150
+ <div class="field wide"><label for="add-base-url">Base URL</label><input id="add-base-url" required spellcheck="false" placeholder="https://relay.example/v1"></div>
24151
+ <div class="field wide"><label for="add-credential">API key</label><input id="add-credential" class="secret" autocomplete="off" data-1p-ignore data-lpignore="true" data-bwignore spellcheck="false" placeholder="Paste your API key"></div>
24113
24152
  </div>
24114
- <div class="row">
24115
- <div class="grow"><label for="add-base-url">Base URL</label><input id="add-base-url" required spellcheck="false" placeholder="https://relay.example/v1"></div>
24116
- </div>
24117
- <div class="row">
24118
- <div class="grow"><label for="add-credential">API key</label><input id="add-credential" type="password" autocomplete="new-password" spellcheck="false" placeholder="Paste your API key"></div>
24119
- </div>
24120
- <div class="row"><button class="primary" type="submit">Add Provider</button></div>
24153
+ <div class="actions"><button class="primary" type="submit">Add Provider</button></div>
24121
24154
  </form>
24122
24155
  </section>
24123
24156
 
24124
24157
  <section id="models">
24125
- <h2>Models</h2>
24126
- <p class="muted">Your Agent picks a Model by its Alias. Model Notes tell it what each Model is good or bad at.</p>
24158
+ <div class="section-head">
24159
+ <h2>Models</h2>
24160
+ <p>Your Agent picks a Model by its Alias. Model Notes tell it what each Model is good or bad at.</p>
24161
+ </div>
24127
24162
  <div id="model-list"><p class="muted">Loading…</p></div>
24128
24163
 
24129
24164
  <form id="add-model" class="card" autocomplete="off">
24130
24165
  <h3>Add a Model</h3>
24131
- <div class="row">
24132
- <div class="grow"><label for="model-provider">Provider</label><select id="model-provider" required></select></div>
24133
- <div class="grow"><label for="model-protocol">Protocol</label><select id="model-protocol" required></select></div>
24134
- </div>
24135
- <div class="row">
24136
- <div class="grow"><label for="model-id">Model id</label><input id="model-id" required spellcheck="false" list="model-ids" placeholder="gpt-image-1"></div>
24137
- <button id="fetch-models" type="button">Fetch model list</button>
24138
- </div>
24139
- <datalist id="model-ids"></datalist>
24140
- <div id="fetch-result" class="result"></div>
24141
- <div class="row">
24142
- <div class="grow"><label for="model-alias">Alias</label><input id="model-alias" required spellcheck="false" placeholder="gpt-image-cheap"></div>
24143
- </div>
24144
- <div class="row">
24145
- <div class="grow"><label for="model-notes">Model Notes</label><textarea id="model-notes" maxlength="${modelNotesMaxLength}" placeholder="photoreal, cheap, bad at Chinese text"></textarea></div>
24166
+ <div class="fields">
24167
+ <div class="field"><label for="model-provider">Provider</label><select id="model-provider" required></select></div>
24168
+ <div class="field"><label for="model-protocol">Protocol</label><select id="model-protocol" required></select></div>
24169
+ <div class="field wide">
24170
+ <label for="model-id">Model id</label>
24171
+ <div class="inline"><input id="model-id" required spellcheck="false" list="model-ids" placeholder="gpt-image-1"><button id="fetch-models" type="button">Fetch model list</button></div>
24172
+ <datalist id="model-ids"></datalist>
24173
+ <div id="fetch-result" class="result"></div>
24174
+ </div>
24175
+ <div class="field wide"><label for="model-alias">Alias</label><input id="model-alias" required spellcheck="false" placeholder="gpt-image-cheap"></div>
24176
+ <div class="field wide"><label for="model-notes">Model Notes</label><textarea id="model-notes" maxlength="${modelNotesMaxLength}" placeholder="photoreal, cheap, bad at Chinese text"></textarea></div>
24146
24177
  </div>
24147
- <div class="row"><button class="primary" type="submit">Add Model</button></div>
24178
+ <div class="actions"><button class="primary" type="submit">Add Model</button></div>
24148
24179
  </form>
24149
24180
  </section>
24150
24181
 
24151
24182
  <section id="history">
24152
- <h2>Job history</h2>
24153
- <p class="muted">Newest first. Rate a Job to keep your verdict on it; rating it again replaces the earlier Rating.</p>
24183
+ <div class="section-head">
24184
+ <h2>Job history</h2>
24185
+ <p>Newest first. Rate a Job to keep your verdict on it; rating it again replaces the earlier Rating.</p>
24186
+ </div>
24154
24187
  <div id="job-list"><p class="muted">Loading…</p></div>
24155
24188
  </section>
24156
24189
  </main>
@@ -24193,6 +24226,25 @@ function el(tag, props, ...children) {
24193
24226
  return node;
24194
24227
  }
24195
24228
 
24229
+ let fieldCount = 0;
24230
+ /** A labelled control, with any buttons on the same line. */
24231
+ function field(text, control, ...buttons) {
24232
+ control.id ||= "field-" + ++fieldCount;
24233
+ return el("div", { className: "field" }, el("label", { htmlFor: control.id, textContent: text }),
24234
+ buttons.length === 0 ? control : el("div", { className: "inline" }, control, ...buttons));
24235
+ }
24236
+
24237
+ /** An API key input, masked with CSS (see .secret) and marked for 1Password, LastPass and Bitwarden to leave alone. */
24238
+ function secretInput(placeholder) {
24239
+ const input = el("input", { className: "secret", autocomplete: "off", spellcheck: false, placeholder });
24240
+ for (const name of ["data-1p-ignore", "data-lpignore", "data-bwignore"]) input.setAttribute(name, "true");
24241
+ return input;
24242
+ }
24243
+
24244
+ function emptyState(text) {
24245
+ return el("p", { className: "empty", textContent: text });
24246
+ }
24247
+
24196
24248
  let statusTimer;
24197
24249
  function showStatus(message, isError) {
24198
24250
  const status = document.getElementById("status");
@@ -24230,8 +24282,7 @@ function protocolSelect(selected) {
24230
24282
 
24231
24283
  function providerCard(provider) {
24232
24284
  const baseUrl = el("input", { value: provider.baseUrl, spellcheck: false });
24233
- const credential = el("input", { type: "password", autocomplete: "new-password", spellcheck: false,
24234
- placeholder: provider.credential ? "New API key to replace the saved one" : "Paste your API key" });
24285
+ const credential = secretInput(provider.credential ? "New API key to replace the saved one" : "Paste your API key");
24235
24286
  const protocol = protocolSelect(protocolFor(provider));
24236
24287
  const result = el("div", { className: "result" });
24237
24288
 
@@ -24248,7 +24299,7 @@ function providerCard(provider) {
24248
24299
  await loadProviders();
24249
24300
  }, "API key saved.");
24250
24301
 
24251
- const removeKey = el("button", { type: "button", className: "danger", textContent: "Remove key", disabled: !provider.credential });
24302
+ const removeKey = el("button", { type: "button", className: "danger", textContent: "Remove key" });
24252
24303
  removeKey.onclick = () => {
24253
24304
  if (!confirm("Remove the saved API key for " + provider.id + "?")) return;
24254
24305
  act(removeKey, async () => {
@@ -24275,15 +24326,16 @@ function providerCard(provider) {
24275
24326
  }, "Provider removed.");
24276
24327
  };
24277
24328
 
24278
- const keyText = provider.credential
24279
- ? el("span", { className: "key", textContent: "API key ••••" + provider.credential.last4 })
24280
- : el("span", { className: "muted", textContent: "No API key saved" });
24329
+ const keyChip = provider.credential
24330
+ ? el("span", { className: "chip ok key", textContent: "Key ••••" + provider.credential.last4 })
24331
+ : el("span", { className: "chip", textContent: "No API key" });
24281
24332
 
24282
24333
  return el("div", { className: "card" },
24283
- el("div", { className: "row" }, el("h3", { className: "grow", textContent: provider.id }), keyText),
24284
- el("div", { className: "row" }, el("div", { className: "grow" }, baseUrl), saveUrl),
24285
- el("div", { className: "row" }, el("div", { className: "grow" }, credential), saveKey, removeKey),
24286
- el("div", { className: "row" }, el("div", { className: "grow" }, protocol), test, remove),
24334
+ el("div", { className: "card-head" }, el("h3", { textContent: provider.id }), keyChip),
24335
+ el("div", { className: "fields" },
24336
+ el("div", { className: "wide" }, field("Base URL", baseUrl, saveUrl)),
24337
+ el("div", { className: "wide" }, provider.credential ? field("API key", credential, saveKey, removeKey) : field("API key", credential, saveKey))),
24338
+ el("div", { className: "actions" }, protocol, test, el("span", { className: "spacer" }), remove),
24287
24339
  result,
24288
24340
  );
24289
24341
  }
@@ -24292,7 +24344,7 @@ async function loadProviders() {
24292
24344
  const list = document.getElementById("provider-list");
24293
24345
  const { providers } = await api("GET", "/api/providers");
24294
24346
  list.replaceChildren(...(providers.length === 0
24295
- ? [el("p", { className: "muted", textContent: "No Providers yet. Add one below." })]
24347
+ ? [emptyState("No Providers yet. Add one below.")]
24296
24348
  : providers.map(providerCard)));
24297
24349
  providerIds = providers.map((p) => p.id);
24298
24350
  const select = document.getElementById("model-provider");
@@ -24314,7 +24366,10 @@ function notesField(value) {
24314
24366
  const update = () => (counter.textContent = textarea.value.length + " / " + notesMax);
24315
24367
  textarea.oninput = update;
24316
24368
  update();
24317
- return { textarea, wrapper: el("div", { className: "grow" }, el("label", { textContent: "Model Notes" }), textarea, counter) };
24369
+ const wrapper = field("Model Notes", textarea);
24370
+ wrapper.classList.add("wide");
24371
+ wrapper.append(counter);
24372
+ return { textarea, wrapper };
24318
24373
  }
24319
24374
 
24320
24375
  /** "Last changed by the Agent, 30 Oct 2026, 10:00", plus an undo button when there is a previous version. */
@@ -24323,20 +24378,16 @@ function notesHistoryRow(model, path) {
24323
24378
  if (!history) return [];
24324
24379
  const who = history.changedBy === "Agent" ? "your Agent" : "you";
24325
24380
  const when = new Date(history.changedAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
24326
- const parts = [el("span", { className: "grow muted", textContent: "Model Notes last changed by " + who + ", " + when + "." })];
24381
+ const parts = [el("span", { textContent: "Last changed by " + who + ", " + when + "." })];
24327
24382
  if (history.previous !== undefined) {
24328
- const undo = el("button", { type: "button", textContent: "Undo", title: "Back to: " + (history.previous || "(no Model Notes)") });
24383
+ const undo = el("button", { type: "button", className: "small", textContent: "Undo", title: "Back to: " + (history.previous || "(no Model Notes)") });
24329
24384
  undo.onclick = () => act(undo, async () => {
24330
24385
  await api("POST", path + "/notes/undo");
24331
24386
  await loadModels();
24332
24387
  }, "Model Notes restored. Undo again to redo.");
24333
24388
  parts.push(undo);
24334
24389
  }
24335
- return [el("div", { className: "row notes-history" }, ...parts)];
24336
- }
24337
-
24338
- function labelled(text, control) {
24339
- return el("div", { className: "grow" }, el("label", { textContent: text }), control);
24390
+ return [el("div", { className: "note" }, ...parts)];
24340
24391
  }
24341
24392
 
24342
24393
  function modelCard(model) {
@@ -24353,7 +24404,7 @@ function modelCard(model) {
24353
24404
  await loadModels();
24354
24405
  }, "Model saved.");
24355
24406
 
24356
- const makeDefault = el("button", { type: "button", textContent: "Make default" });
24407
+ const makeDefault = el("button", { type: "button", className: "small", textContent: "Make default" });
24357
24408
  makeDefault.onclick = () => act(makeDefault, async () => {
24358
24409
  await api("PUT", "/api/default-image-model", { alias: model.alias });
24359
24410
  await loadModels();
@@ -24368,14 +24419,14 @@ function modelCard(model) {
24368
24419
  }, "Model removed.");
24369
24420
  };
24370
24421
 
24422
+ notes.wrapper.append(...notesHistoryRow(model, path));
24423
+
24371
24424
  return el("div", { className: "card" },
24372
- el("div", { className: "row" }, el("h3", { className: "grow", textContent: model.alias }),
24373
- model.default ? el("span", { className: "badge", textContent: "Default" }) : makeDefault),
24374
- el("div", { className: "row" }, labelled("Alias", alias), labelled("Provider", provider)),
24375
- el("div", { className: "row" }, labelled("Model id", modelId), labelled("Protocol", protocol)),
24376
- el("div", { className: "row" }, notes.wrapper),
24377
- ...notesHistoryRow(model, path),
24378
- el("div", { className: "row" }, save, remove),
24425
+ el("div", { className: "card-head" }, el("h3", { textContent: model.alias }),
24426
+ model.default ? el("span", { className: "chip accent", textContent: "Default" }) : makeDefault),
24427
+ el("div", { className: "fields" },
24428
+ field("Alias", alias), field("Provider", provider), field("Model id", modelId), field("Protocol", protocol), notes.wrapper),
24429
+ el("div", { className: "actions" }, save, el("span", { className: "spacer" }), remove),
24379
24430
  );
24380
24431
  }
24381
24432
 
@@ -24383,7 +24434,7 @@ async function loadModels() {
24383
24434
  const list = document.getElementById("model-list");
24384
24435
  const { models } = await api("GET", "/api/models");
24385
24436
  list.replaceChildren(...(models.length === 0
24386
- ? [el("p", { className: "muted", textContent: providerIds.length === 0 ? "Add a Provider first, then add its Models below." : "No Models yet. Add one below." })]
24437
+ ? [emptyState(providerIds.length === 0 ? "Add a Provider first, then add its Models below." : "No Models yet. Add one below.")]
24387
24438
  : models.map(modelCard)));
24388
24439
  }
24389
24440
 
@@ -24485,11 +24536,11 @@ function formatDuration(ms) {
24485
24536
  }
24486
24537
 
24487
24538
  /** The current Rating, and a one-line reason with Good and Bad buttons to rate the Job. Only completed Jobs can be rated. */
24488
- function ratingRows(job) {
24539
+ function ratingRow(job) {
24489
24540
  if (job.outcome !== "completed") return [];
24490
24541
  const current = job.rating
24491
- ? el("span", { className: "grow rating " + job.rating.verdict, textContent: (job.rating.verdict === "good" ? "Good" : "Bad") + ": " + job.rating.reason })
24492
- : el("span", { className: "grow muted", textContent: "Not rated yet." });
24542
+ ? el("span", { className: "rating " + job.rating.verdict, textContent: (job.rating.verdict === "good" ? "Good" : "Bad") + ": " + job.rating.reason })
24543
+ : el("span", { className: "rating muted", textContent: "Not rated yet." });
24493
24544
  const reason = el("input", { maxLength: reasonMax, placeholder: "One-line reason, e.g. the text is misspelled" });
24494
24545
  const rateAs = (verdict, label) => {
24495
24546
  const button = el("button", { type: "button", textContent: label });
@@ -24499,10 +24550,7 @@ function ratingRows(job) {
24499
24550
  }, "Rating saved.");
24500
24551
  return button;
24501
24552
  };
24502
- return [
24503
- el("div", { className: "row" }, current),
24504
- el("div", { className: "row" }, el("div", { className: "grow" }, reason), rateAs("good", "Good"), rateAs("bad", "Bad")),
24505
- ];
24553
+ return [el("div", { className: "actions rate" }, current, el("div", { className: "inline" }, reason, rateAs("good", "Good"), rateAs("bad", "Bad")))];
24506
24554
  }
24507
24555
 
24508
24556
  function jobCard(job) {
@@ -24514,13 +24562,14 @@ function jobCard(job) {
24514
24562
  if (job.error) details.push(el("p", { className: "result failure", textContent: job.error }));
24515
24563
  if (job.shortfall) details.push(el("p", { className: "result unsupported", textContent: job.shortfall }));
24516
24564
  return el("div", { className: "card" },
24517
- el("div", { className: "row job-meta" },
24518
- el("span", { className: "grow", textContent: job.model + " (" + job.modelId + " on " + job.provider + ") · " + when + " · " + formatDuration(job.durationMs) }),
24519
- el("span", { className: "outcome " + job.outcome, textContent: outcome })),
24565
+ el("div", {},
24566
+ el("div", { className: "card-head" }, el("h3", { textContent: job.model }),
24567
+ el("span", { className: "chip " + (job.outcome === "completed" ? "ok" : job.outcome), textContent: outcome })),
24568
+ el("p", { className: "job-meta", textContent: job.modelId + " on " + job.provider + " · " + when + " · " + formatDuration(job.durationMs) })),
24520
24569
  el("p", { className: "prompt", textContent: job.prompt }),
24521
24570
  ...(job.files.length === 0 ? [] : [el("div", { className: "thumbs" }, ...job.files.map(thumbnail))]),
24522
24571
  ...details,
24523
- ...ratingRows(job),
24572
+ ...ratingRow(job),
24524
24573
  );
24525
24574
  }
24526
24575
 
@@ -24528,7 +24577,7 @@ async function loadJobs() {
24528
24577
  const list = document.getElementById("job-list");
24529
24578
  const { jobs } = await api("GET", "/api/jobs");
24530
24579
  list.replaceChildren(...(jobs.length === 0
24531
- ? [el("p", { className: "muted", textContent: "No Jobs yet. Images your Agent generates will appear here." })]
24580
+ ? [emptyState("No Jobs yet. Images your Agent generates will appear here.")]
24532
24581
  : jobs.map(jobCard)));
24533
24582
  }
24534
24583
 
@@ -24698,7 +24747,7 @@ function settingsOpener(options) {
24698
24747
  };
24699
24748
  }
24700
24749
  // package.json
24701
- var version2 = "0.1.0";
24750
+ var version2 = "0.1.1";
24702
24751
 
24703
24752
  // src/server.ts
24704
24753
  var instructions = `litevision generates images through Models the User has configured with their own Providers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litevision",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Let an AI agent generate images and videos through models you already pay for.",
5
5
  "keywords": [
6
6
  "mcp",