slotlock 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/agent-server.d.ts +25 -5
  3. package/agent-server.js +152 -7
  4. package/agent-store-backend.d.ts +6 -1
  5. package/agent-store-backend.js +861 -55
  6. package/application-api.d.ts +19 -1
  7. package/application-api.js +578 -21
  8. package/approvals.d.ts +2 -0
  9. package/approvals.js +12 -5
  10. package/assistant.d.ts +64 -9
  11. package/assistant.js +450 -82
  12. package/availability-store.d.ts +193 -0
  13. package/availability-store.js +558 -0
  14. package/booking-rules.d.ts +92 -5
  15. package/booking-rules.js +243 -15
  16. package/dashboard-assistant-ui.d.ts +21 -0
  17. package/dashboard-assistant-ui.js +539 -0
  18. package/dashboard-markdown.d.ts +2 -0
  19. package/dashboard-markdown.js +157 -0
  20. package/dashboard-settings-ui.d.ts +1 -0
  21. package/dashboard-settings-ui.js +78 -0
  22. package/dashboard.d.ts +84 -11
  23. package/dashboard.js +1654 -384
  24. package/ddl.d.ts +5 -1
  25. package/ddl.js +760 -9
  26. package/event-feed.d.ts +1 -1
  27. package/event-feed.js +18 -0
  28. package/holiday-import.d.ts +38 -0
  29. package/holiday-import.js +57 -0
  30. package/holidays.d.ts +38 -0
  31. package/holidays.js +150 -0
  32. package/hours.d.ts +125 -0
  33. package/hours.js +224 -0
  34. package/index.d.ts +14 -3
  35. package/index.js +10 -2
  36. package/interop.d.ts +25 -7
  37. package/interop.js +145 -17
  38. package/lifecycle.d.ts +31 -0
  39. package/lifecycle.js +115 -0
  40. package/maintenance.d.ts +19 -0
  41. package/maintenance.js +55 -1
  42. package/metrics.js +3 -0
  43. package/openapi.json +16237 -7795
  44. package/package.json +1 -1
  45. package/ranges.d.ts +28 -0
  46. package/ranges.js +135 -0
  47. package/resource-search.d.ts +19 -0
  48. package/resource-search.js +153 -0
  49. package/rest-api.js +7 -0
  50. package/rules.d.ts +2 -0
  51. package/rules.js +2 -0
  52. package/self-host.d.ts +1 -1
  53. package/self-host.js +221 -21
  54. package/slotlock.manifest.json +1 -1
  55. package/store.d.ts +367 -24
  56. package/store.js +1567 -97
  57. package/types.d.ts +198 -0
