@tangleai/assistant 0.21.1 → 0.24.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/CHANGELOG.md +42 -0
- package/package.json +8 -8
- package/src/actions.d.ts +36 -12
- package/src/actions.js +141 -142
- package/src/component.d.ts +8 -8
- package/src/component.js +95 -76
- package/src/controller.d.ts +1 -6
- package/src/controller.js +282 -286
- package/src/index.d.ts +4 -4
- package/src/index.js +4 -4
- package/src/settings.d.ts +2 -2
- package/src/settings.js +8 -7
- package/src/state.d.ts +16 -16
- package/src/state.js +34 -35
- package/src/viewmodel.d.ts +3 -3
- package/src/viewmodel.js +71 -73
- package/src/views.d.ts +21 -21
- package/src/views.js +194 -196
package/src/views.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* The AI assistant panel — mode 'assistant', dispatched with
|
|
4
3
|
* `$.ui.assistant` as the current node. A global slide-out on every
|
|
@@ -7,202 +6,201 @@
|
|
|
7
6
|
* the @jarenjs/md component (dogfooding); the streaming reply is plain
|
|
8
7
|
* text because it changes per token.
|
|
9
8
|
*/
|
|
10
|
-
|
|
11
9
|
/** One instance owns its datalist identifier and host copy. */
|
|
12
10
|
export function createAssistantRules(modelListId) {
|
|
13
|
-
const settingsForm =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
const settingsForm = ['form', {
|
|
12
|
+
class: 'ai-settings',
|
|
13
|
+
on: { submit: { action: 'ai/save-settings', preventDefault: true } }
|
|
14
|
+
},
|
|
15
|
+
['p', { class: 'ai-hint' },
|
|
16
|
+
'$.settingsHint'],
|
|
17
|
+
['label', { class: 'ai-field' },
|
|
18
|
+
['span', {}, 'Provider'],
|
|
19
|
+
['select', {
|
|
20
|
+
class: 'select',
|
|
21
|
+
on: { change: { action: 'ai/setting', with: { key: 'provider' } } },
|
|
22
|
+
}, [{ $apply: '$.providers[*]' }]],
|
|
23
|
+
],
|
|
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
|
+
},
|
|
35
|
+
['label', { class: 'ai-field' },
|
|
36
|
+
['span', {}, 'Base URL'],
|
|
37
|
+
['input', {
|
|
38
|
+
type: 'text', class: 'editor line', spellcheck: 'false',
|
|
39
|
+
placeholder: 'http://localhost:11434 · https://…/v1',
|
|
40
|
+
value: '$.settings.baseUrl',
|
|
41
|
+
on: { input: { action: 'ai/setting', with: { key: 'baseUrl' } } },
|
|
42
|
+
}],
|
|
43
|
+
],
|
|
44
|
+
['label', { class: 'ai-field' },
|
|
45
|
+
['span', {}, 'Model'],
|
|
46
|
+
['input', {
|
|
47
|
+
type: 'text', class: 'editor line', spellcheck: 'false', list: modelListId,
|
|
48
|
+
placeholder: 'qwen/qwen3-4b · llama3.2 · …', value: '$.settings.model',
|
|
49
|
+
on: { input: { action: 'ai/setting', with: { key: 'model' } } },
|
|
50
|
+
}],
|
|
51
|
+
// a successful probe fills this in: the input becomes a picker
|
|
52
|
+
['datalist', { id: modelListId }, [{ $apply: '$.settings.probe.models[*]' }]],
|
|
53
|
+
],
|
|
54
|
+
['div', { class: 'ai-settings-actions' },
|
|
55
|
+
['button', { type: 'submit', class: 'btn small' }, 'Save'],
|
|
56
|
+
['button', {
|
|
57
|
+
type: 'button', class: 'btn small',
|
|
58
|
+
disabled: { $if: ['$.settings.probe.busy', 'disabled', false] },
|
|
59
|
+
on: { click: 'ai/probe' },
|
|
60
|
+
}, { $if: ['$.settings.probe.busy', 'Testing…', 'Test connection'] }],
|
|
61
|
+
],
|
|
62
|
+
{ $if: ['$.settings.probe.ok', ['p', { class: 'ai-hint' }, '$.settings.probe.detail']] },
|
|
63
|
+
{ $if: ['$.settings.probe.fail', ['p', { class: 'ai-error' }, '$.settings.probe.detail']] },
|
|
64
|
+
];
|
|
65
|
+
const intro = ['div', { class: 'ai-intro' }, ['p', {}, '$.intro'], ['ul', {}, [{ $apply: '$.capabilities[*]' }]]];
|
|
66
|
+
// unconfigured: no composer to type into yet — point at the settings
|
|
67
|
+
// form above instead of presenting a chat that cannot send
|
|
68
|
+
const setupIntro = ['div', { class: 'ai-intro' },
|
|
69
|
+
['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.'],
|
|
70
|
+
];
|
|
71
|
+
const composer = ['form', {
|
|
72
|
+
class: 'ai-composer',
|
|
73
|
+
on: { submit: { action: 'ai/send', preventDefault: true } }
|
|
74
|
+
},
|
|
75
|
+
['textarea', {
|
|
76
|
+
class: 'editor', rows: 2, spellcheck: 'false',
|
|
77
|
+
placeholder: 'Ask the assistant…', value: '$.draft',
|
|
78
|
+
on: { input: 'ai/draft' },
|
|
79
|
+
}],
|
|
80
|
+
['button', {
|
|
81
|
+
type: 'submit', class: 'btn small ai-send',
|
|
82
|
+
disabled: { $if: [{ $eq: ['$.status', 'streaming'] }, 'disabled', false] },
|
|
83
|
+
}, { $if: [{ $eq: ['$.status', 'streaming'] }, '…', 'Send'] }],
|
|
84
|
+
{ $if: ['$.streaming', ['button', { type: 'button', class: 'btn small', on: { click: 'ai/cancel' } }, 'Cancel']] },
|
|
85
|
+
];
|
|
86
|
+
// The ledger surface: one objective that outlives the tab, what has
|
|
87
|
+
// been recorded against it, and the archived rounds a compacted session
|
|
88
|
+
// can still reach. Without a goal it is a single input — the smallest
|
|
89
|
+
// thing that can be ignored.
|
|
90
|
+
const goalForm = ['form', {
|
|
91
|
+
class: 'ai-goal-set',
|
|
92
|
+
on: { submit: { action: 'ai/goal-set', preventDefault: true } }
|
|
93
|
+
},
|
|
28
94
|
['input', {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
95
|
+
type: 'text', class: 'editor line', spellcheck: 'false',
|
|
96
|
+
placeholder: 'Set an objective the assistant keeps across sessions…',
|
|
97
|
+
value: '$.goalDraft',
|
|
98
|
+
on: { input: 'ai/goal-draft' },
|
|
99
|
+
}],
|
|
100
|
+
['button', { type: 'submit', class: 'btn small' }, 'Set'],
|
|
101
|
+
];
|
|
102
|
+
const goalPanel = ['div', { class: 'ai-goal' },
|
|
103
|
+
['div', { class: 'ai-goal-head' },
|
|
104
|
+
['span', { class: 'ai-goal-label' }, 'Objective'],
|
|
105
|
+
['button', {
|
|
106
|
+
type: 'button', class: 'ai-icon', title: 'Clear the objective',
|
|
107
|
+
'aria-label': 'Clear the objective', on: { click: 'ai/goal-clear' },
|
|
108
|
+
}, '✕'],
|
|
109
|
+
],
|
|
110
|
+
['p', { class: 'ai-goal-text' }, '$.goal.objective'],
|
|
111
|
+
['ul', { class: 'ai-goal-progress' }, [{ $apply: '$.goal.progress[*]' }]],
|
|
112
|
+
{ $if: ['$.goal.checkpointLabel', ['p', { class: 'ai-hint' }, '$.goal.checkpointLabel']] },
|
|
113
|
+
{ $if: ['$.goal.more', ['p', { class: 'ai-hint' }, '…and ', '$.goal.more', ' earlier entries']] },
|
|
114
|
+
['div', { class: 'ai-goal-actions' },
|
|
115
|
+
['button', {
|
|
116
|
+
type: 'button', class: 'btn small',
|
|
117
|
+
disabled: { $if: ['$.remembering', 'disabled', false] },
|
|
118
|
+
on: { click: 'ai/remember' },
|
|
119
|
+
}, { $if: ['$.remembering', 'Remembering…', 'Remember this session'] }],
|
|
120
|
+
{ $if: ['$.memories', ['span', { class: 'ai-hint' }, '$.memoryLabel']] },
|
|
121
|
+
],
|
|
122
|
+
{ $if: ['$.remembered', ['p', { class: 'ai-hint' }, '$.remembered']] },
|
|
123
|
+
];
|
|
124
|
+
const panel = ['div', { class: 'ai-panel' },
|
|
125
|
+
['div', { class: 'ai-head' },
|
|
126
|
+
['span', { class: 'ai-title' }, '$.title'],
|
|
127
|
+
['button', {
|
|
128
|
+
type: 'button', class: 'ai-icon', title: 'Settings',
|
|
129
|
+
'aria-label': 'Assistant settings', on: { click: 'ai/settings-toggle' },
|
|
130
|
+
}, '⚙'],
|
|
131
|
+
['button', {
|
|
132
|
+
type: 'button', class: 'ai-icon', title: 'Clear the conversation',
|
|
133
|
+
'aria-label': 'Clear conversation', on: { click: 'ai/clear' },
|
|
134
|
+
}, '⌫'],
|
|
135
|
+
['button', {
|
|
136
|
+
type: 'button', class: 'ai-icon', title: 'Close',
|
|
137
|
+
'aria-label': 'Close assistant', on: { click: 'ai/toggle' },
|
|
138
|
+
}, '✕'],
|
|
139
|
+
],
|
|
140
|
+
{ $if: ['$.showSettings', settingsForm] },
|
|
141
|
+
{ $if: ['$.configured', { $if: ['$.goal', goalPanel, goalForm] }] },
|
|
142
|
+
['div', { class: 'ai-log' },
|
|
143
|
+
{ $if: ['$.empty', { $if: ['$.configured', intro, setupIntro] }] },
|
|
144
|
+
[{ $apply: '$.messages[*]' }],
|
|
145
|
+
{
|
|
146
|
+
$if: [{ $and: ['$.streaming', '$.pending'] },
|
|
147
|
+
['div', { class: 'ai-msg assistant' }, ['p', {}, '$.pending']]]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
$if: ['$.activity',
|
|
151
|
+
['p', { class: 'ai-activity' }, 'Running ', ['code', {}, '$.activity'], '…']]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
$if: [{ $and: [{ $eq: ['$.status', 'streaming'] }, { $not: '$.pending' }, { $not: '$.activity' }] },
|
|
155
|
+
['p', { class: 'ai-activity' }, '$.thinkingLabel']]
|
|
156
|
+
},
|
|
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
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
match: '$.ui.assistant', mode: 'assistant',
|
|
170
|
+
body: ['div', { class: { $if: ['$.open', 'ai open', 'ai'] } },
|
|
171
|
+
['button', {
|
|
172
|
+
type: 'button', class: 'ai-launch', title: '$.launchTitle',
|
|
173
|
+
'aria-label': '$.launchTitle', on: { click: 'ai/toggle' },
|
|
174
|
+
}, '✦', ['span', { class: 'ai-launch-label' }, '$.launchLabel']],
|
|
175
|
+
{ $if: ['$.open', panel] },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{ match: '$.ui.assistant.capabilities[*]', mode: 'assistant', body: ['li', {}, '$.text'] },
|
|
179
|
+
{
|
|
180
|
+
match: '$.ui.assistant.providers[*]', mode: 'assistant',
|
|
181
|
+
body: ['option', { value: '$.value', selected: '$.selected' }, '$.label'],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
match: '$.ui.assistant.settings.probe.models[*]', mode: 'assistant',
|
|
185
|
+
body: ['option', { value: '$.id' }],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
match: "$.ui.assistant.messages[?@.role == 'user']", mode: 'assistant',
|
|
189
|
+
body: ['div', { class: 'ai-msg user' }, ['p', {}, '$.text']],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
// the assistant reply is a ready-made @jarenjs/md vnode, spliced
|
|
193
|
+
// in verbatim (no dispatch into it)
|
|
194
|
+
match: "$.ui.assistant.messages[?@.role == 'assistant']", mode: 'assistant',
|
|
195
|
+
body: ['div', { class: 'ai-msg assistant' }, '$.article'],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
// one recorded step towards the objective. The evidence is shown
|
|
199
|
+
// beside the note, always: a progress log of unevidenced claims is
|
|
200
|
+
// exactly what the ledger's schema refuses to store, and the panel
|
|
201
|
+
// should not present one either.
|
|
202
|
+
match: '$.ui.assistant.goal.progress[*]', mode: 'assistant',
|
|
203
|
+
body: ['li', {}, '$.note', ['span', { class: 'ai-evidence' }, '$.evidence']],
|
|
204
|
+
},
|
|
205
|
+
];
|
|
208
206
|
}
|