heartcode 0.1.15 → 0.1.24
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.
- package/CHANGELOG.md +186 -0
- package/SKILL.md +5 -3
- package/bin/heartcode.js +199 -71
- package/package.json +1 -1
- package/prompts/agent.md +147 -17
- package/prompts/guided.md +6 -3
- package/src/acp.js +2 -2
- package/src/agent.js +418 -34
- package/src/client.js +59 -3
- package/src/config.js +11 -6
- package/src/headless.js +1 -1
- package/src/lsp.js +168 -49
- package/src/mcp.js +20 -2
- package/src/sessions.js +2 -3
- package/src/settings.html +262 -131
- package/src/settings.js +11 -2
- package/src/skills.js +20 -1
- package/src/tabs.js +22 -7
- package/src/tools.js +68 -6
- package/src/ui.js +525 -94
- package/src/voice.js +70 -6
- package/src/web.js +211 -13
package/src/settings.html
CHANGED
|
@@ -4,55 +4,95 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>HeartCode Settings</title>
|
|
7
|
+
<script>
|
|
8
|
+
// light or dark: chosen on the page (kept in this browser), else as the system is set
|
|
9
|
+
try { const t = localStorage.getItem('heart-theme'); if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t } catch {}
|
|
10
|
+
</script>
|
|
7
11
|
<style>
|
|
12
|
+
/* One accent (green), one cool-gray family; red only for what failed. */
|
|
8
13
|
:root {
|
|
9
|
-
--bg: #
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
+
--bg: #f6f6f8; --side: #fbfbfc; --panel: #ffffff; --panel2: #fafafb; --text: #17181c; --muted: #5d616c; --faint: #8d919b;
|
|
15
|
+
--line: #e7e8ec; --line-strong: #d6d8de; --hover: #f2f3f5; --input: #ffffff;
|
|
16
|
+
--accent: #16824c; --accent-soft: #e8f5ee; --on-accent: #ffffff;
|
|
17
|
+
--brand: var(--accent); --brand-soft: var(--accent-soft); --on-brand: var(--on-accent);
|
|
18
|
+
--ok: #1d8a4b; --ok-soft: #e7f5ec; --err: #d1242f; --err-soft: #fdecee;
|
|
19
|
+
--ring: 0 0 0 3px rgba(22, 130, 76, .18);
|
|
20
|
+
--shadow: 0 1px 2px rgba(20, 22, 30, .04), 0 8px 24px rgba(20, 22, 30, .06);
|
|
21
|
+
--shadow-pop: 0 24px 64px rgba(20, 22, 30, .18), 0 2px 6px rgba(20, 22, 30, .06);
|
|
22
|
+
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
|
|
23
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
24
|
+
--radius: 14px; --radius-s: 8px;
|
|
25
|
+
color-scheme: light;
|
|
14
26
|
}
|
|
27
|
+
/* dark: as the system is set (unless Light is chosen on the page), or Dark chosen */
|
|
15
28
|
@media (prefers-color-scheme: dark) {
|
|
16
|
-
:root {
|
|
17
|
-
--bg: #
|
|
18
|
-
--
|
|
19
|
-
--
|
|
29
|
+
:root:not([data-theme=light]) {
|
|
30
|
+
--bg: #0c0d10; --side: #0f1014; --panel: #14151a; --panel2: #17191e; --text: #ececf0; --muted: #a2a6b0; --faint: #6d717c;
|
|
31
|
+
--line: #23252c; --line-strong: #2f323a; --hover: #1c1e24; --input: #101115;
|
|
32
|
+
--accent: #5ccf8a; --accent-soft: rgba(92, 207, 138, .13); --on-accent: #07140d;
|
|
33
|
+
--ok: #62cf8b; --ok-soft: rgba(98, 207, 139, .12); --err: #ff7a7a; --err-soft: rgba(255, 122, 122, .12);
|
|
34
|
+
--ring: 0 0 0 3px rgba(92, 207, 138, .25);
|
|
35
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, .3);
|
|
36
|
+
--shadow-pop: 0 28px 72px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .03);
|
|
37
|
+
color-scheme: dark;
|
|
20
38
|
}
|
|
21
39
|
}
|
|
40
|
+
:root[data-theme=dark] {
|
|
41
|
+
--bg: #0c0d10; --side: #0f1014; --panel: #14151a; --panel2: #17191e; --text: #ececf0; --muted: #a2a6b0; --faint: #6d717c;
|
|
42
|
+
--line: #23252c; --line-strong: #2f323a; --hover: #1c1e24; --input: #101115;
|
|
43
|
+
--accent: #5ccf8a; --accent-soft: rgba(92, 207, 138, .13); --on-accent: #07140d;
|
|
44
|
+
--ok: #62cf8b; --ok-soft: rgba(98, 207, 139, .12); --err: #ff7a7a; --err-soft: rgba(255, 122, 122, .12);
|
|
45
|
+
--ring: 0 0 0 3px rgba(92, 207, 138, .25);
|
|
46
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, .3);
|
|
47
|
+
--shadow-pop: 0 28px 72px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .03);
|
|
48
|
+
color-scheme: dark;
|
|
49
|
+
}
|
|
22
50
|
* { box-sizing: border-box; }
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
html { scroll-behavior: smooth; }
|
|
52
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
|
53
|
+
a { color: var(--accent); text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent); text-underline-offset: 2px; }
|
|
54
|
+
a:hover { text-decoration-color: currentColor; }
|
|
55
|
+
::selection { background: var(--accent-soft); }
|
|
25
56
|
button, input, select, textarea { font: inherit; color: inherit; }
|
|
57
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
26
58
|
[hidden] { display: none !important; }
|
|
27
59
|
|
|
28
60
|
/* the app: pages on the left, one page at a time on the right, using the whole width */
|
|
29
61
|
.app { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; }
|
|
30
|
-
aside { border-right: 1px solid var(--line); padding:
|
|
31
|
-
.brand { display: flex; align-items:
|
|
32
|
-
.brand span { color: var(--
|
|
33
|
-
|
|
62
|
+
aside { border-right: 1px solid var(--line); padding: 22px 14px 16px; position: sticky; top: 0; height: 100vh; overflow-y: auto; background: var(--side); display: flex; flex-direction: column; }
|
|
63
|
+
.brand { display: flex; align-items: baseline; gap: 8px; font-weight: 650; font-size: 15px; letter-spacing: -.01em; padding: 0 10px 22px; }
|
|
64
|
+
.brand span { color: var(--accent); font-size: 17px; }
|
|
65
|
+
.brand small { color: var(--faint); font-weight: 500; font-size: 13px; }
|
|
66
|
+
nav a { position: relative; display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: var(--radius-s); color: var(--muted); text-decoration: none; margin-bottom: 1px; font-weight: 500; transition: background .15s, color .15s; }
|
|
34
67
|
nav a:hover { background: var(--hover); color: var(--text); }
|
|
35
|
-
nav a.on { background: var(--
|
|
36
|
-
nav a
|
|
37
|
-
nav a .
|
|
68
|
+
nav a.on { background: var(--panel); color: var(--text); font-weight: 600; box-shadow: var(--shadow), inset 0 0 0 1px var(--line); }
|
|
69
|
+
nav a.on::before { content: ''; position: absolute; left: -14px; top: 9px; bottom: 9px; width: 3px; border-radius: 0 3px 3px 0; background: var(--accent); }
|
|
70
|
+
nav a .ic { width: 18px; text-align: center; opacity: .7; font-size: 13px; }
|
|
71
|
+
nav a.on .ic { color: var(--accent); opacity: 1; }
|
|
72
|
+
nav a .count { margin-left: auto; font-size: 11.5px; color: var(--faint); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
38
73
|
.using:empty { visibility: hidden; } /* until your settings are in */
|
|
39
|
-
.using { margin: auto
|
|
74
|
+
.using { margin: auto 0 0; padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px; font-size: 12px; background: var(--panel); }
|
|
40
75
|
.using .k { color: var(--faint); display: block; }
|
|
41
|
-
.using b { display: block; font-family: var(--mono); font-size: 12.5px; word-break: break-all; margin:
|
|
42
|
-
|
|
76
|
+
.using b { display: block; font-family: var(--mono); font-size: 12.5px; font-weight: 600; word-break: break-all; margin: 3px 0 2px; color: var(--text); }
|
|
77
|
+
.using a { font-weight: 600; }
|
|
78
|
+
.theme-pick { display: flex; margin-top: 10px; height: 34px; }
|
|
79
|
+
.theme-pick label { flex: 1; justify-content: center; font-size: 12.5px; }
|
|
80
|
+
.using:empty + .theme-pick { margin-top: auto; }
|
|
81
|
+
main { padding: 36px 44px 72px; width: 100%; max-width: 1640px; min-width: 0; }
|
|
43
82
|
.page { display: none; }
|
|
44
|
-
.page.on { display: block; }
|
|
45
|
-
|
|
83
|
+
.page.on { display: block; animation: page-in .22s ease-out; }
|
|
84
|
+
@keyframes page-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
85
|
+
.head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px 24px; margin-bottom: 24px; flex-wrap: wrap; }
|
|
46
86
|
.disk { text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
|
|
47
|
-
.disk b { font-size: 22px; }
|
|
87
|
+
.disk b { font-size: 22px; font-weight: 650; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
|
|
48
88
|
.disk .k { color: var(--muted); font-size: 12.5px; }
|
|
49
|
-
h1 { font-size: 24px; margin: 0 0
|
|
50
|
-
.lead { color: var(--muted); margin: 0; max-width:
|
|
51
|
-
h2 { font-size: 15px; margin: 0 0 4px; }
|
|
52
|
-
h3 { font-size:
|
|
53
|
-
.sub { color: var(--muted); margin: 0 0
|
|
89
|
+
h1 { font-size: 24px; font-weight: 680; letter-spacing: -.025em; line-height: 1.2; margin: 0 0 6px; }
|
|
90
|
+
.lead { color: var(--muted); margin: 0; max-width: 68ch; font-size: 13.5px; text-wrap: pretty; }
|
|
91
|
+
h2 { font-size: 15px; font-weight: 620; letter-spacing: -.01em; margin: 0 0 4px; }
|
|
92
|
+
h3 { font-size: 12.5px; font-weight: 600; margin: 0 0 8px; color: var(--muted); }
|
|
93
|
+
.sub { color: var(--muted); margin: 0 0 16px; font-size: 13px; text-wrap: pretty; }
|
|
54
94
|
|
|
55
|
-
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding:
|
|
95
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 22px 24px 24px; }
|
|
56
96
|
.stack { display: flex; flex-direction: column; gap: 18px; }
|
|
57
97
|
/* two columns: the work on the left, a panel on the right */
|
|
58
98
|
.split { display: grid; grid-template-columns: minmax(0, 1fr) 400px; gap: 24px; align-items: start; }
|
|
@@ -65,25 +105,26 @@
|
|
|
65
105
|
.cols > .stack { display: flex; flex-direction: column; }
|
|
66
106
|
.cols > .stack > .card:last-child { flex: 1; } /* both columns end at the same line */
|
|
67
107
|
/* two or three choices in one control (Command | URL) */
|
|
68
|
-
.seg { display: inline-flex;
|
|
69
|
-
.seg label { display: flex; align-items: center; padding: 0
|
|
70
|
-
.seg label
|
|
71
|
-
.seg label.on { background: var(--
|
|
108
|
+
.seg { display: inline-flex; padding: 3px; gap: 2px; border-radius: 10px; background: var(--hover); height: 36px; }
|
|
109
|
+
.seg label { display: flex; align-items: center; padding: 0 12px; border-radius: 7px; cursor: pointer; color: var(--muted); font-weight: 500; font-size: 13px; transition: background .15s, color .15s; }
|
|
110
|
+
.seg label:hover { color: var(--text); }
|
|
111
|
+
.seg label.on { background: var(--panel); color: var(--text); font-weight: 600; box-shadow: var(--shadow), inset 0 0 0 1px var(--line); }
|
|
72
112
|
.seg input { position: absolute; opacity: 0; width: 0; }
|
|
73
113
|
/* a list on the left, what's chosen on the right */
|
|
74
114
|
.md { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 20px; align-items: start; }
|
|
75
115
|
.md .list { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; position: sticky; top: 24px; max-height: calc(100vh - 170px); display: flex; flex-direction: column; }
|
|
76
116
|
.md .list .top { padding: 12px; border-bottom: 1px solid var(--line); display: flex; gap: 8px; }
|
|
77
117
|
.md .list .items { overflow-y: auto; }
|
|
78
|
-
.md .group { padding:
|
|
79
|
-
.md .it { display: block; width: 100%; text-align: left; border: 0; border-radius: 0; background: none; padding: 9px
|
|
118
|
+
.md .group { padding: 14px 16px 6px; font-size: 12px; font-weight: 600; color: var(--faint); }
|
|
119
|
+
.md .it { display: block; width: 100%; text-align: left; border: 0; border-radius: 0; background: none; padding: 9px 16px; box-shadow: inset 3px 0 0 transparent; cursor: pointer; transition: background .12s; }
|
|
80
120
|
.md .it:hover { background: var(--hover); }
|
|
81
|
-
.md .it.on { background: var(--accent-soft);
|
|
121
|
+
.md .it.on { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
|
|
82
122
|
.md .it .t { display: flex; gap: 6px; align-items: center; font-weight: 600; font-size: 13px; word-break: break-all; }
|
|
83
123
|
.md .it .d { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
84
124
|
.md .detail { min-width: 0; }
|
|
85
125
|
.md.solo { grid-template-columns: minmax(0, 1fr); } /* nothing to list: the panel gets the width */
|
|
86
126
|
.md.solo .list { display: none; }
|
|
127
|
+
.md.solo .detail > .card { height: auto !important; }
|
|
87
128
|
/* a long list (the prompts): list and editor fill the screen's height, like an editor app */
|
|
88
129
|
/* a list (or tabs) and its editor fill the screen below the page's title, like an editor app;
|
|
89
130
|
the height is set from where it starts (fillHeight) */
|
|
@@ -98,51 +139,60 @@
|
|
|
98
139
|
.md.tabs .list { position: static; max-height: none; background: none; border: 0; overflow: visible; }
|
|
99
140
|
.md.tabs .items { display: flex; flex-wrap: wrap; gap: 8px; overflow: visible; }
|
|
100
141
|
.md.tabs .group { display: none; }
|
|
101
|
-
.md.tabs .it { width: auto; border: 1px solid var(--line); border-radius:
|
|
102
|
-
.md.tabs .it
|
|
142
|
+
.md.tabs .it { width: auto; border: 1px solid var(--line); border-radius: 10px; padding: 7px 14px; background: var(--panel); box-shadow: none; }
|
|
143
|
+
.md.tabs .it:hover { border-color: var(--line-strong); }
|
|
144
|
+
.md.tabs .it.on { border-color: var(--accent); background: var(--accent-soft); box-shadow: none; }
|
|
103
145
|
.md.tabs .it .d { display: none; }
|
|
104
146
|
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--brand); flex: none; }
|
|
105
147
|
|
|
106
|
-
label.f { display: block; font-size: 12.5px; font-weight: 600; margin:
|
|
107
|
-
.hint { color: var(--faint); font-size: 12.5px; margin-top:
|
|
148
|
+
label.f { display: block; font-size: 12.5px; font-weight: 600; margin: 16px 0 6px; }
|
|
149
|
+
.hint { color: var(--faint); font-size: 12.5px; margin-top: 7px; text-wrap: pretty; }
|
|
108
150
|
input[type=text], input[type=password], input[type=number], input[type=search], select, textarea {
|
|
109
|
-
width: 100%; padding: 8px
|
|
151
|
+
width: 100%; padding: 8px 12px; border: 1px solid var(--line-strong); border-radius: var(--radius-s); background: var(--input);
|
|
152
|
+
transition: border-color .15s, box-shadow .15s;
|
|
110
153
|
}
|
|
111
|
-
input[type=text], input[type=password], input[type=number], input[type=search], select { height:
|
|
112
|
-
textarea {
|
|
154
|
+
input[type=text], input[type=password], input[type=number], input[type=search], select { height: 36px; }
|
|
155
|
+
input::placeholder, textarea::placeholder { color: var(--faint); }
|
|
156
|
+
input[type=text]:hover, input[type=password]:hover, input[type=number]:hover, input[type=search]:hover, select:hover, textarea:hover { border-color: color-mix(in srgb, var(--line-strong) 60%, var(--muted)); }
|
|
157
|
+
select { appearance: none; padding-right: 34px; cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%238d919b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5l3 3 3-3'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; }
|
|
158
|
+
textarea { font-family: var(--mono); font-size: 12.5px; resize: vertical; tab-size: 2; line-height: 1.6; display: block; }
|
|
113
159
|
textarea.editor { min-height: 96px; overflow-y: auto; } /* grows with its text (grow()), up to most of the screen */
|
|
114
|
-
input:focus, select:focus, textarea:focus { outline:
|
|
115
|
-
|
|
116
|
-
button:
|
|
160
|
+
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
|
|
161
|
+
input[type=checkbox] { accent-color: var(--accent); }
|
|
162
|
+
button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 7px 14px; line-height: 18px; border-radius: var(--radius-s); border: 1px solid var(--line-strong); background: var(--panel); cursor: pointer; white-space: nowrap; font-weight: 500; transition: background .15s, border-color .15s, color .15s, transform .08s, filter .15s; }
|
|
163
|
+
button:hover:not(:disabled) { background: var(--hover); }
|
|
164
|
+
button:active:not(:disabled) { transform: translateY(1px); }
|
|
117
165
|
button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
|
|
118
|
-
button.primary:hover:not(:disabled) { filter: brightness(1.
|
|
166
|
+
button.primary:hover:not(:disabled) { background: var(--accent); filter: brightness(1.07); }
|
|
119
167
|
button.danger { color: var(--err); }
|
|
120
|
-
button.
|
|
168
|
+
button.danger:hover:not(:disabled) { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 35%, transparent); }
|
|
169
|
+
button.small { padding: 4px 10px; font-size: 12.5px; }
|
|
121
170
|
button.ghost { border-color: transparent; background: transparent; color: var(--muted); }
|
|
122
|
-
button:disabled {
|
|
171
|
+
button.ghost:hover:not(:disabled) { color: var(--text); background: var(--hover); }
|
|
172
|
+
button:disabled { opacity: .45; cursor: default; }
|
|
123
173
|
.row { display: flex; gap: 12px; align-items: end; flex-wrap: wrap; }
|
|
124
174
|
.row > .grow { flex: 1; min-width: 180px; }
|
|
125
175
|
.actions { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; align-items: center; }
|
|
126
176
|
.actions .spacer { flex: 1; }
|
|
127
177
|
.mono { font-family: var(--mono); font-size: 12.5px; }
|
|
128
178
|
.muted { color: var(--muted); }
|
|
129
|
-
.tag { display: inline-block; white-space: nowrap; font-size: 11px; font-weight: 600; padding:
|
|
130
|
-
.tag.brand { background: var(--
|
|
179
|
+
.tag { display: inline-block; white-space: nowrap; font-size: 11px; font-weight: 600; line-height: 18px; padding: 0 7px; border-radius: 6px; background: var(--hover); color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
180
|
+
.tag.brand { background: var(--accent-soft); color: var(--accent); }
|
|
131
181
|
.tag.ok { background: var(--ok-soft); color: var(--ok); }
|
|
132
182
|
.tag.err { background: var(--err-soft); color: var(--err); }
|
|
133
|
-
.empty { text-align: center; padding:
|
|
183
|
+
.empty { text-align: center; padding: 36px 16px; color: var(--muted); }
|
|
134
184
|
.empty b { display: block; color: var(--text); font-size: 15px; margin-bottom: 4px; }
|
|
135
185
|
|
|
136
186
|
/* first start: the steps, then adding a provider */
|
|
137
187
|
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 22px; }
|
|
138
|
-
.step { display: flex; gap: 12px; align-items: center; padding:
|
|
139
|
-
.step .num { width: 28px; height: 28px; border-radius:
|
|
188
|
+
.step { display: flex; gap: 12px; align-items: center; padding: 15px 16px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); color: var(--muted); font-size: 13px; }
|
|
189
|
+
.step .num { width: 28px; height: 28px; border-radius: 9px; display: grid; place-items: center; font-weight: 700; background: var(--hover); color: var(--muted); flex: none; }
|
|
140
190
|
.step b { display: block; color: var(--text); }
|
|
141
191
|
.step.done .num { background: var(--ok-soft); color: var(--ok); }
|
|
142
192
|
.step.now { border-color: var(--brand); }
|
|
143
193
|
.step.now .num { background: var(--brand); color: var(--on-brand); }
|
|
144
194
|
.presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 10px; }
|
|
145
|
-
.preset { text-align: left; padding: 12px 14px; border-radius:
|
|
195
|
+
.preset { display: block; text-align: left; padding: 12px 14px; border-radius: 12px; }
|
|
146
196
|
.preset b { display: block; }
|
|
147
197
|
.preset span { color: var(--muted); font-size: 12px; }
|
|
148
198
|
.preset.on { border-color: var(--accent); background: var(--accent-soft); }
|
|
@@ -151,7 +201,7 @@
|
|
|
151
201
|
.head-actions { display: flex; gap: 8px; align-items: center; }
|
|
152
202
|
.work { display: flex; flex-direction: column; }
|
|
153
203
|
.work > .card { flex: 1; padding: 0; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
|
154
|
-
.cat-top { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
|
|
204
|
+
.cat-top { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
155
205
|
.cat-top input[type=search] { flex: 1; min-width: 140px; }
|
|
156
206
|
.cat-top select { flex: none; width: auto; padding-right: 30px; }
|
|
157
207
|
.cat-top button { flex: none; }
|
|
@@ -159,7 +209,7 @@
|
|
|
159
209
|
.cat-top button.ghost:hover:not(:disabled) { color: var(--text); border-color: transparent; }
|
|
160
210
|
.cat-top input[type=checkbox], .prov-h .pick-all, .mwrap .pick, .free-only input { flex: none; width: 16px; height: 16px; margin: 0; accent-color: var(--accent); cursor: pointer; }
|
|
161
211
|
/* what to do with the ticked models: at the bottom of the list, only while some are ticked */
|
|
162
|
-
.picks { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-top: 1px solid var(--line); background: var(--accent-soft); font-size: 13px; }
|
|
212
|
+
.picks { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-top: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line)); background: var(--accent-soft); font-size: 13px; animation: page-in .18s ease-out; }
|
|
163
213
|
.picks .n { font-weight: 600; margin-right: auto; }
|
|
164
214
|
.prov-h { cursor: pointer; }
|
|
165
215
|
.prov-h .fold { border: 0; background: none; color: var(--muted); padding: 2px 4px; font-size: 16px; line-height: 1; transition: transform .15s; }
|
|
@@ -167,36 +217,47 @@
|
|
|
167
217
|
section.collapsed .prov-h .fold { transform: rotate(-90deg); }
|
|
168
218
|
section.collapsed > :not(.prov-h) { display: none; } /* folded: just its header */
|
|
169
219
|
.cat-list { flex: 1; overflow-y: auto; }
|
|
170
|
-
.prov-h { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 10px; padding: 10px 16px; background: var(--panel2); border-bottom: 1px solid var(--line); }
|
|
220
|
+
.prov-h { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 10px; padding: 10px 16px; min-height: 52px; background: var(--panel2); border-bottom: 1px solid var(--line); transition: background .12s; }
|
|
221
|
+
.prov-h:hover { background: color-mix(in srgb, var(--panel2) 70%, var(--hover)); }
|
|
171
222
|
.prov-h .who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; overflow: hidden; }
|
|
172
|
-
.prov-h .name { font-weight:
|
|
173
|
-
.prov-h .meta { font-size:
|
|
223
|
+
.prov-h .name { font-weight: 650; font-size: 14.5px; letter-spacing: -.01em; }
|
|
224
|
+
.prov-h .meta { font-family: var(--mono); font-size: 11.5px; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; margin-left: 4px; }
|
|
174
225
|
.prov-h .meta .mono { font-size: 12px; }
|
|
175
226
|
.rows { display: flex; flex-direction: column; } /* one model per line, A to Z */
|
|
176
|
-
.mrow { break-inside: avoid; display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 9px 16px; border: 0; border-radius: 0; border-bottom: 1px solid var(--line); background: none; cursor: pointer; min-width: 0; }
|
|
177
|
-
.mrow:hover { background:
|
|
227
|
+
.mrow { break-inside: avoid; display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 9px 16px; border: 0; border-radius: 0; border-bottom: 1px solid var(--line); background: none; cursor: pointer; min-width: 0; font-weight: 400; transform: none !important; }
|
|
228
|
+
.mrow:hover:not(:disabled) { background: transparent; }
|
|
178
229
|
.mrow.flash { animation: flash 1.6s ease-out; }
|
|
179
230
|
@keyframes flash { from { background: var(--brand-soft); box-shadow: inset 3px 0 0 var(--brand); } to { background: none; box-shadow: none; } }
|
|
180
|
-
.mrow .id { flex: 1; min-width: 0; font-family: var(--mono); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
231
|
+
.mrow .id { flex: 1; min-width: 0; font-family: var(--mono); font-size: 12.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
181
232
|
.rows > .mrow, .rows > .mwrap { height: 56px; } /* one height: a long list only draws what's in view */
|
|
182
233
|
.mwrap .mrow { height: 100%; }
|
|
183
234
|
.mrow .can { display: block; margin-top: 2px; color: var(--muted); font-family: system-ui, sans-serif; font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; }
|
|
184
|
-
.mrow .add { color: var(--accent); font-size: 12.5px; font-weight: 600; white-space: nowrap; }
|
|
235
|
+
.mrow .add { color: var(--accent); font-size: 12.5px; font-weight: 600; white-space: nowrap; opacity: .55; transition: opacity .12s; }
|
|
236
|
+
.mwrap:hover .mrow .add, .mrow:focus-visible .add { opacity: 1; }
|
|
185
237
|
.mrow.taken .id { color: var(--muted); }
|
|
186
|
-
.mwrap { display: flex; break-inside: avoid; border-bottom: 1px solid var(--line); }
|
|
238
|
+
.mwrap { display: flex; break-inside: avoid; border-bottom: 1px solid var(--line); transition: background .12s; }
|
|
239
|
+
.mwrap:hover { background: var(--hover); }
|
|
187
240
|
.mwrap .mrow { border-bottom: 0; }
|
|
188
241
|
.mwrap .forget { border: 0; background: none; color: var(--faint); padding: 0 12px; }
|
|
189
|
-
|
|
242
|
+
/* a row's buttons: quiet until the row is pointed at */
|
|
243
|
+
.mwrap .try { flex: none; margin-right: 12px; border-color: transparent; background: transparent; color: var(--muted); }
|
|
244
|
+
.mwrap:hover .try { border-color: var(--line-strong); background: var(--panel); color: var(--text); }
|
|
190
245
|
.prov-h .by-name { display: flex; gap: 6px; flex: none; }
|
|
191
|
-
.prov-h .
|
|
246
|
+
.prov-h .sw { margin-top: 0; }
|
|
247
|
+
section.off .rows, section.off .prov-h .who { opacity: .45; }
|
|
248
|
+
.mtable li.off > :not(.grip, input, button) { opacity: .45; }
|
|
249
|
+
.prov-h .by-name input { width: 190px; height: 28px; padding: 0 9px; font-size: 12px; font-family: var(--mono); background: transparent; }
|
|
192
250
|
.prov-h .by-name button, .prov-h > button.small { height: 28px; }
|
|
193
251
|
.hello-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
|
|
194
|
-
.hello-cols pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; background: var(--panel2); border: 1px solid var(--line); border-radius:
|
|
252
|
+
.hello-cols pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; line-height: 1.55; background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; max-height: 55vh; overflow: auto; }
|
|
195
253
|
#hello-status.ok { color: var(--ok); }
|
|
196
254
|
#hello-status.bad { color: var(--err); }
|
|
255
|
+
dialog#gone { width: min(560px, 92vw); }
|
|
256
|
+
dialog#gone h2 { color: var(--err); font-family: inherit; font-size: 18px; }
|
|
257
|
+
dialog#gone::backdrop { background: rgba(0, 0, 0, .75); }
|
|
197
258
|
@media (max-width: 860px) { .hello-cols { grid-template-columns: 1fr; } }
|
|
198
|
-
#hellos-list { max-height: 60vh; overflow: auto; border: 1px solid var(--line); border-radius:
|
|
199
|
-
#hellos-list button { display: flex; gap: 12px; width: 100%; text-align: left; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: none; padding:
|
|
259
|
+
#hellos-list { max-height: 60vh; overflow: auto; border: 1px solid var(--line); border-radius: 12px; }
|
|
260
|
+
#hellos-list button { display: flex; justify-content: flex-start; gap: 12px; width: 100%; text-align: left; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: none; padding: 10px 14px; font-size: 12.5px; font-weight: 400; }
|
|
200
261
|
#hellos-list button:last-child { border-bottom: 0; }
|
|
201
262
|
#hellos-list .id { flex: none; width: 40%; font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
202
263
|
#hellos-list .said { flex: 1; min-width: 0; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -216,10 +277,10 @@
|
|
|
216
277
|
.reqs { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); min-height: 420px; }
|
|
217
278
|
.req-list { max-height: 70vh; overflow-y: auto; border-right: 1px solid var(--line); }
|
|
218
279
|
.req-detail { max-height: 70vh; overflow-y: auto; padding: 16px 18px; min-width: 0; }
|
|
219
|
-
.rq { display: block; width: 100%; text-align: left; border: 0; border-radius: 0; border-bottom: 1px solid var(--line);
|
|
220
|
-
.rq:hover { background: var(--hover); }
|
|
221
|
-
.rq.on { background: var(--accent-soft);
|
|
222
|
-
.rq.failed {
|
|
280
|
+
.rq { display: block; width: 100%; text-align: left; border: 0; border-radius: 0; border-bottom: 1px solid var(--line); box-shadow: inset 3px 0 0 transparent; background: none; padding: 11px 16px; cursor: pointer; font-weight: 400; transform: none !important; }
|
|
281
|
+
.rq:hover:not(:disabled) { background: var(--hover); }
|
|
282
|
+
.rq.on, .rq.on:hover:not(:disabled) { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
|
|
283
|
+
.rq.failed { box-shadow: inset 3px 0 0 var(--err); }
|
|
223
284
|
.rq.failed.on { background: var(--err-soft); }
|
|
224
285
|
.rq .l1 { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
225
286
|
.rq .m { flex: 1; min-width: 0; font-family: var(--mono); font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -228,7 +289,7 @@
|
|
|
228
289
|
.kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 14px; font-size: 13px; margin: 8px 0 14px; }
|
|
229
290
|
.kv dt { color: var(--muted); }
|
|
230
291
|
.kv dd { margin: 0; overflow-wrap: anywhere; }
|
|
231
|
-
.req-detail pre { white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; background: var(--panel2); border: 1px solid var(--line); border-radius:
|
|
292
|
+
.req-detail pre { white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; line-height: 1.55; background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 6px 0 14px; max-height: 360px; overflow: auto; }
|
|
232
293
|
.req-detail details { margin: 4px 0; }
|
|
233
294
|
.req-detail summary { cursor: pointer; font-size: 13px; color: var(--muted); }
|
|
234
295
|
.req-detail h3 { font-size: 14px; margin: 16px 0 4px; }
|
|
@@ -241,15 +302,18 @@
|
|
|
241
302
|
.olist .test button { flex: none; }
|
|
242
303
|
.olist .test.ok { color: var(--ok); }
|
|
243
304
|
.olist .test.warn { color: var(--accent); }
|
|
244
|
-
dialog { width: 70vw; max-width: none; max-height: calc(100vh - 32px); overflow: auto; border: 1px solid var(--line); border-radius:
|
|
245
|
-
dialog
|
|
246
|
-
|
|
305
|
+
dialog { width: 70vw; max-width: none; max-height: calc(100vh - 32px); overflow: auto; border: 1px solid var(--line); border-radius: 18px; background: var(--panel); color: var(--text); padding: 26px 28px 24px; box-shadow: var(--shadow-pop); }
|
|
306
|
+
dialog[open] { animation: pop-in .18s cubic-bezier(.2, .8, .3, 1); }
|
|
307
|
+
@keyframes pop-in { from { opacity: 0; transform: translateY(6px) scale(.985); } to { opacity: 1; transform: none; } }
|
|
308
|
+
dialog::backdrop { background: rgba(8, 9, 12, .55); backdrop-filter: blur(3px); }
|
|
309
|
+
dialog h2 { margin: 0 0 6px; font-size: 18px; font-weight: 650; letter-spacing: -.015em; overflow-wrap: anywhere; }
|
|
247
310
|
dialog#ms .opt:last-of-type { border-bottom: 0; }
|
|
248
311
|
#pr-list { list-style: none; margin: 12px 0 0; padding: 0; counter-reset: rank; }
|
|
249
|
-
#pr-list li { counter-increment: rank; padding:
|
|
312
|
+
#pr-list li { counter-increment: rank; padding: 10px 14px; margin-bottom: 6px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--panel2); cursor: grab; user-select: none; font-weight: 500; transition: border-color .12s; }
|
|
313
|
+
#pr-list li:hover { border-color: var(--accent); }
|
|
250
314
|
#pr-list li::before { content: counter(rank) ". ⋮⋮ "; color: var(--muted); }
|
|
251
315
|
#pr-list li.dragging { opacity: .4; }
|
|
252
|
-
dialog .actions { display: flex; justify-content: flex-end; gap: 8px; margin-top:
|
|
316
|
+
dialog .actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
|
|
253
317
|
.olist .test.failed { color: var(--err); }
|
|
254
318
|
.olist .test.testing, .olist .test.waiting { color: var(--accent); }
|
|
255
319
|
.olist .test button { padding: 0 7px; font-size: 11px; }
|
|
@@ -274,9 +338,9 @@
|
|
|
274
338
|
body.sorting, body.sorting * { cursor: grabbing !important; }
|
|
275
339
|
/* Your models: a table, one row a model */
|
|
276
340
|
.mt-card { padding: 0; }
|
|
277
|
-
.mt-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 14px 18px; border-bottom: 1px solid var(--line); min-height:
|
|
341
|
+
.mt-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 14px 18px; border-bottom: 1px solid var(--line); min-height: 60px; }
|
|
278
342
|
.mt-top .sorts { flex: 1 1 320px; min-width: 0; color: var(--muted); font-size: 13px; } /* too narrow beside the buttons: a line of its own */
|
|
279
|
-
.mt-top .chip { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--
|
|
343
|
+
.mt-top .chip { display: inline-flex; align-items: center; gap: 4px; border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); background: var(--accent-soft); border-radius: 8px; padding: 1px 3px 1px 9px; margin: 2px 4px 2px 0; color: var(--text); font-size: 12.5px; }
|
|
280
344
|
.mt-top .chip button { border: 0; background: none; padding: 0 4px; font-size: 12px; color: var(--muted); }
|
|
281
345
|
.mt-scroll { overflow-x: auto; }
|
|
282
346
|
.mt { min-width: 1116px; }
|
|
@@ -290,7 +354,7 @@
|
|
|
290
354
|
.mt-outcomes { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 10px 16px; border-bottom: 1px solid var(--line); font-size: 12.5px; color: var(--muted); }
|
|
291
355
|
.mt-outcomes > span:first-child { margin-right: 4px; }
|
|
292
356
|
/* a test result: what it was and how many (a click selects them), and ↻ to test them again */
|
|
293
|
-
.pill { display: inline-flex; align-items: stretch; border: 1px solid var(--line); border-radius:
|
|
357
|
+
.pill { display: inline-flex; align-items: stretch; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--panel); overflow: hidden; font-size: 12px; line-height: 1; }
|
|
294
358
|
.pill button { border: 0; border-radius: 0; background: none; padding: 5px 8px; display: inline-flex; align-items: center; gap: 5px; }
|
|
295
359
|
.pill button:hover:not(:disabled) { background: var(--hover); }
|
|
296
360
|
.pill .what { font-weight: 600; }
|
|
@@ -299,30 +363,36 @@
|
|
|
299
363
|
.pill .n { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
300
364
|
.pill .again { border-left: 1px solid var(--line); padding: 5px 6px; color: var(--muted); }
|
|
301
365
|
.pill .again:hover:not(:disabled) { color: var(--text); }
|
|
302
|
-
.pill.on { border-color: var(--
|
|
303
|
-
.pill.on .again { border-left-color: var(--
|
|
366
|
+
.pill.on { border-color: var(--accent); background: var(--accent-soft); }
|
|
367
|
+
.pill.on .again { border-left-color: color-mix(in srgb, var(--accent) 40%, transparent); }
|
|
304
368
|
.mt-h, .mtable li { display: grid; grid-template-columns: 16px 20px 44px minmax(150px, 3fr) minmax(70px, 1.1fr) 66px 66px 42px 60px 50px 54px 60px minmax(166px, 1.5fr) minmax(120px, 1.3fr) 28px; align-items: center; column-gap: 6px; padding: 8px 16px; }
|
|
305
|
-
.mt-h
|
|
306
|
-
.mt-h
|
|
369
|
+
.mtable .src, .mt-h > :nth-child(13) { padding-left: 12px; } /* room after the right-aligned prices */
|
|
370
|
+
.mt-h { border-bottom: 1px solid var(--line); font-size: 12px; font-weight: 500; color: var(--faint); padding-top: 10px; padding-bottom: 10px; background: var(--panel2); }
|
|
371
|
+
.mt-h button { display: inline; border: 0; background: none !important; padding: 0; font: inherit; color: inherit; text-align: left; white-space: nowrap; transform: none !important; }
|
|
372
|
+
.mt-h button.num { text-align: right; }
|
|
307
373
|
.mt-h button:hover { color: var(--text); }
|
|
308
|
-
.mt-h button.on { color: var(--
|
|
374
|
+
.mt-h button.on { color: var(--accent); font-weight: 650; }
|
|
309
375
|
.mt-h .num, .mtable .num { text-align: right; }
|
|
310
376
|
.mtable { list-style: none; margin: 0; padding: 0; }
|
|
311
|
-
.mtable li { border-bottom: 1px solid var(--line); background: var(--panel); font-size: 13px; position: relative; }
|
|
377
|
+
.mtable li { border-bottom: 1px solid var(--line); background: var(--panel); font-size: 13px; position: relative; font-variant-numeric: tabular-nums; transition: background .12s; }
|
|
378
|
+
.mtable li.in:hover { background: var(--hover); }
|
|
312
379
|
.mtable li:last-child { border-bottom: 0; }
|
|
313
380
|
.mtable li.in { user-select: none; transition: transform .16s ease, box-shadow .16s ease; }
|
|
314
381
|
.mtable.drag li.in { cursor: grab; }
|
|
315
|
-
.mtable li.in.lifted { transition: none; z-index: 2; cursor: grabbing; box-shadow:
|
|
382
|
+
.mtable li.in.lifted { transition: none; z-index: 2; cursor: grabbing; box-shadow: var(--shadow-pop), 0 0 0 2px var(--accent); }
|
|
316
383
|
.mtable .grip { color: var(--faint); touch-action: none; }
|
|
317
384
|
.mtable:not(.drag) .grip { visibility: hidden; }
|
|
318
|
-
.mtable .n { font-weight:
|
|
385
|
+
.mtable .n { font-weight: 650; color: var(--faint); }
|
|
386
|
+
.mtable li:first-child .n { color: var(--accent); }
|
|
319
387
|
.mtable .m { font-family: var(--mono); font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
320
388
|
.mtable .p { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
321
389
|
.mtable .yes { color: var(--ok); }
|
|
322
390
|
.mtable .no { color: var(--faint); }
|
|
323
391
|
.mtable .src { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
324
392
|
.mtable .src.warn { color: var(--accent); }
|
|
325
|
-
.mtable button { padding:
|
|
393
|
+
.mtable button { padding: 2px 8px; font-size: 11.5px; border-color: transparent; background: transparent; color: var(--muted); }
|
|
394
|
+
.mtable li:hover button, .mtable li.picked button { border-color: var(--line-strong); background: var(--panel); color: var(--text); }
|
|
395
|
+
.mtable li:hover button.danger { color: var(--err); }
|
|
326
396
|
.mtable .try { font-size: 12px; color: var(--faint); display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
327
397
|
.mtable .try .msg { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
328
398
|
.mtable .try.ok { color: var(--ok); }
|
|
@@ -347,28 +417,28 @@
|
|
|
347
417
|
.opt:first-child { padding-top: 0; }
|
|
348
418
|
.opt .t { flex: 1; min-width: 0; }
|
|
349
419
|
.opt b { display: block; }
|
|
350
|
-
.opt span { color: var(--muted); font-size: 13px; }
|
|
420
|
+
.opt span { color: var(--muted); font-size: 13px; line-height: 1.45; }
|
|
351
421
|
.sw { position: relative; width: 38px; height: 22px; flex: none; margin-top: 2px; }
|
|
352
422
|
.sw input { opacity: 0; width: 0; height: 0; position: absolute; }
|
|
353
|
-
.sw i { position: absolute; inset: 0; border-radius: 11px; background: var(--line); transition: background .
|
|
354
|
-
.sw i::after { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; border-radius: 50%; background: #fff; transition: transform .
|
|
355
|
-
.sw input:checked + i { background: var(--
|
|
423
|
+
.sw i { position: absolute; inset: 0; border-radius: 11px; background: var(--line-strong); transition: background .18s; cursor: pointer; }
|
|
424
|
+
.sw i::after { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .25); transition: transform .18s cubic-bezier(.3, .7, .4, 1); }
|
|
425
|
+
.sw input:checked + i { background: var(--accent); }
|
|
356
426
|
.sw input:checked + i::after { transform: translateX(16px); }
|
|
357
427
|
.sw input:focus-visible + i { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
358
428
|
|
|
359
429
|
/* choices as cards (permissions, colors) */
|
|
360
430
|
.choices { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
|
|
361
|
-
.choice { position: relative; border: 1px solid var(--line); border-radius:
|
|
362
|
-
.choice:hover { border-color: var(--accent); }
|
|
363
|
-
.choice.on { border-color: var(--
|
|
431
|
+
.choice { position: relative; border: 1px solid var(--line-strong); border-radius: 12px; padding: 13px 15px; cursor: pointer; background: var(--panel); transition: border-color .15s, box-shadow .15s, background .15s; }
|
|
432
|
+
.choice:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--line-strong)); }
|
|
433
|
+
.choice.on { border-color: var(--accent); background: var(--accent-soft); box-shadow: var(--ring); }
|
|
364
434
|
.choice b { display: block; }
|
|
365
|
-
.choice span { color: var(--muted); font-size: 12.5px; }
|
|
435
|
+
.choice span { display: block; margin-top: 2px; color: var(--muted); font-size: 12.5px; line-height: 1.45; }
|
|
366
436
|
.choice input { position: absolute; opacity: 0; }
|
|
367
437
|
.swatch { display: flex; gap: 3px; margin-bottom: 8px; }
|
|
368
438
|
.swatch i { width: 14px; height: 14px; border-radius: 4px; border: 1px solid var(--line); }
|
|
369
439
|
|
|
370
440
|
/* a model's tooltip: shown at once on hover (or keyboard focus), next to the row */
|
|
371
|
-
#tip { position: fixed; z-index: 30; max-width: 380px; padding:
|
|
441
|
+
#tip { position: fixed; z-index: 30; max-width: 380px; padding: 13px 15px; border-radius: 12px; background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow-pop); font-size: 12.5px; pointer-events: none; opacity: 0; transition: opacity .08s; }
|
|
372
442
|
#tip.show { opacity: 1; }
|
|
373
443
|
#tip .tid { font-family: var(--mono); font-weight: 700; font-size: 13px; word-break: break-all; }
|
|
374
444
|
#tip .tname { color: var(--muted); margin-top: 1px; }
|
|
@@ -377,14 +447,15 @@
|
|
|
377
447
|
#tip dd { margin: 0; }
|
|
378
448
|
#tip .tdesc { color: var(--muted); margin-top: 10px; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
|
|
379
449
|
#tip .thint { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line); color: var(--accent); font-weight: 600; }
|
|
380
|
-
#toast { position: fixed; bottom:
|
|
381
|
-
#toast.show { opacity: 1; }
|
|
450
|
+
#toast { position: fixed; bottom: 24px; left: 50%; transform: translate(-50%, 8px); padding: 10px 18px; border-radius: 12px; background: var(--text); color: var(--bg); font-weight: 500; box-shadow: var(--shadow-pop); opacity: 0; transition: opacity .2s, transform .2s; pointer-events: none; max-width: calc(100% - 32px); z-index: 10; }
|
|
451
|
+
#toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
382
452
|
#toast.err { background: var(--err); color: #fff; }
|
|
383
453
|
/* what's running (tests, loading): a strip at the top of the panel it's about, updated in place */
|
|
384
454
|
.job-slot:empty { display: none; }
|
|
385
455
|
.job-slot.solo { margin: -6px 0 18px; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; } /* on its own, under a page's title */
|
|
386
456
|
.job-slot.solo .job:last-child { border-bottom: 0; }
|
|
387
457
|
.job { padding: 10px 16px 0; border-bottom: 1px solid var(--line); background: var(--panel2); font-size: 12.5px; }
|
|
458
|
+
.job .bar i, .job .spin { color: var(--accent); }
|
|
388
459
|
.job .top { display: flex; align-items: center; gap: 10px; min-height: 24px; }
|
|
389
460
|
.job .spin { flex: none; color: var(--brand); }
|
|
390
461
|
.job .what { font-weight: 600; white-space: nowrap; }
|
|
@@ -412,7 +483,9 @@
|
|
|
412
483
|
.brand { padding: 0 0 8px; }
|
|
413
484
|
nav { display: flex; overflow-x: auto; gap: 4px; padding-bottom: 4px; }
|
|
414
485
|
nav a { flex: none; margin: 0; }
|
|
486
|
+
nav a.on::before { display: none; } /* a strip of tabs: the highlighted one is enough */
|
|
415
487
|
nav a .count, .using { display: none; }
|
|
488
|
+
.theme-pick { margin: 6px 0 4px; max-width: 280px; }
|
|
416
489
|
main { padding: 20px 16px 60px; }
|
|
417
490
|
.steps { grid-template-columns: 1fr; }
|
|
418
491
|
.choices { grid-template-columns: 1fr; }
|
|
@@ -429,7 +502,7 @@
|
|
|
429
502
|
<body>
|
|
430
503
|
<div class="app">
|
|
431
504
|
<aside>
|
|
432
|
-
<div class="brand"><span>♥</span> HeartCode settings</div>
|
|
505
|
+
<div class="brand"><span>♥</span> HeartCode <small>settings</small></div>
|
|
433
506
|
<nav id="nav">
|
|
434
507
|
<a href="#providers" data-page="providers"><span class="ic">◉</span> Providers & models <span class="count" id="n-providers"></span></a>
|
|
435
508
|
<a href="#models" data-page="models"><span class="ic">☰</span> Your models <span class="count" id="n-mine"></span></a>
|
|
@@ -441,6 +514,9 @@
|
|
|
441
514
|
<a href="#files" data-page="files"><span class="ic">≡</span> Advanced files</a>
|
|
442
515
|
</nav>
|
|
443
516
|
<div class="using" id="using"></div>
|
|
517
|
+
<div class="seg theme-pick" id="theme-pick" role="radiogroup" aria-label="Light or dark">
|
|
518
|
+
<label><input type="radio" name="theme-pick" value="system">System</label><label><input type="radio" name="theme-pick" value="light">Light</label><label><input type="radio" name="theme-pick" value="dark">Dark</label>
|
|
519
|
+
</div>
|
|
444
520
|
</aside>
|
|
445
521
|
|
|
446
522
|
<main>
|
|
@@ -448,7 +524,7 @@
|
|
|
448
524
|
<!-- Providers & models -->
|
|
449
525
|
<section class="page" id="page-providers">
|
|
450
526
|
<div class="head">
|
|
451
|
-
<div><h1>Providers & models</h1><p class="lead">Click a model to add it to your list, or tick several
|
|
527
|
+
<div><h1>Providers & models</h1><p class="lead">Click a model to add it to your list, or tick several. HeartCode tries your list in order: #1 first, the next when one fails.</p></div>
|
|
452
528
|
<div class="head-actions"><button id="p-mine" type="button"></button><button class="primary" id="p-open">+ Add a provider</button></div>
|
|
453
529
|
</div>
|
|
454
530
|
<div class="steps" id="steps"></div>
|
|
@@ -471,7 +547,7 @@
|
|
|
471
547
|
|
|
472
548
|
<!-- Your models: all at once, sortable -->
|
|
473
549
|
<section class="page" id="page-models">
|
|
474
|
-
<div class="head"><div><h1>Your models</h1><p class="lead">Every model
|
|
550
|
+
<div class="head"><div><h1>Your models</h1><p class="lead">Every model HeartCode can use, and what each can do. Drag a row to reorder, or sort by columns (click more to break ties) and save the order you see.</p></div></div>
|
|
475
551
|
<div class="card mt-card" id="mt"></div>
|
|
476
552
|
</section>
|
|
477
553
|
|
|
@@ -553,7 +629,7 @@
|
|
|
553
629
|
</div>
|
|
554
630
|
<div class="card">
|
|
555
631
|
<h2>Voice input</h2>
|
|
556
|
-
<p class="sub"
|
|
632
|
+
<p class="sub">/voice in HeartCode. Empty: whisper-cli on this machine. Or a provider's transcription API: <span class="mono">{ "provider": "groq", "model": "whisper-large-v3" }</span>; add <span class="mono">"language": "auto"</span> for other languages.</p>
|
|
557
633
|
<textarea id="v-text" spellcheck="false" rows="3" placeholder="(whisper-cli on this machine)"></textarea>
|
|
558
634
|
<div class="actions"><button class="primary" id="v-save">Save voice</button></div>
|
|
559
635
|
</div>
|
|
@@ -642,6 +718,12 @@
|
|
|
642
718
|
<div class="actions"><button id="c-cancel">Cancel</button><button class="primary" id="c-add">Add server</button></div>
|
|
643
719
|
</dialog>
|
|
644
720
|
<dialog id="k-form" aria-labelledby="k-title"></dialog>
|
|
721
|
+
<dialog id="gone" aria-labelledby="gone-title">
|
|
722
|
+
<h2 id="gone-title">Disconnected</h2>
|
|
723
|
+
<p class="sub">HeartCode's settings page was turned off, so nothing here can be changed or saved now.</p>
|
|
724
|
+
<p class="sub">To open it again, type <b>/settings</b> in HeartCode, or run <b>heart settings</b> in a terminal: it opens a new page (this one can't reconnect, its key is gone).</p>
|
|
725
|
+
<div class="actions"><span class="spacer"></span><button id="gone-again" type="button">Try again</button></div>
|
|
726
|
+
</dialog>
|
|
645
727
|
<dialog id="hello" aria-labelledby="hello-title">
|
|
646
728
|
<h2 id="hello-title"></h2>
|
|
647
729
|
<p class="sub" id="hello-said"></p>
|
|
@@ -669,6 +751,20 @@
|
|
|
669
751
|
<div id="tip" role="tooltip"></div>
|
|
670
752
|
<script>
|
|
671
753
|
const token = new URLSearchParams(location.search).get('t')
|
|
754
|
+
// Light, dark, or as the system is set: kept in this browser (the one thing the page keeps itself)
|
|
755
|
+
{
|
|
756
|
+
const pick = document.getElementById('theme-pick')
|
|
757
|
+
const now = () => document.documentElement.dataset.theme || 'system'
|
|
758
|
+
const show = () => pick.querySelectorAll('label').forEach((l) => { const i = l.querySelector('input'); i.checked = i.value === now(); l.classList.toggle('on', i.checked) })
|
|
759
|
+
pick.addEventListener('change', (e) => {
|
|
760
|
+
const v = e.target.value
|
|
761
|
+
if (v === 'system') delete document.documentElement.dataset.theme
|
|
762
|
+
else document.documentElement.dataset.theme = v
|
|
763
|
+
try { v === 'system' ? localStorage.removeItem('heart-theme') : localStorage.setItem('heart-theme', v) } catch {}
|
|
764
|
+
show()
|
|
765
|
+
})
|
|
766
|
+
show()
|
|
767
|
+
}
|
|
672
768
|
const $ = (id) => document.getElementById(id)
|
|
673
769
|
const h = (tag, props = {}, ...kids) => {
|
|
674
770
|
const el = document.createElement(tag)
|
|
@@ -724,8 +820,22 @@ function fillSlot(slot) {
|
|
|
724
820
|
}
|
|
725
821
|
return slot
|
|
726
822
|
}
|
|
823
|
+
// The settings server turned off (or this page's key no longer works there): the page says so and
|
|
824
|
+
// is blocked, nothing can be changed. Still there (a moment's network trouble): it goes on.
|
|
825
|
+
const reachable = () => fetch(`/?t=${token}`, { cache: 'no-store' }).then((r) => r.ok, () => false)
|
|
826
|
+
async function checkGone() {
|
|
827
|
+
if (await reachable()) return $('gone').open && $('gone').close()
|
|
828
|
+
if ($('gone').open) return
|
|
829
|
+
for (const d of document.querySelectorAll('dialog[open]')) d.close()
|
|
830
|
+
$('gone').showModal()
|
|
831
|
+
}
|
|
832
|
+
$('gone').addEventListener('cancel', (e) => e.preventDefault()) // Esc doesn't close it
|
|
833
|
+
$('gone-again').onclick = checkGone
|
|
727
834
|
async function api(path, body) {
|
|
728
|
-
const res = await fetch(path, { method: body === undefined ? 'GET' : 'POST', headers: { 'x-heart-token': token, ...(body !== undefined && { 'content-type': 'application/json' }) }, body: body === undefined ? undefined : JSON.stringify(body) })
|
|
835
|
+
const res = await fetch(path, { method: body === undefined ? 'GET' : 'POST', headers: { 'x-heart-token': token, ...(body !== undefined && { 'content-type': 'application/json' }) }, body: body === undefined ? undefined : JSON.stringify(body) }).catch((e) => {
|
|
836
|
+
checkGone()
|
|
837
|
+
throw new Error("the settings page is off: HeartCode isn't listening")
|
|
838
|
+
})
|
|
729
839
|
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
730
840
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
731
841
|
return data
|
|
@@ -735,6 +845,7 @@ const DOING = {
|
|
|
735
845
|
'/api/provider': 'Checking the address and saving the provider',
|
|
736
846
|
'/api/provider/delete': 'Deleting the provider',
|
|
737
847
|
'/api/provider/list': 'Saving the model list setting',
|
|
848
|
+
'/api/provider/on': 'Switching the provider',
|
|
738
849
|
'/api/order': 'Saving your models',
|
|
739
850
|
'/api/models-dev': null, // its own strip
|
|
740
851
|
}
|
|
@@ -1228,9 +1339,10 @@ function drawProviders(s) {
|
|
|
1228
1339
|
$('steps').hidden = order.length > 0 // the first start only
|
|
1229
1340
|
$('p-work').hidden = none
|
|
1230
1341
|
// your list, at the top: how many, and the way to its page (order, tests, what to do when one fails)
|
|
1231
|
-
|
|
1232
|
-
$('p-mine').
|
|
1233
|
-
$('p-mine').
|
|
1342
|
+
const shown = shownOrder()
|
|
1343
|
+
$('p-mine').hidden = !shown.length
|
|
1344
|
+
$('p-mine').textContent = `Your models (${shown.length}) →`
|
|
1345
|
+
$('p-mine').title = shown.length ? `HeartCode uses ${shown[0].provider} · ${shown[0].model}. Open the Your models page: the order, tests, and what happens when one fails` : ''
|
|
1234
1346
|
fillHeight()
|
|
1235
1347
|
|
|
1236
1348
|
// every provider's models, A to Z; one click adds it (the first one becomes #1). Built
|
|
@@ -1360,9 +1472,22 @@ const addModel = (provider, id) => {
|
|
|
1360
1472
|
const order = S.order
|
|
1361
1473
|
return order.length ? saveOrder([...order, { provider, model: id }], `${id} added as #${order.length + 1}`) : useModel(provider, id)
|
|
1362
1474
|
}
|
|
1475
|
+
// a provider switched off: its models stay in your list, but HeartCode skips them; the one in use is
|
|
1476
|
+
// the first of a provider that's on
|
|
1477
|
+
const isOff = (name) => Boolean(S?.providers.find((p) => p.name === name)?.off)
|
|
1478
|
+
const inUse = () => S.order.find((t) => !isOff(t.provider))
|
|
1479
|
+
// Your models as shown: none of a provider that's switched off (they come back when it's on again)
|
|
1480
|
+
const shownOrder = () => S.order.filter((t) => !isOff(t.provider))
|
|
1481
|
+
// An order saved from what's shown: the models of a provider that's off keep their places in yours
|
|
1482
|
+
const withOff = (shown) => {
|
|
1483
|
+
const rest = shown[Symbol.iterator]()
|
|
1484
|
+
return [...S.order.map((t) => (isOff(t.provider) ? t : rest.next().value)).filter(Boolean), ...rest]
|
|
1485
|
+
}
|
|
1363
1486
|
function rowTail(provider, id) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1487
|
+
if (placeOf(provider, id) >= 0 && isOff(provider)) return h('span', { className: 'tag', textContent: 'in your list · off', title: `${provider} is switched off: HeartCode skips its models (and Your models doesn't show them) until it's on` })
|
|
1488
|
+
const at = shownOrder().findIndex((t) => t.provider === provider && t.model === id) // its place among the ones HeartCode uses
|
|
1489
|
+
const now = inUse()
|
|
1490
|
+
return now?.provider === provider && now.model === id ? h('span', { className: 'tag brand', textContent: `#${at + 1} in use` }) : at >= 0 ? h('span', { className: 'tag brand', textContent: `#${at + 1} fallback` }) : h('span', { className: 'add', textContent: S.order.length ? '+ Add' : 'Use' })
|
|
1366
1491
|
}
|
|
1367
1492
|
// what a listed model can do: as tested or set up, else what models.dev says (one of yours: its
|
|
1368
1493
|
// profile, also when the provider's list is off)
|
|
@@ -1545,14 +1670,18 @@ function buildCatalog(s) {
|
|
|
1545
1670
|
}
|
|
1546
1671
|
if (q && !ids.length) return null // searching: only the providers with a match
|
|
1547
1672
|
if (freeOnly && !ids.length) return null // free only: the providers with a free one
|
|
1548
|
-
return h('section', { 'data-provider': p.name },
|
|
1673
|
+
return h('section', { 'data-provider': p.name, className: p.off ? 'off' : '' },
|
|
1549
1674
|
// the header folds it (a click on it, or its arrow; not on its buttons and switch)
|
|
1550
1675
|
h('div', { className: 'prov-h', onclick: (e) => !e.target.closest('button:not(.fold), input, label, a') && fold(p.name) },
|
|
1551
1676
|
h('button', { className: 'fold', type: 'button', textContent: '▾', 'aria-label': `fold ${p.name}`, 'aria-expanded': 'true' }),
|
|
1552
1677
|
ids.length ? h('input', { type: 'checkbox', className: 'pick-all', 'data-provider': p.name, 'aria-label': `select ${p.name}'s models`, title: `select all of ${p.name}'s models shown (or none)`, onchange: (e) => (ids.forEach((id) => (e.target.checked ? pickedOf(p.name).add(id) : pickedOf(p.name).delete(id))), drawPicks()) }) : null,
|
|
1678
|
+
h('label', { className: 'sw', title: p.off ? `switched off: HeartCode skips ${p.name}'s models. Switch it on` : `switch ${p.name} off: HeartCode skips its models (nothing is deleted)` },
|
|
1679
|
+
h('input', { type: 'checkbox', checked: !p.off, 'aria-label': `${p.name} on`, onchange: (e) => act('/api/provider/on', { name: p.name, on: e.target.checked }, e.target.checked ? `${p.name} is on` : `${p.name} is off: HeartCode skips its models`) }),
|
|
1680
|
+
h('i')),
|
|
1553
1681
|
h('div', { className: 'who' },
|
|
1554
1682
|
h('span', { className: 'name', textContent: p.name }),
|
|
1555
|
-
|
|
1683
|
+
p.off ? h('span', { className: 'tag', textContent: 'off' }) : null,
|
|
1684
|
+
m.off ? h('span', { className: 'tag', textContent: 'list off', title: "its list isn't loaded: add a model by its name, in the box on the right" }) : m.loading ? h('span', { className: 'tag', textContent: 'loading…' }) : m.error ? h('span', { className: 'tag err', textContent: "can't list its models", title: m.error }) : h('span', { className: 'tag', textContent: `${m.list.length} model${m.list.length === 1 ? '' : 's'}` }),
|
|
1556
1685
|
h('span', { className: 'tag inlist' }),
|
|
1557
1686
|
h('span', { className: 'meta mono', textContent: p.baseUrl, title: `${p.baseUrl} · key ${p.key || 'none'}` })),
|
|
1558
1687
|
h('form', { className: 'by-name', onsubmit: addByName },
|
|
@@ -1594,7 +1723,7 @@ let SORT = []
|
|
|
1594
1723
|
// your ranking of the providers, best first (kept: savePref); a provider not ranked yet comes after, A to Z
|
|
1595
1724
|
let RANK = []
|
|
1596
1725
|
const ranked = () => {
|
|
1597
|
-
const names = [...new Set(
|
|
1726
|
+
const names = [...new Set(shownOrder().map((t) => t.provider))]
|
|
1598
1727
|
return [...RANK.filter((n) => names.includes(n)), ...names.filter((n) => !RANK.includes(n)).sort(byName)]
|
|
1599
1728
|
}
|
|
1600
1729
|
// the ranking dialog: drag a provider to its place, then Save
|
|
@@ -1654,7 +1783,7 @@ const dirText = (key, dir) => (key === 'provider' ? (dir === 'up' ? 'your rankin
|
|
|
1654
1783
|
const PICKED = new Set()
|
|
1655
1784
|
let lastPicked = null
|
|
1656
1785
|
function drawMine(s) {
|
|
1657
|
-
const order =
|
|
1786
|
+
const order = shownOrder() // a provider switched off: its models aren't shown (nor tested, sorted, ranked)
|
|
1658
1787
|
$('n-mine').textContent = order.length || ''
|
|
1659
1788
|
const view = sorted(order)
|
|
1660
1789
|
for (const k of PICKED) if (placeOf(...k.split(/\/(.*)/s)) === undefined) PICKED.delete(k) // gone from the list
|
|
@@ -1663,7 +1792,7 @@ function drawMine(s) {
|
|
|
1663
1792
|
const n = order.length - rest.length
|
|
1664
1793
|
if (!confirm(`Take ${n} model${n === 1 ? '' : 's'} off your list?${!rest.length ? ' None is left then: HeartCode has no model to use until you choose one.' : rest[0] !== order[0] ? ` HeartCode then uses ${rest[0].provider} · ${rest[0].model}.` : ''} (They stay under their providers.)`)) return
|
|
1665
1794
|
PICKED.clear()
|
|
1666
|
-
saveOrder(rest, `${n} removed${rest.length ? '' : ': no model chosen now'}`)
|
|
1795
|
+
saveOrder(withOff(rest), `${n} removed${rest.length ? '' : ': no model chosen now'}`)
|
|
1667
1796
|
}
|
|
1668
1797
|
const all = view.length > 0 && view.every((t) => PICKED.has(keyOf(t)))
|
|
1669
1798
|
// the tests' results, by outcome: a click selects those models (again: unselects them)
|
|
@@ -1681,7 +1810,7 @@ function drawMine(s) {
|
|
|
1681
1810
|
const keep = (list, moved) => {
|
|
1682
1811
|
SORT = []
|
|
1683
1812
|
savePref('sort', SORT)
|
|
1684
|
-
return saveOrder(list, list[0] !== order[0] ? `${moved ? '' : 'order saved: '}HeartCode now uses ${list[0].provider} · ${list[0].model}` : 'order saved')
|
|
1813
|
+
return saveOrder(withOff(list), list[0] !== order[0] ? `${moved ? '' : 'order saved: '}HeartCode now uses ${list[0].provider} · ${list[0].model}` : 'order saved')
|
|
1685
1814
|
}
|
|
1686
1815
|
// a row dragged (in the order shown, sorted or not)
|
|
1687
1816
|
const move = (i, j) => {
|
|
@@ -1706,13 +1835,13 @@ function drawMine(s) {
|
|
|
1706
1835
|
lastPicked = i
|
|
1707
1836
|
drawMine(S)
|
|
1708
1837
|
}
|
|
1709
|
-
const tableV = table && virtual(table, { count: view.length, height: TABLE_ROW, row: (i) => tableRow(view[i],
|
|
1838
|
+
const tableV = table && virtual(table, { count: view.length, height: TABLE_ROW, row: (i) => tableRow(view[i], order.indexOf(view[i]), (e) => pick(i, e)), scroller: window })
|
|
1710
1839
|
if (tableV) tableV.items = view.length
|
|
1711
1840
|
put($('mt'),
|
|
1712
1841
|
h('div', { className: 'mt-top' },
|
|
1713
1842
|
h('div', { className: 'sorts' }, SORT.length
|
|
1714
1843
|
? ['Sorted by ', SORT.map(({ key, dir }, k) => h('span', { className: 'chip' }, `${k + 1}. ${COLS[key].label}: ${dirText(key, dir)}`, h('button', { type: 'button', textContent: '✕', 'aria-label': `stop sorting by ${COLS[key].label}`, onclick: () => setSort(SORT.filter((x) => x.key !== key)) })))]
|
|
1715
|
-
: order.length > 1 ? '
|
|
1844
|
+
: order.length > 1 ? 'Your order: #1 first, the next when one fails.' : ''),
|
|
1716
1845
|
PICKED.size ? h('button', { className: 'small danger', textContent: `Remove ${PICKED.size} selected`, onclick: removePicked }) : null,
|
|
1717
1846
|
PICKED.size ? h('button', { className: 'small', textContent: `Test ${PICKED.size} selected`, disabled: testingAll, onclick: () => tryAll(order.filter((t) => PICKED.has(keyOf(t)))) }) : null,
|
|
1718
1847
|
PICKED.size ? h('button', { className: 'small', textContent: 'Clear selection', onclick: () => (PICKED.clear(), drawMine(S)) }) : null,
|
|
@@ -1721,17 +1850,17 @@ function drawMine(s) {
|
|
|
1721
1850
|
SORT.length ? h('button', { className: 'small', textContent: 'Clear sort', onclick: () => setSort([]) }) : null,
|
|
1722
1851
|
order.length > 1 ? h('button', { className: 'small primary', textContent: 'Save order', title: 'save the order shown as your model order (#1 first)', onclick: () => keep(view) }) : null,
|
|
1723
1852
|
order.length ? h('button', { className: 'small', textContent: 'Refresh from models.dev', title: 'ask models.dev again what each model can do: context, tools, thinking, images, price', onclick: () => lookUp() }) : null,
|
|
1724
|
-
order.length ? h('button', { className: 'small', textContent: 'Test all', disabled: testingAll, title: 'send each a short message to see which really answer: every provider at once, a few from each (one at a time per 100 models)', onclick: () => tryAll() }) : null),
|
|
1853
|
+
order.length ? h('button', { className: 'small', textContent: 'Test all', disabled: testingAll, title: 'send each a short message to see which really answer: every provider at once, a few from each (one at a time per 100 models)', onclick: () => tryAll(order) }) : null),
|
|
1725
1854
|
// when the one in use fails, and what a new message starts with
|
|
1726
1855
|
order.length > 1 ? h('div', { className: 'mt-rules' },
|
|
1727
1856
|
h('span', { className: 'rule' },
|
|
1728
1857
|
h('label', { for: 'r-on', textContent: 'When a model fails, go on to the next' }),
|
|
1729
|
-
h('select', { id: 'r-on', onchange: (e) => saveOrder(order, 'saved', { on: e.target.value }) },
|
|
1858
|
+
h('select', { id: 'r-on', onchange: (e) => saveOrder(S.order, 'saved', { on: e.target.value }) },
|
|
1730
1859
|
[['failure', 'at its first failure'], ['retries', 'after some tries of the same model']].map(([v, t]) => h('option', { value: v, textContent: t, selected: s.rotation.on === v }))),
|
|
1731
|
-
s.rotation.on === 'retries' ? h('input', { type: 'number', 'aria-label': 'tries', title: 'tries of the same model first (rate limits, outages)', min: 1, max: 20, value: s.rotation.retries, onchange: (e) => saveOrder(order, 'saved', { retries: Number(e.target.value) }) }) : null),
|
|
1860
|
+
s.rotation.on === 'retries' ? h('input', { type: 'number', 'aria-label': 'tries', title: 'tries of the same model first (rate limits, outages)', min: 1, max: 20, value: s.rotation.retries, onchange: (e) => saveOrder(S.order, 'saved', { retries: Number(e.target.value) }) }) : null),
|
|
1732
1861
|
h('span', { className: 'rule' },
|
|
1733
1862
|
h('label', { for: 'r-next', textContent: 'A new message starts with' }),
|
|
1734
|
-
h('select', { id: 'r-next', onchange: (e) => saveOrder(order, 'saved', { next: e.target.value }) },
|
|
1863
|
+
h('select', { id: 'r-next', onchange: (e) => saveOrder(S.order, 'saved', { next: e.target.value }) },
|
|
1735
1864
|
[['first', '#1 again'], ['stay', 'the model that answered last']].map(([v, t]) => h('option', { value: v, textContent: t, selected: s.rotation.next === v })))),
|
|
1736
1865
|
h('span', { className: 'hint', textContent: s.rotation.on === 'retries' ? 'A rate limit or outage: that model is tried again that many times first; any other error moves on at once.' : "Any error moves on at once, to another provider's model first." })) : null,
|
|
1737
1866
|
order.length
|
|
@@ -1759,12 +1888,12 @@ function tableRow(t, at, pick) {
|
|
|
1759
1888
|
const c = can(t)
|
|
1760
1889
|
const ctx = COLS.context.get(t)
|
|
1761
1890
|
const picked = PICKED.has(keyOf(t))
|
|
1762
|
-
return h('li', { className: 'in' + (picked ? ' picked' : ''), 'data-key': `${t.provider}/${t.model}` },
|
|
1891
|
+
return h('li', { className: 'in' + (picked ? ' picked' : '') + (isOff(t.provider) ? ' off' : ''), 'data-key': `${t.provider}/${t.model}`, title: isOff(t.provider) ? `${t.provider} is switched off: HeartCode skips this model until it's on` : '' },
|
|
1763
1892
|
h('span', { className: 'grip', textContent: '⋮⋮', 'aria-hidden': 'true' }),
|
|
1764
1893
|
h('input', { type: 'checkbox', checked: picked, 'aria-label': `select ${t.model}`, title: 'select (Shift: and every row up to the last one selected)', onclick: (e) => (e.preventDefault(), pick(e)) }),
|
|
1765
1894
|
h('span', { className: 'n', textContent: `#${at + 1}`, title: SORT.length ? 'its place in your order now' : '' }),
|
|
1766
1895
|
h('span', { className: 'm', textContent: t.model, title: t.model }),
|
|
1767
|
-
h('span', { className: 'p', textContent: t.provider, title: t.provider }),
|
|
1896
|
+
h('span', { className: 'p', textContent: isOff(t.provider) ? `${t.provider} · off` : t.provider, title: t.provider }),
|
|
1768
1897
|
h('span', { className: 'num', textContent: ctx ? `${t.tested?.sure === false ? '~' : ''}${kTok(ctx)}` : '–' }),
|
|
1769
1898
|
h('span', { className: 'num', textContent: `${Object.values(c).filter(Boolean).length} of 3` }),
|
|
1770
1899
|
yes(c.tools), yes(c.thinking), yes(c.images),
|
|
@@ -1786,15 +1915,15 @@ function markCatalog() {
|
|
|
1786
1915
|
const n = S.order.filter((t) => t.provider === sec.dataset.provider).length
|
|
1787
1916
|
const tag = sec.querySelector('.inlist')
|
|
1788
1917
|
tag.hidden = !n
|
|
1789
|
-
tag.textContent = `${n} in your list${
|
|
1918
|
+
tag.textContent = `${n} in your list${inUse()?.provider === sec.dataset.provider ? ' · in use' : ''}`
|
|
1790
1919
|
}
|
|
1791
1920
|
drawPicks()
|
|
1792
1921
|
}
|
|
1793
1922
|
|
|
1794
1923
|
function drawUsing(s) {
|
|
1795
|
-
const o = s.order
|
|
1924
|
+
const o = s.order.filter((t) => !isOff(t.provider))
|
|
1796
1925
|
$('using').hidden = page() === 'providers' // there it's on the page itself
|
|
1797
|
-
put($('using'), o.length ? [h('span', { className: 'k', textContent: 'HeartCode uses' }), h('b', { textContent: o[0].model }), h('span', { className: 'k', textContent: `${o[0].provider}${o.length > 1 ? ` · ${o.length - 1} fallback${o.length > 2 ? 's' : ''}` : ''}` })] : [h('span', { className: 'k', textContent: 'No model chosen yet' }), h('a', { href: '#providers', textContent: 'Choose one' })])
|
|
1926
|
+
put($('using'), o.length ? [h('span', { className: 'k', textContent: 'HeartCode uses' }), h('b', { textContent: o[0].model }), h('span', { className: 'k', textContent: `${o[0].provider}${o.length > 1 ? ` · ${o.length - 1} fallback${o.length > 2 ? 's' : ''}` : ''}` })] : s.order.length ? [h('span', { className: 'k', textContent: 'The providers of all your models are switched off' }), h('a', { href: '#providers', textContent: 'Switch one on' })] : [h('span', { className: 'k', textContent: 'No model chosen yet' }), h('a', { href: '#providers', textContent: 'Choose one' })])
|
|
1798
1927
|
}
|
|
1799
1928
|
|
|
1800
1929
|
// --- General ---
|
|
@@ -2128,6 +2257,8 @@ else {
|
|
|
2128
2257
|
// A model's test, as it goes: the server streams each change; only its line in your list changes.
|
|
2129
2258
|
if (token) {
|
|
2130
2259
|
const events = new EventSource(`/api/events?t=${token}`)
|
|
2260
|
+
events.addEventListener('error', checkGone) // the server went away (turned off), or the connection did for a moment
|
|
2261
|
+
events.addEventListener('open', () => $('gone').open && checkGone())
|
|
2131
2262
|
// a request logged: at the top of the list, nothing else redrawn
|
|
2132
2263
|
// the settings changed (another HeartCode or page, a hand): shown at once; while a dialog is open,
|
|
2133
2264
|
// when it closes (what's typed in it stays)
|