openclacky 1.2.8 → 1.2.9

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.
@@ -3858,6 +3858,7 @@ body {
3858
3858
  .model-card-grid-actions {
3859
3859
  display: flex;
3860
3860
  flex-direction: row;
3861
+ align-items: center;
3861
3862
  flex-wrap: wrap;
3862
3863
  gap: 0.5rem;
3863
3864
  padding-top: 0.625rem;
@@ -3867,7 +3868,7 @@ body {
3867
3868
  display: inline-flex;
3868
3869
  align-items: center;
3869
3870
  justify-content: center;
3870
- gap: 0.25rem;
3871
+ gap: 0.3125rem;
3871
3872
  padding: 0.375rem 0.5rem;
3872
3873
  border: 1px solid var(--color-border-primary);
3873
3874
  border-radius: 6px;
@@ -3877,15 +3878,9 @@ body {
3877
3878
  cursor: pointer;
3878
3879
  transition: all 0.15s;
3879
3880
  white-space: nowrap;
3881
+ min-width: 0;
3880
3882
  }
3881
- .btn-card-grid-action svg {
3882
- flex-shrink: 0;
3883
- }
3884
- .btn-card-grid-action span {
3885
- overflow: hidden;
3886
- text-overflow: ellipsis;
3887
- white-space: nowrap;
3888
- }
3883
+ .btn-card-grid-action svg { flex-shrink: 0; }
3889
3884
  .btn-card-grid-action:hover:not(:disabled) {
3890
3885
  border-color: var(--color-accent-primary);
3891
3886
  color: var(--color-accent-primary);
@@ -3895,13 +3890,22 @@ body {
3895
3890
  opacity: 0.5;
3896
3891
  cursor: not-allowed;
3897
3892
  }
3893
+ .btn-card-grid-action-primary {
3894
+ background: var(--color-bg-secondary);
3895
+ color: var(--color-text-primary);
3896
+ font-weight: 500;
3897
+ }
3898
3898
  .btn-card-grid-action-danger:hover:not(:disabled) {
3899
3899
  border-color: var(--color-error);
3900
3900
  color: var(--color-error);
3901
3901
  background: color-mix(in srgb, var(--color-error) 8%, transparent);
3902
3902
  }
3903
- a.btn-card-grid-action {
3904
- text-decoration: none;
3903
+ .model-card-grid-toolbar {
3904
+ display: flex;
3905
+ flex-wrap: wrap;
3906
+ gap: 0.375rem;
3907
+ margin-left: auto;
3908
+ justify-content: flex-end;
3905
3909
  }
3906
3910
  .model-card-grid-footer {
3907
3911
  display: flex;
@@ -9710,4 +9714,160 @@ body.setup-mode[data-theme="dark"] {
9710
9714
  grid-column: 1 / -1;
9711
9715
  text-align: right;
9712
9716
  }
9713
- }
9717
+ }
9718
+
9719
+ /* ════ Media generation (Settings → Models tab) ════ */
9720
+ .settings-section-desc {
9721
+ font-size: 0.8125rem;
9722
+ color: var(--color-text-secondary);
9723
+ line-height: 1.5;
9724
+ margin: -0.25rem 0 0.5rem;
9725
+ }
9726
+ .media-row {
9727
+ border: 1px solid var(--color-border-primary);
9728
+ border-radius: 8px;
9729
+ background: var(--color-bg-secondary);
9730
+ margin-bottom: 0.5rem;
9731
+ overflow: hidden;
9732
+ }
9733
+ .media-row.is-expanded {
9734
+ background: var(--color-bg-secondary);
9735
+ }
9736
+ .media-row-head {
9737
+ display: flex;
9738
+ align-items: center;
9739
+ gap: 0.75rem;
9740
+ padding: 0.5rem 0.75rem;
9741
+ min-height: 2.25rem;
9742
+ }
9743
+ .media-row-title {
9744
+ font-size: 0.875rem;
9745
+ font-weight: 600;
9746
+ color: var(--color-text-primary);
9747
+ min-width: 3rem;
9748
+ }
9749
+ .media-row-segmented {
9750
+ display: inline-flex;
9751
+ background: var(--color-bg-primary);
9752
+ border: 1px solid var(--color-border-primary);
9753
+ border-radius: 6px;
9754
+ padding: 1px;
9755
+ }
9756
+ .media-row-segmented button {
9757
+ background: transparent;
9758
+ border: none;
9759
+ padding: 0.25rem 0.625rem;
9760
+ font-size: 0.75rem;
9761
+ color: var(--color-text-secondary);
9762
+ border-radius: 5px;
9763
+ cursor: pointer;
9764
+ transition: background-color 0.15s ease, color 0.15s ease;
9765
+ font-family: inherit;
9766
+ }
9767
+ .media-row-segmented button:hover:not(.is-active):not(:disabled) {
9768
+ color: var(--color-text-primary);
9769
+ }
9770
+ .media-row-segmented button.is-active {
9771
+ background: var(--color-button-primary);
9772
+ color: #fff;
9773
+ }
9774
+ .media-row-segmented button:disabled {
9775
+ opacity: 0.4;
9776
+ cursor: not-allowed;
9777
+ }
9778
+ .media-row-status {
9779
+ margin-left: auto;
9780
+ font-size: 0.75rem;
9781
+ color: var(--color-text-secondary);
9782
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
9783
+ white-space: nowrap;
9784
+ overflow: hidden;
9785
+ text-overflow: ellipsis;
9786
+ max-width: 16rem;
9787
+ }
9788
+ .media-row-detail {
9789
+ border-top: 1px solid var(--color-border-primary);
9790
+ padding: 0.625rem 0.75rem;
9791
+ font-size: 0.8125rem;
9792
+ color: var(--color-text-secondary);
9793
+ line-height: 1.5;
9794
+ background: var(--color-bg-primary);
9795
+ }
9796
+ .media-row-detail.is-warning {
9797
+ color: var(--color-warning, #d97706);
9798
+ }
9799
+ .media-row-hint {
9800
+ margin-top: 0.375rem;
9801
+ font-size: 0.75rem;
9802
+ color: var(--color-text-tertiary, var(--color-text-secondary));
9803
+ }
9804
+ .media-row-detail .media-kv {
9805
+ display: grid;
9806
+ grid-template-columns: max-content 1fr;
9807
+ gap: 0.25rem 0.75rem;
9808
+ font-size: 0.8125rem;
9809
+ }
9810
+ .media-row-detail .media-kv-key {
9811
+ color: var(--color-text-secondary);
9812
+ }
9813
+ .media-row-detail .media-kv-val {
9814
+ color: var(--color-text-primary);
9815
+ font-weight: 500;
9816
+ word-break: break-all;
9817
+ }
9818
+ .media-row-actions {
9819
+ display: flex;
9820
+ gap: 0.5rem;
9821
+ margin-top: 0.5rem;
9822
+ justify-content: flex-end;
9823
+ }
9824
+ .media-row-btn {
9825
+ background: transparent;
9826
+ border: 1px solid var(--color-border-primary);
9827
+ color: var(--color-text-primary);
9828
+ border-radius: 6px;
9829
+ padding: 0.3125rem 0.75rem;
9830
+ font-size: 0.75rem;
9831
+ cursor: pointer;
9832
+ transition: background-color 0.15s ease;
9833
+ font-family: inherit;
9834
+ }
9835
+ .media-row-btn:hover {
9836
+ background: var(--color-bg-secondary);
9837
+ }
9838
+ .media-row-btn.is-primary {
9839
+ background: var(--color-button-primary);
9840
+ color: #fff;
9841
+ border-color: transparent;
9842
+ }
9843
+ .media-row-btn.is-primary:hover {
9844
+ background: var(--color-button-primary-hover);
9845
+ }
9846
+ .media-custom-form {
9847
+ display: grid;
9848
+ grid-template-columns: max-content 1fr;
9849
+ gap: 0.4375rem 0.625rem;
9850
+ align-items: center;
9851
+ }
9852
+ .media-custom-form label {
9853
+ font-size: 0.75rem;
9854
+ color: var(--color-text-secondary);
9855
+ }
9856
+ .media-custom-form input {
9857
+ width: 100%;
9858
+ background: var(--color-bg-secondary);
9859
+ border: 1px solid var(--color-border-primary);
9860
+ color: var(--color-text-primary);
9861
+ border-radius: 6px;
9862
+ padding: 0.375rem 0.5rem;
9863
+ font-size: 0.8125rem;
9864
+ font-family: inherit;
9865
+ box-sizing: border-box;
9866
+ }
9867
+ .media-custom-form .media-form-actions {
9868
+ grid-column: 1 / -1;
9869
+ display: flex;
9870
+ gap: 0.5rem;
9871
+ justify-content: flex-end;
9872
+ margin-top: 0.25rem;
9873
+ }
@@ -490,6 +490,35 @@ const I18n = (() => {
490
490
  "settings.models.empty": "No models configured. Click \"+ Add Model\" to add one.",
491
491
  "settings.models.badge.default": "Default",
492
492
  "settings.models.badge.lite": "Lite",
493
+ "settings.media.title": "Media Generation",
494
+ "settings.media.desc": "Optional. Image / video / audio generation models.",
495
+ "settings.media.loading": "Loading…",
496
+ "settings.media.error": "Failed to load: {{msg}}",
497
+ "settings.media.kind.image": "Image",
498
+ "settings.media.kind.video": "Video",
499
+ "settings.media.kind.audio": "Audio",
500
+ "settings.media.source.off": "Off",
501
+ "settings.media.source.auto": "Auto",
502
+ "settings.media.source.custom": "Custom",
503
+ "settings.media.field.model": "Model",
504
+ "settings.media.field.baseUrl": "Base URL",
505
+ "settings.media.field.apiKey": "API Key",
506
+ "settings.media.field.provider":"Provider",
507
+ "settings.media.off.hint": "Disabled.",
508
+ "settings.media.auto.followsDefault": "Follows default chat model",
509
+ "settings.media.auto.noDefaultModel": "Set a default chat model first.",
510
+ "settings.media.auto.unsupported": "Current provider has no built-in model for this kind. Switch to Custom.",
511
+ "settings.media.auto.comingSoon": "Not available yet — no built-in providers.",
512
+ "settings.media.auto.disabledTitle": "No built-in provider available — use Custom.",
513
+ "settings.media.action.edit": "Edit",
514
+ "settings.media.action.save": "Save",
515
+ "settings.media.action.cancel": "Cancel",
516
+ "settings.media.action.saving": "Saving…",
517
+ "settings.media.action.saved": "Saved",
518
+ "settings.media.apiKey.placeholder": "Enter API key",
519
+ "settings.media.apiKey.required": "API key required",
520
+ "settings.media.model.required": "Model name required",
521
+ "settings.media.baseUrl.required": "Base URL required",
493
522
  "settings.models.field.quicksetup": "Quick Setup",
494
523
  "settings.models.field.model": "Model",
495
524
  "settings.models.field.baseurl": "Base URL",
@@ -1180,6 +1209,35 @@ const I18n = (() => {
1180
1209
  "settings.models.empty": "暂未配置模型,点击「+ 添加模型」添加。",
1181
1210
  "settings.models.badge.default": "默认",
1182
1211
  "settings.models.badge.lite": "轻量",
1212
+ "settings.media.title": "媒体生成",
1213
+ "settings.media.desc": "可选。图片 / 视频 / 音频 生成模型。",
1214
+ "settings.media.loading": "加载中…",
1215
+ "settings.media.error": "加载失败:{{msg}}",
1216
+ "settings.media.kind.image": "图片",
1217
+ "settings.media.kind.video": "视频",
1218
+ "settings.media.kind.audio": "音频",
1219
+ "settings.media.source.off": "关闭",
1220
+ "settings.media.source.auto": "自动",
1221
+ "settings.media.source.custom": "自定义",
1222
+ "settings.media.field.model": "模型",
1223
+ "settings.media.field.baseUrl": "Base URL",
1224
+ "settings.media.field.apiKey": "API Key",
1225
+ "settings.media.field.provider":"服务商",
1226
+ "settings.media.off.hint": "已关闭。",
1227
+ "settings.media.auto.followsDefault": "跟随默认聊天模型",
1228
+ "settings.media.auto.noDefaultModel": "请先设置默认聊天模型。",
1229
+ "settings.media.auto.unsupported": "当前服务商无内置模型,请切换到「自定义」。",
1230
+ "settings.media.auto.comingSoon": "暂无内置服务商,敬请期待。",
1231
+ "settings.media.auto.disabledTitle": "暂无内置服务商,请使用自定义。",
1232
+ "settings.media.action.edit": "编辑",
1233
+ "settings.media.action.save": "保存",
1234
+ "settings.media.action.cancel": "取消",
1235
+ "settings.media.action.saving": "保存中…",
1236
+ "settings.media.action.saved": "已保存",
1237
+ "settings.media.apiKey.placeholder": "请输入 API Key",
1238
+ "settings.media.apiKey.required": "请填写 API Key",
1239
+ "settings.media.model.required": "请填写模型名称",
1240
+ "settings.media.baseUrl.required": "请填写 Base URL",
1183
1241
  "settings.models.field.quicksetup": "快速配置",
1184
1242
  "settings.models.field.model": "Model",
1185
1243
  "settings.models.field.baseurl": "Base URL",
@@ -771,6 +771,17 @@
771
771
  </div>
772
772
  <div id="model-cards"></div>
773
773
  </section>
774
+
775
+ <!-- Media generation section -->
776
+ <section class="settings-section" id="media-section">
777
+ <div class="settings-section-title">
778
+ <span data-i18n="settings.media.title">Media Generation</span>
779
+ </div>
780
+ <div class="settings-section-desc" data-i18n="settings.media.desc">
781
+ Generate images, video, and audio using your configured providers. "Auto" follows your default chat model.
782
+ </div>
783
+ <div id="media-rows"></div>
784
+ </section>
774
785
  </div>
775
786
 
776
787
  <!-- ══ Tab: UI ══ -->
@@ -1052,8 +1063,8 @@
1052
1063
  <div id="model-modal-test-result" class="model-test-result"></div>
1053
1064
 
1054
1065
  <label class="model-field model-field-checkbox" id="model-modal-default-field">
1055
- <input type="checkbox" id="model-modal-set-default">
1056
- <span data-i18n="settings.models.field.setDefault">Set as default model</span>
1066
+ <input type="checkbox" id="model-modal-set-default" class="field-checkbox">
1067
+ <span class="field-label" data-i18n="settings.models.field.setDefault">Set as default model</span>
1057
1068
  </label>
1058
1069
  </div>
1059
1070
  <div class="modal-footer">
@@ -1288,6 +1299,7 @@
1288
1299
  <script src="/skills.js"></script>
1289
1300
  <script src="/channels.js"></script>
1290
1301
  <script src="/mcp.js"></script>
1302
+ <script src="/model-tester.js"></script>
1291
1303
  <script src="/settings.js"></script>
1292
1304
  <script src="/billing.js"></script>
1293
1305
  <script src="/onboard.js"></script>
@@ -0,0 +1,58 @@
1
+ // Shared helpers for the model config UI flows.
2
+ // Used by both the onboarding wizard and the settings model modal.
3
+ window.ModelTester = (function () {
4
+ // Test a model connection.
5
+ // Returns one of:
6
+ // { ok: true, base_url, message } — connected, no rewrite
7
+ // { ok: true, base_url, message, rewrote: true } — connected, base_url auto-corrected (/v1 appended)
8
+ // { ok: false, message } — failed (server-reported or network)
9
+ async function testConnection({ model, base_url, api_key, anthropic_format, index } = {}) {
10
+ const body = { model, base_url, api_key };
11
+ if (typeof index === "number") body.index = index;
12
+ if (anthropic_format) body.anthropic_format = true;
13
+
14
+ let data;
15
+ try {
16
+ const res = await fetch("/api/config/test", {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify(body)
20
+ });
21
+ data = await res.json();
22
+ } catch (e) {
23
+ return { ok: false, message: e.message };
24
+ }
25
+
26
+ if (!data.ok) return { ok: false, message: data.message || "" };
27
+
28
+ if (data.effective_base_url && data.effective_base_url !== base_url) {
29
+ return { ok: true, base_url: data.effective_base_url, message: data.message || "", rewrote: true };
30
+ }
31
+ return { ok: true, base_url, message: data.message || "" };
32
+ }
33
+
34
+ // Persist a model config (create or update).
35
+ // existingId === null/undefined → POST /api/config/models (create).
36
+ // existingId === string → PATCH /api/config/models/:id (update).
37
+ // Returns { ok: bool, error? }.
38
+ async function saveModel(payload, { existingId } = {}) {
39
+ const url = existingId
40
+ ? `/api/config/models/${encodeURIComponent(existingId)}`
41
+ : "/api/config/models";
42
+ const method = existingId ? "PATCH" : "POST";
43
+
44
+ try {
45
+ const res = await fetch(url, {
46
+ method,
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify(payload)
49
+ });
50
+ const data = await res.json();
51
+ return data.ok ? { ok: true } : { ok: false, error: data.error || "" };
52
+ } catch (e) {
53
+ return { ok: false, error: e.message };
54
+ }
55
+ }
56
+
57
+ return { testConnection, saveModel };
58
+ })();
@@ -417,43 +417,30 @@ const Onboard = (() => {
417
417
  _setResult(null, "");
418
418
 
419
419
  // Step 1: test connection
420
- try {
421
- const res = await fetch("/api/config/test", {
422
- method: "POST",
423
- headers: { "Content-Type": "application/json" },
424
- body: JSON.stringify({ model, base_url: baseUrl, api_key: apiKey, index: 0 })
425
- });
426
- const data = await res.json();
427
- if (!data.ok) {
428
- _setResult(false, data.message || (zh ? "连接失败。" : "Connection failed."));
429
- btn.disabled = false;
430
- btn.textContent = I18n.t("onboard.key.btn.test");
431
- return;
432
- }
433
- } catch (e) {
434
- _setResult(false, e.message);
420
+ const testResult = await ModelTester.testConnection({
421
+ model, base_url: baseUrl, api_key: apiKey, index: 0
422
+ });
423
+
424
+ if (!testResult.ok) {
425
+ _setResult(false, testResult.message || (zh ? "连接失败。" : "Connection failed."));
435
426
  btn.disabled = false;
436
427
  btn.textContent = I18n.t("onboard.key.btn.test");
437
428
  return;
438
429
  }
439
430
 
431
+ let effectiveBaseUrl = testResult.base_url;
432
+ if (testResult.rewrote) {
433
+ const baseInput = document.getElementById("setup-base-url");
434
+ if (baseInput) baseInput.value = effectiveBaseUrl;
435
+ }
436
+
440
437
  // Step 2: save config
441
438
  btn.textContent = I18n.t("onboard.key.saving");
442
- try {
443
- const res = await fetch("/api/config/models", {
444
- method: "POST",
445
- headers: { "Content-Type": "application/json" },
446
- body: JSON.stringify({ type: "default", model, base_url: baseUrl, api_key: apiKey, anthropic_format: false })
447
- });
448
- const data = await res.json();
449
- if (!data.ok) {
450
- _setResult(false, data.error || (zh ? "保存失败。" : "Save failed."));
451
- btn.disabled = false;
452
- btn.textContent = I18n.t("onboard.key.btn.test");
453
- return;
454
- }
455
- } catch (e) {
456
- _setResult(false, e.message);
439
+ const saveResult = await ModelTester.saveModel({
440
+ type: "default", model, base_url: effectiveBaseUrl, api_key: apiKey
441
+ });
442
+ if (!saveResult.ok) {
443
+ _setResult(false, saveResult.error || (zh ? "保存失败。" : "Save failed."));
457
444
  btn.disabled = false;
458
445
  btn.textContent = I18n.t("onboard.key.btn.test");
459
446
  return;