@tangleai/assistant 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/views.d.ts ADDED
@@ -0,0 +1,332 @@
1
+ /**
2
+ * The AI assistant panel — mode 'assistant', dispatched with
3
+ * `$.ui.assistant` as the current node. A global slide-out on every
4
+ * page: a bring-your-own-key chat that drives the playground through
5
+ * @tangleai/agents's schema-guarded tools. Assistant replies render through
6
+ * the @jarenjs/md component (dogfooding); the streaming reply is plain
7
+ * text because it changes per token.
8
+ */
9
+ /** One instance owns its datalist identifier and host copy. */
10
+ export function createAssistantRules(modelListId: any): ({
11
+ match: string;
12
+ mode: string;
13
+ body: (string | {
14
+ class: {
15
+ $if: string[];
16
+ };
17
+ $if?: undefined;
18
+ } | (string | {
19
+ type: string;
20
+ class: string;
21
+ title: string;
22
+ 'aria-label': string;
23
+ on: {
24
+ click: string;
25
+ };
26
+ } | (string | {
27
+ class: string;
28
+ })[])[] | {
29
+ $if: (string | (string | {
30
+ class: string;
31
+ $if?: undefined;
32
+ } | (string | {
33
+ class: string;
34
+ } | (string | {
35
+ class: string;
36
+ })[] | (string | {
37
+ type: string;
38
+ class: string;
39
+ title: string;
40
+ 'aria-label': string;
41
+ on: {
42
+ click: string;
43
+ };
44
+ })[])[] | {
45
+ $if: (string | (string | {
46
+ class: string;
47
+ on: {
48
+ submit: {
49
+ action: string;
50
+ preventDefault: boolean;
51
+ };
52
+ };
53
+ $if?: undefined;
54
+ } | (string | {
55
+ class: string;
56
+ } | {}[] | (string | {
57
+ class: string;
58
+ on: {
59
+ change: {
60
+ action: string;
61
+ with: {
62
+ key: string;
63
+ };
64
+ };
65
+ };
66
+ } | {
67
+ $apply: string;
68
+ }[])[])[] | {
69
+ $if: (string | (string | {
70
+ class: string;
71
+ } | {}[] | (string | {
72
+ type: string;
73
+ class: string;
74
+ spellcheck: string;
75
+ autocomplete: string;
76
+ placeholder: string;
77
+ value: string;
78
+ on: {
79
+ input: {
80
+ action: string;
81
+ with: {
82
+ key: string;
83
+ };
84
+ };
85
+ };
86
+ })[])[])[];
87
+ class?: undefined;
88
+ on?: undefined;
89
+ } | (string | {
90
+ class: string;
91
+ } | {}[] | (string | {
92
+ type: string;
93
+ class: string;
94
+ spellcheck: string;
95
+ placeholder: string;
96
+ value: string;
97
+ on: {
98
+ input: {
99
+ action: string;
100
+ with: {
101
+ key: string;
102
+ };
103
+ };
104
+ };
105
+ })[])[] | (string | {
106
+ class: string;
107
+ } | {}[] | (string | {
108
+ type: string;
109
+ class: string;
110
+ spellcheck: string;
111
+ list: any;
112
+ placeholder: string;
113
+ value: string;
114
+ on: {
115
+ input: {
116
+ action: string;
117
+ with: {
118
+ key: string;
119
+ };
120
+ };
121
+ };
122
+ })[] | (string | {
123
+ id: any;
124
+ } | {
125
+ $apply: string;
126
+ }[])[])[] | (string | {
127
+ class: string;
128
+ } | (string | {
129
+ type: string;
130
+ class: string;
131
+ })[] | (string | {
132
+ type: string;
133
+ class: string;
134
+ disabled: {
135
+ $if: (string | boolean)[];
136
+ };
137
+ on: {
138
+ click: string;
139
+ };
140
+ $if?: undefined;
141
+ } | {
142
+ $if: string[];
143
+ type?: undefined;
144
+ class?: undefined;
145
+ disabled?: undefined;
146
+ on?: undefined;
147
+ })[])[])[])[];
148
+ class?: undefined;
149
+ } | {
150
+ $if: (string | {
151
+ $if: (string | (string | {
152
+ class: string;
153
+ $if?: undefined;
154
+ } | (string | {
155
+ class: string;
156
+ } | (string | {
157
+ class: string;
158
+ })[] | (string | {
159
+ type: string;
160
+ class: string;
161
+ title: string;
162
+ 'aria-label': string;
163
+ on: {
164
+ click: string;
165
+ };
166
+ })[])[] | (string | {
167
+ class: string;
168
+ } | {
169
+ $apply: string;
170
+ }[])[] | {
171
+ $if: (string | (string | {
172
+ class: string;
173
+ })[])[];
174
+ class?: undefined;
175
+ } | (string | {
176
+ class: string;
177
+ $if?: undefined;
178
+ } | (string | {
179
+ type: string;
180
+ class: string;
181
+ disabled: {
182
+ $if: (string | boolean)[];
183
+ };
184
+ on: {
185
+ click: string;
186
+ };
187
+ $if?: undefined;
188
+ } | {
189
+ $if: string[];
190
+ type?: undefined;
191
+ class?: undefined;
192
+ disabled?: undefined;
193
+ on?: undefined;
194
+ })[] | {
195
+ $if: (string | (string | {
196
+ class: string;
197
+ })[])[];
198
+ class?: undefined;
199
+ })[])[] | (string | {
200
+ class: string;
201
+ on: {
202
+ submit: {
203
+ action: string;
204
+ preventDefault: boolean;
205
+ };
206
+ };
207
+ } | (string | {
208
+ type: string;
209
+ class: string;
210
+ spellcheck: string;
211
+ placeholder: string;
212
+ value: string;
213
+ on: {
214
+ input: string;
215
+ };
216
+ })[] | (string | {
217
+ type: string;
218
+ class: string;
219
+ })[])[])[];
220
+ })[];
221
+ class?: undefined;
222
+ } | (string | {
223
+ class: string;
224
+ $if?: undefined;
225
+ } | {
226
+ $if: (string | {
227
+ $if: (string | (string | {
228
+ class: string;
229
+ } | {}[])[])[];
230
+ })[];
231
+ class?: undefined;
232
+ } | {
233
+ $apply: string;
234
+ }[] | {
235
+ $if: ({
236
+ $and: string[];
237
+ } | (string | {
238
+ class: string;
239
+ } | {}[])[])[];
240
+ class?: undefined;
241
+ } | {
242
+ $if: (string | (string | {
243
+ class: string;
244
+ } | {}[])[])[];
245
+ class?: undefined;
246
+ } | {
247
+ $if: ({
248
+ $and: ({
249
+ $eq: string[];
250
+ $not?: undefined;
251
+ } | {
252
+ $not: string;
253
+ $eq?: undefined;
254
+ })[];
255
+ } | (string | {
256
+ class: string;
257
+ })[])[];
258
+ class?: undefined;
259
+ })[] | {
260
+ $if: (string | (string | {
261
+ class: string;
262
+ on: {
263
+ submit: {
264
+ action: string;
265
+ preventDefault: boolean;
266
+ };
267
+ };
268
+ $if?: undefined;
269
+ } | (string | {
270
+ class: string;
271
+ rows: number;
272
+ spellcheck: string;
273
+ placeholder: string;
274
+ value: string;
275
+ on: {
276
+ input: string;
277
+ };
278
+ })[] | (string | {
279
+ type: string;
280
+ class: string;
281
+ disabled: {
282
+ $if: (string | boolean | {
283
+ $eq: string[];
284
+ })[];
285
+ };
286
+ $if?: undefined;
287
+ } | {
288
+ $if: (string | {
289
+ $eq: string[];
290
+ })[];
291
+ type?: undefined;
292
+ class?: undefined;
293
+ disabled?: undefined;
294
+ })[] | {
295
+ $if: (string | (string | {
296
+ type: string;
297
+ class: string;
298
+ on: {
299
+ click: string;
300
+ };
301
+ })[])[];
302
+ class?: undefined;
303
+ on?: undefined;
304
+ })[])[];
305
+ class?: undefined;
306
+ })[])[];
307
+ class?: undefined;
308
+ })[];
309
+ } | {
310
+ match: string;
311
+ mode: string;
312
+ body: (string | {
313
+ value: string;
314
+ selected: string;
315
+ })[];
316
+ } | {
317
+ match: string;
318
+ mode: string;
319
+ body: (string | {
320
+ value: string;
321
+ })[];
322
+ } | {
323
+ match: string;
324
+ mode: string;
325
+ body: (string | {
326
+ class: string;
327
+ } | {}[])[];
328
+ } | {
329
+ match: string;
330
+ mode: string;
331
+ body: {}[];
332
+ })[];
package/src/views.js ADDED
@@ -0,0 +1,208 @@
1
+ //@ts-check
2
+ /**
3
+ * The AI assistant panel — mode 'assistant', dispatched with
4
+ * `$.ui.assistant` as the current node. A global slide-out on every
5
+ * page: a bring-your-own-key chat that drives the playground through
6
+ * @tangleai/agents's schema-guarded tools. Assistant replies render through
7
+ * the @jarenjs/md component (dogfooding); the streaming reply is plain
8
+ * text because it changes per token.
9
+ */
10
+
11
+ /** One instance owns its datalist identifier and host copy. */
12
+ export function createAssistantRules(modelListId) {
13
+ const settingsForm =
14
+ ['form', { class: 'ai-settings',
15
+ on: { submit: { action: 'ai/save-settings', preventDefault: true } } },
16
+ ['p', { class: 'ai-hint' },
17
+ '$.settingsHint'],
18
+ ['label', { class: 'ai-field' },
19
+ ['span', {}, 'Provider'],
20
+ ['select', {
21
+ class: 'select',
22
+ on: { change: { action: 'ai/setting', with: { key: 'provider' } } },
23
+ }, [{ $apply: '$.providers[*]' }]],
24
+ ],
25
+ { $if: ['$.settings.needsKey',
26
+ ['label', { class: 'ai-field' },
27
+ ['span', {}, 'API key'],
28
+ ['input', {
29
+ type: 'password', class: 'editor line', spellcheck: 'false',
30
+ autocomplete: 'off', placeholder: 'sk-…', value: '$.settings.apiKey',
31
+ on: { input: { action: 'ai/setting', with: { key: 'apiKey' } } },
32
+ }],
33
+ ]] },
34
+ ['label', { class: 'ai-field' },
35
+ ['span', {}, 'Base URL'],
36
+ ['input', {
37
+ type: 'text', class: 'editor line', spellcheck: 'false',
38
+ placeholder: 'http://localhost:11434 · https://…/v1',
39
+ value: '$.settings.baseUrl',
40
+ on: { input: { action: 'ai/setting', with: { key: 'baseUrl' } } },
41
+ }],
42
+ ],
43
+ ['label', { class: 'ai-field' },
44
+ ['span', {}, 'Model'],
45
+ ['input', {
46
+ type: 'text', class: 'editor line', spellcheck: 'false', list: modelListId,
47
+ placeholder: 'qwen/qwen3-4b · llama3.2 · …', value: '$.settings.model',
48
+ on: { input: { action: 'ai/setting', with: { key: 'model' } } },
49
+ }],
50
+ // a successful probe fills this in: the input becomes a picker
51
+ ['datalist', { id: modelListId }, [{ $apply: '$.settings.probe.models[*]' }]],
52
+ ],
53
+ ['div', { class: 'ai-settings-actions' },
54
+ ['button', { type: 'submit', class: 'btn small' }, 'Save'],
55
+ ['button', {
56
+ type: 'button', class: 'btn small',
57
+ disabled: { $if: ['$.settings.probe.busy', 'disabled', false] },
58
+ on: { click: 'ai/probe' },
59
+ }, { $if: ['$.settings.probe.busy', 'Testing…', 'Test connection'] }],
60
+ ],
61
+ { $if: ['$.settings.probe.ok', ['p', { class: 'ai-hint' }, '$.settings.probe.detail']] },
62
+ { $if: ['$.settings.probe.fail', ['p', { class: 'ai-error' }, '$.settings.probe.detail']] },
63
+ ];
64
+
65
+ const intro = ['div', { class: 'ai-intro' }, ['p', {}, '$.intro'], ['ul', {}, [{ $apply: '$.capabilities[*]' }]]];
66
+
67
+ // unconfigured: no composer to type into yet — point at the settings
68
+ // form above instead of presenting a chat that cannot send
69
+ const setupIntro =
70
+ ['div', { class: 'ai-intro' },
71
+ ['p', {}, 'Pick a provider above, add a model (and a key for OpenRouter), then save — the chat opens right here. Nothing leaves your browser except the calls to the provider you choose.'],
72
+ ];
73
+
74
+ const composer =
75
+ ['form', { class: 'ai-composer',
76
+ on: { submit: { action: 'ai/send', preventDefault: true } } },
77
+ ['textarea', {
78
+ class: 'editor', rows: 2, spellcheck: 'false',
79
+ placeholder: 'Ask the assistant…', value: '$.draft',
80
+ on: { input: 'ai/draft' },
81
+ }],
82
+ ['button', {
83
+ type: 'submit', class: 'btn small ai-send',
84
+ disabled: { $if: [{ $eq: ['$.status', 'streaming'] }, 'disabled', false] },
85
+ }, { $if: [{ $eq: ['$.status', 'streaming'] }, '…', 'Send'] }],
86
+ { $if: ['$.streaming', ['button', { type: 'button', class: 'btn small', on: { click: 'ai/cancel' } }, 'Cancel']] },
87
+ ];
88
+
89
+ // The ledger surface: one objective that outlives the tab, what has
90
+ // been recorded against it, and the archived rounds a compacted session
91
+ // can still reach. Without a goal it is a single input — the smallest
92
+ // thing that can be ignored.
93
+ const goalForm =
94
+ ['form', { class: 'ai-goal-set',
95
+ on: { submit: { action: 'ai/goal-set', preventDefault: true } } },
96
+ ['input', {
97
+ type: 'text', class: 'editor line', spellcheck: 'false',
98
+ placeholder: 'Set an objective the assistant keeps across sessions…',
99
+ value: '$.goalDraft',
100
+ on: { input: 'ai/goal-draft' },
101
+ }],
102
+ ['button', { type: 'submit', class: 'btn small' }, 'Set'],
103
+ ];
104
+
105
+ const goalPanel =
106
+ ['div', { class: 'ai-goal' },
107
+ ['div', { class: 'ai-goal-head' },
108
+ ['span', { class: 'ai-goal-label' }, 'Objective'],
109
+ ['button', {
110
+ type: 'button', class: 'ai-icon', title: 'Clear the objective',
111
+ 'aria-label': 'Clear the objective', on: { click: 'ai/goal-clear' },
112
+ }, '✕'],
113
+ ],
114
+ ['p', { class: 'ai-goal-text' }, '$.goal.objective'],
115
+ ['ul', { class: 'ai-goal-progress' }, [{ $apply: '$.goal.progress[*]' }]],
116
+ { $if: ['$.goal.checkpointLabel', ['p', { class: 'ai-hint' }, '$.goal.checkpointLabel']] },
117
+ { $if: ['$.goal.more', ['p', { class: 'ai-hint' }, '…and ', '$.goal.more', ' earlier entries']] },
118
+ ['div', { class: 'ai-goal-actions' },
119
+ ['button', {
120
+ type: 'button', class: 'btn small',
121
+ disabled: { $if: ['$.remembering', 'disabled', false] },
122
+ on: { click: 'ai/remember' },
123
+ }, { $if: ['$.remembering', 'Remembering…', 'Remember this session'] }],
124
+ { $if: ['$.memories', ['span', { class: 'ai-hint' }, '$.memoryLabel']] },
125
+ ],
126
+ { $if: ['$.remembered', ['p', { class: 'ai-hint' }, '$.remembered']] },
127
+ ];
128
+
129
+ const panel =
130
+ ['div', { class: 'ai-panel' },
131
+ ['div', { class: 'ai-head' },
132
+ ['span', { class: 'ai-title' }, '$.title'],
133
+ ['button', {
134
+ type: 'button', class: 'ai-icon', title: 'Settings',
135
+ 'aria-label': 'Assistant settings', on: { click: 'ai/settings-toggle' },
136
+ }, '⚙'],
137
+ ['button', {
138
+ type: 'button', class: 'ai-icon', title: 'Clear the conversation',
139
+ 'aria-label': 'Clear conversation', on: { click: 'ai/clear' },
140
+ }, '⌫'],
141
+ ['button', {
142
+ type: 'button', class: 'ai-icon', title: 'Close',
143
+ 'aria-label': 'Close assistant', on: { click: 'ai/toggle' },
144
+ }, '✕'],
145
+ ],
146
+ { $if: ['$.showSettings', settingsForm] },
147
+ { $if: ['$.configured', { $if: ['$.goal', goalPanel, goalForm] }] },
148
+ ['div', { class: 'ai-log' },
149
+ { $if: ['$.empty', { $if: ['$.configured', intro, setupIntro] }] },
150
+ [{ $apply: '$.messages[*]' }],
151
+ { $if: [{ $and: ['$.streaming', '$.pending'] },
152
+ ['div', { class: 'ai-msg assistant' }, ['p', {}, '$.pending']]] },
153
+ { $if: ['$.activity',
154
+ ['p', { class: 'ai-activity' }, 'Running ', ['code', {}, '$.activity'], '…']] },
155
+ { $if: [{ $and: [{ $eq: ['$.status', 'streaming'] }, { $not: '$.pending' }, { $not: '$.activity' }] },
156
+ ['p', { class: 'ai-activity' }, '$.thinkingLabel']] },
157
+ { $if: ['$.error', ['p', { class: 'ai-error' }, '$.error']] },
158
+ // a compacted session says so: the rounds that left the request are
159
+ // in slots with addresses, not gone, and the panel is where that
160
+ // stops being an implementation detail
161
+ { $if: ['$.persistenceLabel', ['p', { class: 'ai-archived' }, '$.persistenceLabel']] },
162
+ { $if: ['$.retentionLabel', ['p', { class: 'ai-archived' }, '$.retentionLabel']] },
163
+ { $if: ['$.archived', ['p', { class: 'ai-archived' }, '$.archivedLabel']] },
164
+ ],
165
+ { $if: ['$.configured', composer] },
166
+ ];
167
+
168
+ return [
169
+ {
170
+ match: '$.ui.assistant', mode: 'assistant',
171
+ body: ['div', { class: { $if: ['$.open', 'ai open', 'ai'] } },
172
+ ['button', {
173
+ type: 'button', class: 'ai-launch', title: '$.launchTitle',
174
+ 'aria-label': '$.launchTitle', on: { click: 'ai/toggle' },
175
+ }, '✦', ['span', { class: 'ai-launch-label' }, '$.launchLabel']],
176
+ { $if: ['$.open', panel] },
177
+ ],
178
+ },
179
+ { match: '$.ui.assistant.capabilities[*]', mode: 'assistant', body: ['li', {}, '$.text'] },
180
+ {
181
+ match: '$.ui.assistant.providers[*]', mode: 'assistant',
182
+ body: ['option', { value: '$.value', selected: '$.selected' }, '$.label'],
183
+ },
184
+ {
185
+ match: '$.ui.assistant.settings.probe.models[*]', mode: 'assistant',
186
+ body: ['option', { value: '$.id' }],
187
+ },
188
+ {
189
+ match: "$.ui.assistant.messages[?@.role == 'user']", mode: 'assistant',
190
+ body: ['div', { class: 'ai-msg user' }, ['p', {}, '$.text']],
191
+ },
192
+ {
193
+ // the assistant reply is a ready-made @jarenjs/md vnode, spliced
194
+ // in verbatim (no dispatch into it)
195
+ match: "$.ui.assistant.messages[?@.role == 'assistant']", mode: 'assistant',
196
+ body: ['div', { class: 'ai-msg assistant' }, '$.article'],
197
+ },
198
+ {
199
+ // one recorded step towards the objective. The evidence is shown
200
+ // beside the note, always: a progress log of unevidenced claims is
201
+ // exactly what the ledger's schema refuses to store, and the panel
202
+ // should not present one either.
203
+ match: '$.ui.assistant.goal.progress[*]', mode: 'assistant',
204
+ body: ['li', {}, '$.note', ['span', { class: 'ai-evidence' }, '$.evidence']],
205
+ },
206
+ ];
207
+
208
+ }
@@ -0,0 +1,115 @@
1
+ /* ---- the AI assistant slide-out (@tangleai/assistant) ---- */
2
+ :where(.tangle-assistant) .ai-launch {
3
+ position: fixed; right: var(--space-5, 1.25rem); bottom: var(--space-5, 1.25rem); z-index: 150;
4
+ min-height: 2.75rem; /* primary tap target (DESIGN §5) */
5
+ border: 1px solid var(--accent, #3b82f6); border-radius: 999px;
6
+ padding: var(--space-2, .5rem) var(--space-4, 1rem); font-size: 0.9rem; font-weight: 600;
7
+ background: var(--accent, #3b82f6); color: var(--accent-fg, white); cursor: pointer;
8
+ box-shadow: 0 6px 24px rgb(0 0 0 / 0.18);
9
+ }
10
+ :where(.tangle-assistant) .ai-launch:hover { background: var(--accent-hover, #2563eb); border-color: var(--accent-hover, #2563eb); }
11
+ :where(.tangle-assistant) .ai.open .ai-launch { display: none; }
12
+ :where(.tangle-assistant) .ai-panel {
13
+ position: fixed; right: var(--space-5, 1.25rem); bottom: var(--space-5, 1.25rem); z-index: 150;
14
+ display: flex; flex-direction: column; width: min(28rem, calc(100vw - 2 * var(--space-5, 1.25rem)));
15
+ max-height: min(40rem, calc(100dvh - 2 * var(--space-5, 1.25rem)));
16
+ background: var(--surface, #fff); border: 1px solid var(--border, #d6dae1); border-radius: var(--radius, .625rem);
17
+ box-shadow: 0 12px 40px rgb(0 0 0 / 0.22); overflow: hidden;
18
+ }
19
+ :where(.tangle-assistant) .ai-head {
20
+ display: flex; align-items: center; gap: var(--space-2, .5rem);
21
+ padding: var(--space-3, .75rem) var(--space-4, 1rem); border-bottom: 1px solid var(--border, #d6dae1);
22
+ }
23
+ :where(.tangle-assistant) .ai-title { font-weight: 700; margin-right: auto; }
24
+ :where(.tangle-assistant) .ai-icon {
25
+ border: 0; background: transparent; color: var(--muted, #596273); cursor: pointer;
26
+ width: 2.75rem; height: 2.75rem; /* primary tap targets (DESIGN §5) */
27
+ border-radius: var(--radius-sm, .5rem); font-size: 1rem;
28
+ }
29
+ :where(.tangle-assistant) .ai-icon:hover { color: var(--accent, #3b82f6); background: var(--accent-soft, #eff6ff); }
30
+ :where(.tangle-assistant) .ai-settings {
31
+ display: flex; flex-direction: column; gap: var(--space-3, .75rem);
32
+ padding: var(--space-4, 1rem); border-bottom: 1px solid var(--border, #d6dae1);
33
+ }
34
+ :where(.tangle-assistant) .ai-hint { color: var(--muted, #596273); font-size: 0.82rem; margin: 0; }
35
+ :where(.tangle-assistant) .ai-field { display: flex; flex-direction: column; gap: var(--space-1, .25rem); font-size: 0.85rem; }
36
+ :where(.tangle-assistant) .ai-field > span { color: var(--muted, #596273); }
37
+ :where(.tangle-assistant) .ai-settings .btn { align-self: flex-start; }
38
+ :where(.tangle-assistant) .ai-settings-actions { display: flex; gap: var(--space-2, .5rem); }
39
+ /* the ledger surface: the objective that outlives the tab, its evidenced
40
+ progress, and what a compacted session can still recall */
41
+ :where(.tangle-assistant) .ai-goal, :where(.tangle-assistant) .ai-goal-set {
42
+ display: flex; gap: var(--space-2, .5rem);
43
+ padding: var(--space-3, .75rem) var(--space-4, 1rem); border-bottom: 1px solid var(--border, #d6dae1);
44
+ }
45
+ :where(.tangle-assistant) .ai-goal { flex-direction: column; }
46
+ :where(.tangle-assistant) .ai-goal-set > input { flex: 1; min-width: 0; }
47
+ :where(.tangle-assistant) .ai-goal-head { display: flex; align-items: center; gap: var(--space-2, .5rem); }
48
+ :where(.tangle-assistant) .ai-goal-label {
49
+ color: var(--muted, #596273); font-size: 0.75rem; font-weight: 600;
50
+ letter-spacing: 0.04em; text-transform: uppercase; margin-right: auto;
51
+ }
52
+ :where(.tangle-assistant) .ai-goal-text { margin: 0; font-size: 0.9rem; font-weight: 600; overflow-wrap: anywhere; }
53
+ :where(.tangle-assistant) .ai-goal-progress {
54
+ margin: 0; padding-left: var(--space-5, 1.25rem);
55
+ color: var(--muted, #596273); font-size: 0.82rem;
56
+ }
57
+ :where(.tangle-assistant) .ai-goal-progress li { margin: var(--space-1, .25rem) 0; overflow-wrap: anywhere; }
58
+ :where(.tangle-assistant) .ai-goal-progress:empty { display: none; }
59
+ :where(.tangle-assistant) .ai-evidence { display: block; color: var(--muted, #596273); font-size: 0.75rem; font-style: italic; }
60
+ :where(.tangle-assistant) .ai-goal-actions { display: flex; align-items: center; gap: var(--space-2, .5rem); flex-wrap: wrap; }
61
+ :where(.tangle-assistant) .ai-log {
62
+ flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
63
+ display: flex; flex-direction: column; gap: var(--space-3, .75rem); padding: var(--space-4, 1rem);
64
+ }
65
+ :where(.tangle-assistant) .ai-archived {
66
+ color: var(--muted, #596273); font-size: 0.78rem; margin: 0;
67
+ border-top: 1px dashed var(--border, #d6dae1); padding-top: var(--space-2, .5rem);
68
+ }
69
+ :where(.tangle-assistant) .ai-intro { color: var(--muted, #596273); font-size: 0.88rem; }
70
+ :where(.tangle-assistant) .ai-intro ul { margin: var(--space-2, .5rem) 0 0; padding-left: var(--space-5, 1.25rem); }
71
+ :where(.tangle-assistant) .ai-intro li { margin: var(--space-1, .25rem) 0; }
72
+ :where(.tangle-assistant) .ai-msg { border-radius: var(--radius-sm, .5rem); padding: var(--space-2, .5rem) var(--space-3, .75rem); font-size: 0.9rem; }
73
+ :where(.tangle-assistant) .ai-msg.user { background: var(--accent-soft, #eff6ff); color: var(--fg, #182230); align-self: flex-end; max-width: 85%; }
74
+ :where(.tangle-assistant) .ai-msg.user p { margin: 0; overflow-wrap: anywhere; }
75
+ :where(.tangle-assistant) .ai-msg.assistant { background: transparent; padding-inline: 0; }
76
+ :where(.tangle-assistant) .ai-msg.assistant .md { font-size: 0.9rem; }
77
+ :where(.tangle-assistant) .ai-activity { color: var(--muted, #596273); font-size: 0.82rem; margin: 0; }
78
+ :where(.tangle-assistant) .ai-error {
79
+ color: var(--fail, #bd2525); background: var(--fail-soft, #fff0f0); border-radius: var(--radius-sm, .5rem);
80
+ padding: var(--space-2, .5rem) var(--space-3, .75rem); font-size: 0.85rem; margin: 0;
81
+ }
82
+ :where(.tangle-assistant) .ai-composer {
83
+ display: flex; gap: var(--space-2, .5rem); align-items: flex-end;
84
+ padding: var(--space-3, .75rem) var(--space-4, 1rem); border-top: 1px solid var(--border, #d6dae1);
85
+ }
86
+ :where(.tangle-assistant) .ai-composer .editor { flex: 1; min-height: auto; }
87
+ :where(.tangle-assistant) .ai-send[disabled] { opacity: 0.6; cursor: default; }
88
+ @media (max-width: 760px) {
89
+ :where(.tangle-assistant) .ai-panel {
90
+ right: 0; bottom: 0; left: 0; width: 100vw;
91
+ max-height: 85dvh; border-radius: var(--radius, .625rem) var(--radius, .625rem) 0 0;
92
+ padding-bottom: env(safe-area-inset-bottom);
93
+ }
94
+ :where(.tangle-assistant) .ai-launch { right: var(--space-4, 1rem); bottom: var(--space-4, 1rem); }
95
+ }
96
+ /* small phones: the labeled pill is wide enough to sit on the hero CTA
97
+ or the calculator plot at some fold positions — collapse it to a
98
+ round icon-only launcher (the aria-label carries the name; the
99
+ 2.75rem primary tap target holds, DESIGN §5) */
100
+ @media (max-width: 480px) {
101
+ :where(.tangle-assistant) .ai-launch {
102
+ width: 2.75rem; height: 2.75rem; padding: 0;
103
+ display: grid; place-items: center; font-size: 1.05rem;
104
+ }
105
+ :where(.tangle-assistant) .ai-launch-label { display: none; }
106
+ }
107
+
108
+
109
+ :where(.tangle-assistant) { font-family: var(--font, system-ui, sans-serif); color: var(--fg, #182230); line-height: 1.5; }
110
+ :where(.tangle-assistant) *, :where(.tangle-assistant) *::before, :where(.tangle-assistant) *::after { box-sizing: border-box; }
111
+ :where(.tangle-assistant) .btn, :where(.tangle-assistant) .select { min-height: 2.75rem; border: 1px solid var(--border, #d6dae1); border-radius: .5rem; padding: .4rem .75rem; color: var(--fg, #182230); background: var(--surface, white); font: inherit; cursor: pointer; }
112
+ :where(.tangle-assistant) .editor { display: block; width: 100%; min-width: 0; border: 1px solid var(--border, #d6dae1); border-radius: .5rem; background: var(--bg, white); color: var(--fg, #182230); font: inherit; padding: .5rem .75rem; }
113
+ :where(.tangle-assistant) .editor.line { min-height: 2.75rem; }
114
+ :where(.tangle-assistant) :focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
115
+ :where(.tangle-assistant) button:disabled { opacity: .6; cursor: default; }