@@ -0,0 +1,539 @@
1
+ // The dashboard assistant's words, styles and browser script: how a tool step and a proposed change
2
+ // are named, the chat's CSS, and the script that streams a reply into the page. dashboard.ts renders
3
+ // the page; this keeps the chat's presentation in one place.
4
+ const STEP_WORDS = Object.freeze({
5
+ slotlock_list_resources: ['Reading resources', 'Read resources', 'read resources'],
6
+ slotlock_get_free_busy: ['Checking free and busy time', 'Checked free and busy time', 'check free and busy time'],
7
+ slotlock_find_next_available: ['Finding the next free time', 'Found the next free time', 'find the next free time'],
8
+ slotlock_find_available_slots: ['Checking availability', 'Checked availability', 'check availability'],
9
+ slotlock_find_available_ranges: ['Finding free ranges', 'Found free ranges', 'find free ranges'],
10
+ slotlock_get_pool_availability: ['Checking the pool', 'Checked the pool', 'check the pool'],
11
+ slotlock_get_event: ['Reading a booking', 'Read a booking', 'read the booking'],
12
+ slotlock_list_events: ['Reading bookings', 'Read bookings', 'read bookings'],
13
+ });
14
+ /** A tool step in plain words: "Checking availability…", "Checked availability", "Couldn't check availability". */
15
+ export function slotlockAssistantStepLabel(name, title, state, write = false) {
16
+ const words = STEP_WORDS[name];
17
+ if (write) {
18
+ if (state === 'run')
19
+ return 'Preparing a change…';
20
+ if (state === 'ok')
21
+ return 'Prepared a change for you to approve';
22
+ return "Couldn't prepare the change";
23
+ }
24
+ if (words)
25
+ return state === 'run' ? `${words[0]}…` : state === 'ok' ? words[1] : `Couldn't ${words[2]}`;
26
+ const lower = title.charAt(0).toLowerCase() + title.slice(1);
27
+ return state === 'run' ? `${title}…` : state === 'ok' ? title : `Couldn't ${lower}`;
28
+ }
29
+ /** The verb a proposed change's card is approved with, and whether it removes something. */
30
+ export function slotlockAssistantActionVerb(operation) {
31
+ switch (operation) {
32
+ case 'slotlock_create_event':
33
+ case 'slotlock_book_pool':
34
+ case 'slotlock_book_together':
35
+ return { verb: 'Book', busy: 'Booking…', done: 'Booked', danger: false };
36
+ case 'slotlock_confirm_slot':
37
+ return { verb: 'Book', busy: 'Booking…', done: 'Booked', danger: false };
38
+ case 'slotlock_reschedule':
39
+ return { verb: 'Move', busy: 'Moving…', done: 'Moved', danger: false };
40
+ case 'slotlock_update_event':
41
+ return { verb: 'Change', busy: 'Changing…', done: 'Changed', danger: false };
42
+ case 'slotlock_cancel':
43
+ return { verb: 'Cancel booking', busy: 'Cancelling…', done: 'Cancelled', danger: true };
44
+ case 'slotlock_delete_event':
45
+ return { verb: 'Delete booking', busy: 'Deleting…', done: 'Deleted', danger: true };
46
+ case 'slotlock_hold_slots':
47
+ case 'slotlock_hold_together':
48
+ return { verb: 'Hold', busy: 'Holding…', done: 'Held', danger: false };
49
+ case 'slotlock_release_slots':
50
+ return { verb: 'Release', busy: 'Releasing…', done: 'Released', danger: true };
51
+ default:
52
+ return { verb: 'Approve', busy: 'Running…', done: 'Done', danger: false };
53
+ }
54
+ }
55
+ /** The chat's styles: one centred column, the composer docked under it, quiet steps and cards. */
56
+ export const SLOTLOCK_ASSISTANT_CSS = `
57
+ .assistant-page { display: grid; }
58
+ .assistant { position: relative; display: flex; flex-direction: column; width: min(100%, 720px); margin: 0 auto; min-height: calc(100dvh - 180px); }
59
+ .assistant .thread { flex: 1; }
60
+ .assistant[data-empty] { justify-content: center; padding-bottom: 12vh; }
61
+ .assistant[data-empty] .thread { display: none; }
62
+ .assistant[data-empty] .dock { order: 1; position: static; padding: 0; background: none; }
63
+ .assistant[data-empty] .assistant-start { order: 2; padding-top: var(--space-3); }
64
+ .assistant:not([data-empty]) .assistant-start { display: none; }
65
+ .chips { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); }
66
+ .chips form { margin: 0; }
67
+ .chip { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-full); border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); font: inherit; font-size: var(--text-md); cursor: pointer; transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease); }
68
+ .chip:hover { background: var(--hover); border-color: var(--line-strong); color: var(--ink); }
69
+ .thread { list-style: none; margin: 0; padding: var(--space-2) 0 var(--space-6); display: grid; gap: var(--space-6); align-content: start; }
70
+ .turn { margin: 0; min-width: 0; }
71
+ .turn.mine { justify-self: end; max-width: 80%; }
72
+ .bubble { padding: 10px 14px; border-radius: 18px; background: var(--sunken); color: var(--ink); font-size: var(--text-lg); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
73
+ .turn.bot { justify-self: stretch; }
74
+ .prose { max-width: 68ch; color: var(--ink); font-size: var(--text-lg); line-height: 1.6; overflow-wrap: anywhere; }
75
+ .prose > :first-child { margin-top: 0; }
76
+ .prose > :last-child { margin-bottom: 0; }
77
+ .prose p { margin: 0 0 var(--space-3); color: inherit; }
78
+ .prose strong { font-weight: 600; }
79
+ .prose h4 { margin: var(--space-4) 0 var(--space-2); font-size: var(--text-lg); font-weight: 600; }
80
+ .prose ul, .prose ol { margin: 0 0 var(--space-3); padding-left: 20px; }
81
+ .prose li { margin: 4px 0; }
82
+ .prose li > ul, .prose li > ol { margin: 4px 0 0; }
83
+ .prose a { color: var(--accent); text-underline-offset: 2px; }
84
+ .prose code { font: 400 12.5px/1.4 var(--mono); background: var(--sunken); border-radius: 4px; padding: 1px 5px; }
85
+ .prose pre { margin: 0 0 var(--space-3); padding: var(--space-3); background: var(--sunken); border: 1px solid var(--line); border-radius: var(--radius); overflow-x: auto; }
86
+ .prose pre code { background: none; padding: 0; font-size: 12.5px; }
87
+ .prose blockquote { margin: 0 0 var(--space-3); padding-left: var(--space-3); border-left: 2px solid var(--line-strong); color: var(--ink-2); }
88
+ .prose hr { border: 0; border-top: 1px solid var(--line); margin: var(--space-4) 0; }
89
+ .md-table { overflow-x: auto; margin: 0 0 var(--space-3); }
90
+ .md-table table { border-collapse: collapse; font-size: var(--text-md); min-width: 50%; }
91
+ .md-table table { width: auto; }
92
+ .md-table th { font: 600 var(--text-md)/1.4 var(--font); text-transform: none; letter-spacing: 0; background: none; text-align: left; color: var(--ink-2); padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line); white-space: nowrap; }
93
+ .md-table td { padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
94
+ .md-table tr:last-child td { border-bottom: 0; }
95
+ .change details.more { margin: 0; }
96
+ .change details.more > summary { cursor: pointer; color: var(--ink-3); font-size: var(--text-sm); list-style: none; }
97
+ .change details.more > summary::-webkit-details-marker { display: none; }
98
+ .change details.more > summary:hover { color: var(--ink-2); }
99
+ .change details.more[open] > summary { margin-bottom: var(--space-2); }
100
+ .turn.steps { justify-self: start; }
101
+ details.steps > summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: var(--ink-3); font-size: var(--text-md); padding: 2px 0; }
102
+ details.steps > summary::-webkit-details-marker { display: none; }
103
+ details.steps > summary::after { content: ""; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); transition: transform var(--fast) var(--ease); margin-left: 2px; }
104
+ details.steps[open] > summary::after { transform: rotate(45deg); }
105
+ details.steps > summary:hover { color: var(--ink-2); }
106
+ details.steps.has-bad > summary::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--bad); }
107
+ .step-list { list-style: none; margin: var(--space-2) 0 0; padding: 0 0 0 var(--space-3); border-left: 1px solid var(--line); display: grid; gap: 6px; }
108
+ .step { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }
109
+ .step::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--good); transform: translateY(-1px); }
110
+ .step.bad::before { background: var(--bad); }
111
+ .step.run::before { background: var(--ink-3); }
112
+ .step .why { color: var(--ink-3); font-size: var(--text-sm); }
113
+ .live-steps { display: grid; gap: 6px; }
114
+ .live-steps .step.run, .thinking { background: linear-gradient(90deg, var(--ink-3) 0%, var(--ink) 50%, var(--ink-3) 100%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: assistant-shimmer 1.6s linear infinite; }
115
+ .thinking { font-size: var(--text-md); justify-self: start; }
116
+ @keyframes assistant-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
117
+ .prose.streaming > :last-child::after { content: ""; display: inline-block; width: 8px; height: 8px; margin-left: 4px; border-radius: 50%; background: var(--ink-3); vertical-align: baseline; animation: assistant-pulse 1s ease-in-out infinite; }
118
+ @keyframes assistant-pulse { 50% { opacity: 0.25; } }
119
+ .turn.note { color: var(--ink-3); font-size: var(--text-md); }
120
+ .turn.error { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: 10px 12px; border: 1px solid var(--bad-line); background: var(--bad-wash); border-radius: var(--radius); font-size: var(--text-md); color: var(--ink); }
121
+ .turn.error::before { content: ""; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--bad); }
122
+ .turn.error > span { flex: 1; }
123
+ .turn.error form, .turn.note form { margin: 0; }
124
+ .text-button { appearance: none; border: 0; background: none; padding: 0; font: inherit; font-size: var(--text-md); color: var(--accent); cursor: pointer; }
125
+ .text-button:hover { text-decoration: underline; text-underline-offset: 2px; }
126
+ .turn.action { justify-self: start; width: min(100%, 560px); }
127
+ .change { padding: var(--space-4) var(--space-5); background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-lg); display: grid; gap: var(--space-3); }
128
+ .change-title { margin: 0; font-size: var(--text-lg); font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
129
+ .change .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px var(--space-4); margin: 0; font-size: var(--text-md); }
130
+ .change .facts dt { color: var(--ink-3); font-size: var(--text-sm); padding-top: 1px; }
131
+ .change .facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
132
+ .change .facts dd pre { margin: 0; white-space: pre-wrap; font: 400 12px/1.5 var(--mono); }
133
+ .change .facts .was { color: var(--ink-3); text-decoration: line-through; }
134
+ .change-buttons { display: flex; gap: var(--space-2); flex-wrap: wrap; }
135
+ .change-buttons form { margin: 0; }
136
+ .change-buttons button { height: 32px; }
137
+ button.danger { background: var(--bad); color: var(--bad-ink); border-color: var(--bad); }
138
+ button.danger:hover { filter: brightness(1.08); }
139
+ button.ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
140
+ button.ghost:hover { background: var(--hover); color: var(--ink); }
141
+ .receipt { margin: 0; display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }
142
+ .receipt::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); transform: translateY(-1px); }
143
+ .receipt.ok { color: var(--ink); }
144
+ .receipt.ok::before { background: var(--good); }
145
+ .receipt.bad::before { background: var(--bad); }
146
+ .receipt .why { color: var(--ink-3); }
147
+ .dock { position: sticky; bottom: 0; padding: var(--space-6) 0 var(--space-6); background: linear-gradient(to bottom, transparent, var(--canvas) 24px); z-index: 2; }
148
+ .composer { display: grid; margin: 0; background: var(--surface); border: 1px solid var(--line); border-radius: 16px; transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease); }
149
+ .composer:focus-within { border-color: var(--line-strong); box-shadow: 0 0 0 3px var(--ring); }
150
+ .composer textarea { display: block; width: 100%; min-height: 26px; max-height: 200px; resize: none; border: 0; outline: 0; background: transparent; padding: 14px 16px 0; color: var(--ink); font: inherit; font-size: var(--text-lg); line-height: 1.5; box-shadow: none; }
151
+ .composer textarea:focus { box-shadow: none; border: 0; }
152
+ .composer-bar { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); padding: var(--space-2); }
153
+ .composer-bar .count { margin-right: auto; padding-left: var(--space-2); font-size: var(--text-sm); color: var(--ink-3); }
154
+ .composer-bar .count.over { color: var(--warn); }
155
+ .send { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 0; background: var(--button); color: var(--button-ink); cursor: pointer; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }
156
+ .send:hover { background: var(--button-hover); }
157
+ .send:disabled { opacity: 0.4; cursor: default; }
158
+ .send svg { width: 16px; height: 16px; }
159
+ .send .stop { display: none; }
160
+ .assistant.streaming .send .arrow { display: none; }
161
+ .assistant.streaming .send .stop { display: block; }
162
+ .jump { position: absolute; left: 50%; transform: translate(-50%, 0); top: -8px; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); display: grid; place-items: center; cursor: pointer; box-shadow: var(--pop-shadow); }
163
+ .jump[hidden] { display: none; }
164
+ .new-chat svg, .chats-link svg { width: 14px; height: 14px; }
165
+ .assistant-page[data-empty] .new-chat { display: none; }
166
+ .assistant-actions { display: flex; gap: var(--space-2); }
167
+ .chats-link { display: none; }
168
+ /* Chats: the member's conversations beside the one shown, most recent first, grouped by day. */
169
+ .assistant-shell { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: var(--space-6); align-items: start; }
170
+ /* It scrolls with the page: a column that scrolled on its own would clip a row's menu. */
171
+ .chats { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
172
+ .chats-head { display: flex; align-items: center; justify-content: space-between; min-height: 36px; }
173
+ .chats-head h2 { margin: 0; font-size: var(--text-md); font-weight: 600; color: var(--ink-2); }
174
+ .icon-button { display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-sm); color: var(--ink-2); transition: background var(--fast) var(--ease), color var(--fast) var(--ease); }
175
+ .icon-button:hover { background: var(--hover); color: var(--ink); text-decoration: none; }
176
+ .icon-button svg { width: 15px; height: 15px; }
177
+ .chats-filter { height: 32px; font-size: var(--text-md); }
178
+ .chats-filter[hidden] { display: none; }
179
+ /* Sentence case, not the dashboard's uppercase h3. */
180
+ .chat-group { margin: var(--space-3) 0 2px; padding: 0 var(--space-2); font-family: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1.5; text-transform: none; letter-spacing: 0; color: var(--ink-3); }
181
+ .chat-group[hidden] { display: none; }
182
+ .chat-list { min-width: 0; }
183
+ /* minmax(0, 1fr): a long title is cut with an ellipsis, never widening the column. */
184
+ .chat-rows { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; }
185
+ .chat-row { position: relative; display: flex; align-items: center; border-radius: var(--radius); transition: background var(--fast) var(--ease); }
186
+ .chat-row[hidden] { display: none; }
187
+ .chat-row:hover, .chat-row:focus-within, .chat-row.current { background: var(--hover); }
188
+ .chat-link { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--space-2); padding: 7px 4px 7px var(--space-2); color: var(--ink-2); font-size: var(--text-md); }
189
+ .chat-link:hover { color: var(--ink); text-decoration: none; }
190
+ .chat-row.current .chat-link { color: var(--ink); font-weight: 500; }
191
+ .chat-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
192
+ .chat-when { flex: none; font-size: var(--text-xs); color: var(--ink-3); font-variant-numeric: tabular-nums; }
193
+ .chat-menu { position: relative; flex: none; }
194
+ .chat-menu summary { list-style: none; display: grid; place-items: center; width: 28px; height: 28px; margin-right: 2px; border-radius: var(--radius-sm); color: var(--ink-3); cursor: pointer; opacity: 0; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }
195
+ .chat-menu summary::-webkit-details-marker { display: none; }
196
+ .chat-row:hover .chat-menu summary, .chat-row:focus-within .chat-menu summary, .chat-row.current .chat-menu summary, .chat-menu[open] summary { opacity: 1; }
197
+ .chat-menu summary:hover, .chat-menu[open] summary { background: var(--line-soft); color: var(--ink); }
198
+ .chat-menu summary svg { width: 14px; height: 14px; }
199
+ .chat-menu .pop { z-index: 20; width: 260px; display: grid; gap: var(--space-2); }
200
+ .chat-menu .pop form { display: grid; gap: var(--space-2); margin: 0; }
201
+ .chat-menu .pop form + form { padding-top: var(--space-2); border-top: 1px solid var(--line-soft); }
202
+ .chat-menu .pop button.danger { background: none; border-color: transparent; box-shadow: none; color: var(--bad); }
203
+ .chat-menu .pop button.danger:hover { background: var(--bad-wash); }
204
+ .chat-rename input { width: 100%; min-width: 0; height: 34px; }
205
+ .chats-empty { padding: var(--space-2); font-size: var(--text-md); }
206
+ .chats-page .chats { max-width: 560px; }
207
+ .chats-page .chats-head { display: none; }
208
+ .chats-page .chat-menu summary { opacity: 1; }
209
+ @media (max-width: 1100px) {
210
+ .assistant-shell { grid-template-columns: minmax(0, 1fr); }
211
+ .assistant-shell > .chats { display: none; }
212
+ .chats-link { display: inline-flex; }
213
+ }
214
+ @media (hover: none) {
215
+ .chat-menu summary { opacity: 1; }
216
+ }
217
+ @media (prefers-reduced-motion: reduce) {
218
+ .live-steps .step.run, .thinking { animation: none; color: var(--ink-3); background: none; }
219
+ .prose.streaming > :last-child::after { animation: none; }
220
+ }
221
+ @media (max-width: 760px) {
222
+ .assistant { min-height: calc(100dvh - 140px); }
223
+ .assistant[data-empty] { justify-content: flex-end; padding-bottom: 0; }
224
+ .assistant[data-empty] .assistant-start { order: 1; padding: 0 0 var(--space-3); }
225
+ .assistant[data-empty] .dock { order: 2; position: sticky; padding-bottom: max(12px, env(safe-area-inset-bottom)); }
226
+ .chips { flex-wrap: nowrap; justify-content: flex-start; overflow-x: auto; max-width: 100%; padding-bottom: 2px; scrollbar-width: none; }
227
+ .chips::-webkit-scrollbar { display: none; }
228
+ .chip { flex: none; }
229
+ .turn.mine { max-width: 88%; }
230
+ .md-table table { display: table; }
231
+ .md-table thead { display: table-header-group; }
232
+ .md-table tbody { display: table-row-group; }
233
+ .md-table tr { display: table-row; padding: 0; border: 0; }
234
+ .md-table th, .md-table td { display: table-cell; padding: 6px 12px 6px 0; }
235
+ .md-table td { border-bottom: 1px solid var(--line-soft); }
236
+ .composer textarea { font-size: 16px; }
237
+ .dock { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
238
+ .change { padding: var(--space-4); }
239
+ .change .facts { grid-template-columns: minmax(0, 1fr); gap: 2px; }
240
+ .change .facts dd { margin-bottom: var(--space-2); }
241
+ .change-buttons form { flex: 1; }
242
+ .change-buttons button { width: 100%; height: 40px; }
243
+ }
244
+ `;
245
+ /**
246
+ * The browser side of the chat, appended to app.js: Enter sends, the box grows, a write chip fills
247
+ * the box instead of sending, and a reply streams in from `<root>/assistant/stream` (newline-
248
+ * delimited JSON: text deltas, steps, then the turn as the server renders it). Without it the
249
+ * page still works: every form posts and the page reloads at the latest message.
250
+ * `markdownSource` is renderSlotlockMarkdown's source, so a reply renders as it will once stored.
251
+ */
252
+ export function slotlockAssistantScript(markdownSource) {
253
+ return `
254
+ // Search chats: rows whose title holds what is typed (ignoring case and accents) stay shown.
255
+ function filterChats() {
256
+ var filter = document.getElementById('chats-filter');
257
+ if (!filter) return;
258
+ filter.hidden = false;
259
+ var fold = function (text) { return text.normalize('NFKD').replace(/\\p{M}/gu, '').toLowerCase(); };
260
+ var apply = function () {
261
+ var wanted = fold(filter.value.trim());
262
+ for (var group of document.querySelectorAll('.chat-rows')) {
263
+ var shown = 0;
264
+ for (var row of group.children) {
265
+ var title = row.querySelector('.chat-title');
266
+ row.hidden = wanted !== '' && fold(title ? title.textContent : '').indexOf(wanted) === -1;
267
+ if (!row.hidden) shown += 1;
268
+ }
269
+ var label = group.previousElementSibling;
270
+ if (label && label.classList.contains('chat-group')) label.hidden = shown === 0;
271
+ }
272
+ };
273
+ filter.addEventListener('input', apply);
274
+ apply();
275
+ }
276
+ filterChats();
277
+ (function () {
278
+ var page = document.querySelector('[data-assistant]');
279
+ if (!page || typeof fetch !== 'function' || typeof TextDecoder !== 'function') return;
280
+ var __name = function (fn) { return fn; };
281
+ var renderMarkdown = (${markdownSource});
282
+ var form = page.querySelector('form.composer');
283
+ var box = form ? form.querySelector('textarea') : null;
284
+ var send = form ? form.querySelector('button.send') : null;
285
+ var thread = page.querySelector('ol.thread');
286
+ var jump = page.querySelector('button.jump');
287
+ var count = form ? form.querySelector('.count') : null;
288
+ var shell = document.querySelector('.assistant-page');
289
+ if (!form || !box || !send || !thread) return;
290
+ var max = Number(box.getAttribute('maxlength')) || 2000;
291
+ // Two rows without script; one that grows with it.
292
+ box.setAttribute('rows', '1');
293
+ var busy = null;
294
+
295
+ function grow() {
296
+ box.style.height = 'auto';
297
+ box.style.height = Math.min(box.scrollHeight, 200) + 'px';
298
+ }
299
+ function update() {
300
+ if (!busy) send.disabled = box.value.trim() === '';
301
+ if (count) {
302
+ var left = box.value.length;
303
+ count.hidden = left < max - 200;
304
+ count.textContent = left + ' / ' + max;
305
+ count.classList.toggle('over', left >= max);
306
+ }
307
+ }
308
+ function nearBottom() {
309
+ return window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 80;
310
+ }
311
+ function follow(was) {
312
+ if (was) window.scrollTo(0, document.documentElement.scrollHeight);
313
+ else if (jump) jump.hidden = false;
314
+ }
315
+ if (jump) {
316
+ jump.addEventListener('click', function () {
317
+ window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' });
318
+ jump.hidden = true;
319
+ });
320
+ window.addEventListener('scroll', function () { if (nearBottom()) jump.hidden = true; }, { passive: true });
321
+ }
322
+ function item(className) {
323
+ var li = document.createElement('li');
324
+ li.className = 'turn ' + className;
325
+ thread.appendChild(li);
326
+ return li;
327
+ }
328
+ function setOnce(value) {
329
+ if (!value) return;
330
+ var field = form.querySelector('input[name="once"]');
331
+ if (field) field.value = value;
332
+ }
333
+ function stopped(reason) {
334
+ page.classList.remove('streaming');
335
+ send.setAttribute('aria-label', 'Send');
336
+ busy = null;
337
+ update();
338
+ }
339
+
340
+ box.addEventListener('input', function () { grow(); update(); });
341
+ box.addEventListener('keydown', function (event) {
342
+ if (event.key !== 'Enter' || event.shiftKey || event.isComposing || event.keyCode === 229) return;
343
+ event.preventDefault();
344
+ if (!busy && box.value.trim() !== '') form.requestSubmit();
345
+ });
346
+ // A chip that asks sends through the box, so its reply streams; one that writes only fills it.
347
+ for (var chip of page.querySelectorAll('.chips button.chip')) {
348
+ chip.addEventListener('click', function (event) {
349
+ event.preventDefault();
350
+ var fill = this.getAttribute('data-fill');
351
+ box.value = fill !== null ? fill : (this.value || '');
352
+ grow();
353
+ update();
354
+ if (fill !== null) {
355
+ box.focus();
356
+ box.setSelectionRange(box.value.length, box.value.length);
357
+ } else if (!busy && box.value.trim() !== '') {
358
+ form.requestSubmit();
359
+ }
360
+ });
361
+ }
362
+ for (var decide of document.querySelectorAll('.change-buttons form')) {
363
+ decide.addEventListener('submit', function () {
364
+ var buttons = this.closest('.change-buttons').querySelectorAll('button');
365
+ for (var b of buttons) b.disabled = true;
366
+ var own = this.querySelector('button');
367
+ if (own && own.getAttribute('data-busy')) own.textContent = own.getAttribute('data-busy');
368
+ });
369
+ }
370
+
371
+ send.addEventListener('click', function (event) {
372
+ if (!busy) return;
373
+ event.preventDefault();
374
+ busy.abort();
375
+ });
376
+
377
+ form.addEventListener('submit', function (event) {
378
+ var text = box.value.trim();
379
+ if (busy || text === '') { event.preventDefault(); return; }
380
+ event.preventDefault();
381
+ var controller = new AbortController();
382
+ busy = controller;
383
+ var body = new URLSearchParams(new FormData(form));
384
+ body.set('message', text);
385
+ if (page.hasAttribute('data-empty')) {
386
+ page.removeAttribute('data-empty');
387
+ if (shell) shell.removeAttribute('data-empty');
388
+ }
389
+ page.classList.add('streaming');
390
+ send.disabled = false;
391
+ send.setAttribute('aria-label', 'Stop');
392
+ var was = true;
393
+ var mine = item('mine');
394
+ var bubble = document.createElement('div');
395
+ bubble.className = 'bubble';
396
+ bubble.textContent = text;
397
+ mine.appendChild(bubble);
398
+ box.value = '';
399
+ box.setAttribute('placeholder', 'Reply…');
400
+ grow();
401
+ var added = [mine];
402
+ var thinking = item('note thinking');
403
+ thinking.textContent = 'Thinking';
404
+ added.push(thinking);
405
+ var prose = null;
406
+ var proseText = '';
407
+ var steps = null;
408
+ // Keyed by the provider's call id: no id, __proto__ included, may name an inherited property.
409
+ var stepNodes = Object.create(null);
410
+ var frame = 0;
411
+ follow(was);
412
+ function draw() {
413
+ frame = 0;
414
+ if (prose) prose.innerHTML = renderMarkdown(proseText);
415
+ }
416
+ function clearThinking() {
417
+ if (thinking && thinking.parentNode) thinking.parentNode.removeChild(thinking);
418
+ thinking = null;
419
+ }
420
+ function onEvent(e) {
421
+ was = nearBottom();
422
+ if (e.t === 'text') {
423
+ clearThinking();
424
+ if (!prose) {
425
+ var li = item('bot');
426
+ added.push(li);
427
+ prose = document.createElement('div');
428
+ prose.className = 'prose streaming';
429
+ li.appendChild(prose);
430
+ proseText = '';
431
+ steps = null;
432
+ }
433
+ proseText += e.d;
434
+ if (!frame) frame = requestAnimationFrame(draw);
435
+ } else if (e.t === 'step') {
436
+ clearThinking();
437
+ if (prose) { if (frame) { cancelAnimationFrame(frame); draw(); } prose.classList.remove('streaming'); prose = null; }
438
+ if (!steps) {
439
+ var holder = item('steps');
440
+ added.push(holder);
441
+ steps = document.createElement('div');
442
+ steps.className = 'live-steps';
443
+ holder.appendChild(steps);
444
+ }
445
+ var node = stepNodes[e.id];
446
+ if (!node) {
447
+ node = document.createElement('div');
448
+ stepNodes[e.id] = node;
449
+ steps.appendChild(node);
450
+ }
451
+ node.className = 'step ' + e.s;
452
+ node.textContent = e.label;
453
+ } else if (e.t === 'done' || e.t === 'error') {
454
+ clearThinking();
455
+ if (frame) cancelAnimationFrame(frame);
456
+ for (var n of added) if (n.parentNode) n.parentNode.removeChild(n);
457
+ added = [];
458
+ thread.insertAdjacentHTML('beforeend', e.html || '');
459
+ setOnce(e.once);
460
+ // The conversation it went to: a new one has its id now, its own address, and a place in Chats.
461
+ if (e.chat) {
462
+ for (var field of page.querySelectorAll('input[name="chat"]')) field.value = e.chat;
463
+ if (e.url && window.history && history.replaceState) history.replaceState(null, '', e.url);
464
+ var heading = document.querySelector('.assistant-page .page-head h1');
465
+ if (heading && e.title) heading.textContent = e.title;
466
+ var panel = document.querySelector('aside.chats');
467
+ if (panel && e.chats) {
468
+ panel.outerHTML = e.chats;
469
+ filterChats();
470
+ }
471
+ }
472
+ }
473
+ follow(was);
474
+ }
475
+ fetch(page.getAttribute('data-stream'), {
476
+ method: 'POST',
477
+ body: body,
478
+ headers: { accept: 'application/x-ndjson' },
479
+ credentials: 'same-origin',
480
+ signal: controller.signal,
481
+ }).then(function (response) {
482
+ var type = response.headers.get('content-type') || '';
483
+ if (!response.body || type.indexOf('application/x-ndjson') !== 0) {
484
+ var current = form.querySelector('input[name="chat"]');
485
+ location.assign(page.getAttribute('data-page') + (current && current.value ? '?c=' + current.value : '') + '#latest');
486
+ location.reload();
487
+ return;
488
+ }
489
+ var reader = response.body.getReader();
490
+ var decoder = new TextDecoder();
491
+ var buffer = '';
492
+ function pump() {
493
+ return reader.read().then(function (chunk) {
494
+ if (chunk.done) {
495
+ if (buffer.trim()) onEvent(JSON.parse(buffer));
496
+ stopped();
497
+ return;
498
+ }
499
+ buffer += decoder.decode(chunk.value, { stream: true });
500
+ var lines = buffer.split('\\n');
501
+ buffer = lines.pop() || '';
502
+ for (var line of lines) if (line.trim()) onEvent(JSON.parse(line));
503
+ return pump();
504
+ });
505
+ }
506
+ return pump();
507
+ }).catch(function () {
508
+ clearThinking();
509
+ if (controller.signal.aborted) {
510
+ if (prose) prose.classList.remove('streaming');
511
+ var note = item('note');
512
+ note.textContent = 'Stopped';
513
+ stopped();
514
+ // The server keeps what it had; the page it renders next has a fresh form.
515
+ setTimeout(function () { location.assign(page.getAttribute('data-page') + '#latest'); location.reload(); }, 900);
516
+ return;
517
+ }
518
+ for (var n of added) if (n.parentNode && n !== mine) n.parentNode.removeChild(n);
519
+ if (mine.parentNode) mine.parentNode.removeChild(mine);
520
+ box.value = text;
521
+ grow();
522
+ var fail = item('error');
523
+ var why = document.createElement('span');
524
+ why.textContent = "Couldn't send. Check the connection and try again.";
525
+ fail.appendChild(why);
526
+ stopped();
527
+ });
528
+ });
529
+ grow();
530
+ update();
531
+ // To the latest message only when it would be behind the box: a short conversation keeps its
532
+ // heading (and Chats) in view.
533
+ var dockAtLoad = page.querySelector('.dock');
534
+ var lastAtLoad = thread.lastElementChild;
535
+ var behind = !!lastAtLoad && lastAtLoad.getBoundingClientRect().bottom > window.innerHeight - (dockAtLoad ? dockAtLoad.offsetHeight : 0);
536
+ if (!page.hasAttribute('data-empty') && behind) window.scrollTo(0, document.documentElement.scrollHeight);
537
+ })();
538
+ `;
539
+ }
@@ -0,0 +1,2 @@
1
+ /** Safe HTML for a chat reply written in Markdown. */
2
+ export declare function renderSlotlockMarkdown(source: string): string;