@things-factory/ai-assistant 10.1.154 → 10.1.155
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/client/bootstrap.ts +27 -0
- package/client/components/assistant-chat.ts +229 -178
- package/client/components/assistant-dock.ts +53 -48
- package/client/components/assistant-session-manager.ts +82 -100
- package/client/components/assistant-session-toolbar.ts +11 -3
- package/client/components/chat-input-builder.ts +8 -2
- package/client/settings/agent-settings-model.ts +164 -0
- package/client/settings/ai-agent-setting-let.ts +684 -0
- package/client/settings/ai-usage-setting-let.ts +252 -0
- package/client/settings/usage-model.ts +46 -0
- package/client/setup-assistant-dock.ts +2 -3
- package/client/utils/agent-choice.ts +56 -0
- package/client/utils/assistant-navigation.test.ts +14 -8
- package/client/utils/assistant-navigation.ts +8 -3
- package/client/utils/composer-frame.ts +28 -2
- package/client/utils/menu-room.ts +33 -0
- package/dist-client/bootstrap.d.ts +8 -0
- package/dist-client/bootstrap.js +26 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/components/assistant-chat.d.ts +32 -4
- package/dist-client/components/assistant-chat.js +234 -175
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +6 -8
- package/dist-client/components/assistant-dock.js +52 -60
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-session-manager.d.ts +8 -7
- package/dist-client/components/assistant-session-manager.js +84 -124
- package/dist-client/components/assistant-session-manager.js.map +1 -1
- package/dist-client/components/assistant-session-toolbar.d.ts +1 -0
- package/dist-client/components/assistant-session-toolbar.js +10 -3
- package/dist-client/components/assistant-session-toolbar.js.map +1 -1
- package/dist-client/components/chat-input-builder.d.ts +4 -0
- package/dist-client/components/chat-input-builder.js +4 -2
- package/dist-client/components/chat-input-builder.js.map +1 -1
- package/dist-client/settings/agent-settings-model.d.ts +93 -0
- package/dist-client/settings/agent-settings-model.js +95 -0
- package/dist-client/settings/agent-settings-model.js.map +1 -0
- package/dist-client/settings/ai-agent-setting-let.d.ts +51 -0
- package/dist-client/settings/ai-agent-setting-let.js +672 -0
- package/dist-client/settings/ai-agent-setting-let.js.map +1 -0
- package/dist-client/settings/ai-usage-setting-let.d.ts +26 -0
- package/dist-client/settings/ai-usage-setting-let.js +276 -0
- package/dist-client/settings/ai-usage-setting-let.js.map +1 -0
- package/dist-client/settings/usage-model.d.ts +32 -0
- package/dist-client/settings/usage-model.js +25 -0
- package/dist-client/settings/usage-model.js.map +1 -0
- package/dist-client/setup-assistant-dock.d.ts +0 -1
- package/dist-client/setup-assistant-dock.js +0 -1
- package/dist-client/setup-assistant-dock.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/agent-choice.d.ts +24 -0
- package/dist-client/utils/agent-choice.js +38 -0
- package/dist-client/utils/agent-choice.js.map +1 -0
- package/dist-client/utils/assistant-navigation.d.ts +8 -3
- package/dist-client/utils/assistant-navigation.js +7 -2
- package/dist-client/utils/assistant-navigation.js.map +1 -1
- package/dist-client/utils/assistant-navigation.test.js +14 -8
- package/dist-client/utils/assistant-navigation.test.js.map +1 -1
- package/dist-client/utils/composer-frame.d.ts +23 -2
- package/dist-client/utils/composer-frame.js +20 -2
- package/dist-client/utils/composer-frame.js.map +1 -1
- package/dist-client/utils/menu-room.d.ts +13 -0
- package/dist-client/utils/menu-room.js +26 -0
- package/dist-client/utils/menu-room.js.map +1 -0
- package/dist-server/service/ai-agent-resolver.d.ts +82 -0
- package/dist-server/service/ai-agent-resolver.js +417 -0
- package/dist-server/service/ai-agent-resolver.js.map +1 -0
- package/dist-server/service/assistant-capability-resolver.d.ts +1 -1
- package/dist-server/service/assistant-capability-resolver.js +7 -7
- package/dist-server/service/assistant-capability-resolver.js.map +1 -1
- package/dist-server/service/assistant-chat-resolver.d.ts +2 -0
- package/dist-server/service/assistant-chat-resolver.js +32 -15
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/resolve.d.ts +0 -1
- package/dist-server/service/assistant-mode/resolve.js +0 -1
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +1 -2
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/chat-session/chat-session.d.ts +2 -0
- package/dist-server/service/chat-session/chat-session.js +5 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +3 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/testing/mode-conformance.js +0 -1
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -7
- package/server/service/ai-agent-resolver.ts +330 -0
- package/server/service/assistant-capability-resolver.ts +7 -7
- package/server/service/assistant-chat-resolver.ts +32 -24
- package/server/service/assistant-mode/resolve.test.ts +0 -2
- package/server/service/assistant-mode/resolve.ts +0 -2
- package/server/service/assistant-mode/types.ts +1 -2
- package/server/service/chat-session/chat-session.ts +5 -0
- package/server/service/index.ts +3 -0
- package/server/service/testing/mode-conformance.ts +0 -1
- package/server/tool-registry-boot-report.test.ts +2 -0
- package/test/ai-agent-settings.test.ts +123 -0
- package/test/assistant-dock.test.ts +3 -2
- package/test/assistant-session-manager.test.ts +10 -4
- package/test/assistant-session-toolbar.test.ts +8 -0
- package/test/composer-frame.test.ts +40 -2
- package/test/menu-room.test.ts +33 -0
- package/test/open-doors.test.ts +3 -1
- package/test/session-discovery.test.ts +7 -6
- package/things-factory.config.js +2 -0
- package/translations/en.json +78 -12
- package/translations/es.json +78 -12
- package/translations/ja.json +78 -12
- package/translations/ko.json +78 -12
- package/translations/ms.json +78 -12
- package/translations/zh.json +78 -12
- package/client/components/chat-triggers.test.ts +0 -72
- package/client/components/chat-triggers.ts +0 -62
- package/dist-client/components/chat-triggers.d.ts +0 -16
- package/dist-client/components/chat-triggers.js +0 -42
- package/dist-client/components/chat-triggers.js.map +0 -1
- package/dist-client/components/chat-triggers.test.d.ts +0 -1
- package/dist-client/components/chat-triggers.test.js +0 -61
- package/dist-client/components/chat-triggers.test.js.map +0 -1
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import '@things-factory/setting-base/dist-client/layout/settinglet.js';
|
|
4
|
+
import gql from 'graphql-tag';
|
|
5
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
6
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
7
|
+
import { client } from '@operato/graphql';
|
|
8
|
+
import { i18next, localize } from '@operato/i18n';
|
|
9
|
+
import { OxPrompt } from '@operato/popup';
|
|
10
|
+
import '@operato/popup/ox-popup-menu.js';
|
|
11
|
+
import { OxPopupMenu } from '@operato/popup/ox-popup-menu.js';
|
|
12
|
+
import { ButtonContainerStyles, FormFieldStyles } from '@operato/styles';
|
|
13
|
+
import { providerLabel } from '../utils/agent-choice.js';
|
|
14
|
+
import { draftOf, groupByOwner, saveInput } from './agent-settings-model.js';
|
|
15
|
+
const t = (key, defaultValue, options = {}) => i18next.t(`ai-assistant.agent.${key}`, { ...options, defaultValue });
|
|
16
|
+
const SETTINGS_QUERY = gql `
|
|
17
|
+
query AiAgentSettings {
|
|
18
|
+
aiAgentSettings {
|
|
19
|
+
chain {
|
|
20
|
+
id
|
|
21
|
+
name
|
|
22
|
+
}
|
|
23
|
+
agents {
|
|
24
|
+
id
|
|
25
|
+
name
|
|
26
|
+
provider
|
|
27
|
+
model
|
|
28
|
+
baseURL
|
|
29
|
+
organization
|
|
30
|
+
active
|
|
31
|
+
owner {
|
|
32
|
+
id
|
|
33
|
+
name
|
|
34
|
+
}
|
|
35
|
+
inherited
|
|
36
|
+
turnedOff
|
|
37
|
+
keyState
|
|
38
|
+
keyDomain {
|
|
39
|
+
id
|
|
40
|
+
name
|
|
41
|
+
}
|
|
42
|
+
keyHint
|
|
43
|
+
usable
|
|
44
|
+
isDefault
|
|
45
|
+
usedBelow {
|
|
46
|
+
id
|
|
47
|
+
name
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
defaultAgentId
|
|
51
|
+
defaultFrom
|
|
52
|
+
state
|
|
53
|
+
reason
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
const MODELS_QUERY = gql `
|
|
58
|
+
query AiProviderModels($provider: String!, $apiKey: String, $agentId: String, $baseURL: String, $organization: String) {
|
|
59
|
+
aiProviderModels(provider: $provider, apiKey: $apiKey, agentId: $agentId, baseURL: $baseURL, organization: $organization)
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
const SAVE_MUTATION = gql `
|
|
63
|
+
mutation SaveAiAgents($input: AiAgentsInput!) {
|
|
64
|
+
saveAiAgents(input: $input)
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
const PROVIDERS = ['openai', 'anthropic', 'gemini'];
|
|
68
|
+
/**
|
|
69
|
+
* A domain's AI agents - the card in Settings › {domain} settings (design/ai/agent-settings.md §7-1, §7-2).
|
|
70
|
+
*
|
|
71
|
+
* The first line says whether AI works here and, if not, the one thing that is missing. The list is grouped by the
|
|
72
|
+
* domain that declared each agent: this domain's own, editable, then each domain above, read-only, closest first. A key
|
|
73
|
+
* is written and never shown again; the model is only ever picked from the list the provider gives that key.
|
|
74
|
+
*/
|
|
75
|
+
let AiAgentSettingLet = class AiAgentSettingLet extends localize(i18next)(LitElement) {
|
|
76
|
+
constructor() {
|
|
77
|
+
super(...arguments);
|
|
78
|
+
this.models = { state: 'idle', models: [] };
|
|
79
|
+
this.saving = false;
|
|
80
|
+
this.keyDraft = '';
|
|
81
|
+
this.modelsToken = 0;
|
|
82
|
+
}
|
|
83
|
+
static { this.styles = [
|
|
84
|
+
/* The button look comes from the action bar's styles, which also restyle select and text inputs - the form
|
|
85
|
+
field rules come after them, so every field in the card has the standard field look. */
|
|
86
|
+
ButtonContainerStyles,
|
|
87
|
+
FormFieldStyles,
|
|
88
|
+
css `
|
|
89
|
+
:host {
|
|
90
|
+
display: block;
|
|
91
|
+
}
|
|
92
|
+
[slot='content'] {
|
|
93
|
+
display: grid;
|
|
94
|
+
gap: 12px;
|
|
95
|
+
text-transform: none;
|
|
96
|
+
}
|
|
97
|
+
.head {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 8px;
|
|
101
|
+
}
|
|
102
|
+
.status {
|
|
103
|
+
flex: 1;
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
106
|
+
}
|
|
107
|
+
.status.off {
|
|
108
|
+
color: var(--md-sys-color-error);
|
|
109
|
+
}
|
|
110
|
+
.group-title {
|
|
111
|
+
margin: 4px 0 0;
|
|
112
|
+
font-size: 11px;
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
115
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
116
|
+
padding-bottom: 2px;
|
|
117
|
+
}
|
|
118
|
+
.row {
|
|
119
|
+
display: grid;
|
|
120
|
+
grid-template-columns: 20px minmax(100px, 1.2fr) 70px minmax(120px, 1.4fr) minmax(110px, 1fr) auto auto 28px;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 8px;
|
|
123
|
+
min-height: 32px;
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
}
|
|
126
|
+
.row.muted > :not(.more-menu) {
|
|
127
|
+
opacity: 0.55;
|
|
128
|
+
}
|
|
129
|
+
.name {
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
text-overflow: ellipsis;
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
}
|
|
135
|
+
.model {
|
|
136
|
+
font-family: var(--md-sys-typescale-mono-font, monospace);
|
|
137
|
+
font-size: 12px;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
text-overflow: ellipsis;
|
|
140
|
+
white-space: nowrap;
|
|
141
|
+
}
|
|
142
|
+
.key {
|
|
143
|
+
font-size: 12px;
|
|
144
|
+
white-space: nowrap;
|
|
145
|
+
}
|
|
146
|
+
.key.missing {
|
|
147
|
+
color: var(--md-sys-color-error);
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
.use {
|
|
151
|
+
display: inline-flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 4px;
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
.below {
|
|
158
|
+
border: none;
|
|
159
|
+
background: none;
|
|
160
|
+
padding: 0;
|
|
161
|
+
height: auto;
|
|
162
|
+
font-size: 11px;
|
|
163
|
+
font-weight: 400;
|
|
164
|
+
color: var(--md-sys-color-primary);
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
}
|
|
167
|
+
.more-menu {
|
|
168
|
+
border: none;
|
|
169
|
+
background: none;
|
|
170
|
+
height: auto;
|
|
171
|
+
padding: 2px 6px;
|
|
172
|
+
font-size: 14px;
|
|
173
|
+
font-weight: 400;
|
|
174
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
175
|
+
}
|
|
176
|
+
form.editor {
|
|
177
|
+
display: grid;
|
|
178
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
179
|
+
gap: 12px;
|
|
180
|
+
padding: 12px;
|
|
181
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
182
|
+
border-radius: var(--md-sys-shape-corner-small);
|
|
183
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
184
|
+
}
|
|
185
|
+
@media (max-width: 600px) {
|
|
186
|
+
form.editor {
|
|
187
|
+
grid-template-columns: 1fr;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
form.editor label {
|
|
191
|
+
display: grid;
|
|
192
|
+
gap: 4px;
|
|
193
|
+
font-size: 11px;
|
|
194
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
195
|
+
}
|
|
196
|
+
form.editor input,
|
|
197
|
+
form.editor select {
|
|
198
|
+
height: var(--form-element-height-medium, 32px);
|
|
199
|
+
box-sizing: border-box;
|
|
200
|
+
font-size: 13px;
|
|
201
|
+
}
|
|
202
|
+
.with-action {
|
|
203
|
+
display: flex;
|
|
204
|
+
gap: 4px;
|
|
205
|
+
}
|
|
206
|
+
.with-action > select {
|
|
207
|
+
flex: 1;
|
|
208
|
+
min-width: 0;
|
|
209
|
+
}
|
|
210
|
+
.hint {
|
|
211
|
+
font-size: 11px;
|
|
212
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
213
|
+
}
|
|
214
|
+
.error {
|
|
215
|
+
font-size: 11.5px;
|
|
216
|
+
color: var(--md-sys-color-error);
|
|
217
|
+
}
|
|
218
|
+
details.advanced {
|
|
219
|
+
grid-column: 1 / -1;
|
|
220
|
+
font-size: 12px;
|
|
221
|
+
}
|
|
222
|
+
details.advanced[open] > div {
|
|
223
|
+
display: grid;
|
|
224
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
225
|
+
gap: 12px;
|
|
226
|
+
margin-top: 8px;
|
|
227
|
+
}
|
|
228
|
+
.actions {
|
|
229
|
+
grid-column: 1 / -1;
|
|
230
|
+
display: flex;
|
|
231
|
+
justify-content: flex-end;
|
|
232
|
+
gap: 8px;
|
|
233
|
+
}
|
|
234
|
+
button.primary {
|
|
235
|
+
background: var(--md-sys-color-primary);
|
|
236
|
+
color: var(--md-sys-color-on-primary);
|
|
237
|
+
border-color: var(--md-sys-color-primary);
|
|
238
|
+
}
|
|
239
|
+
button.primary md-icon {
|
|
240
|
+
color: inherit;
|
|
241
|
+
}
|
|
242
|
+
.key-override {
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: 8px;
|
|
245
|
+
align-items: center;
|
|
246
|
+
padding: 8px 0 8px 28px;
|
|
247
|
+
}
|
|
248
|
+
.key-override input {
|
|
249
|
+
flex: 1;
|
|
250
|
+
height: var(--form-element-height-medium, 32px);
|
|
251
|
+
box-sizing: border-box;
|
|
252
|
+
}
|
|
253
|
+
`
|
|
254
|
+
]; }
|
|
255
|
+
connectedCallback() {
|
|
256
|
+
super.connectedCallback();
|
|
257
|
+
this.load();
|
|
258
|
+
}
|
|
259
|
+
async load() {
|
|
260
|
+
this.loadFailed = undefined;
|
|
261
|
+
try {
|
|
262
|
+
const response = await client.query({ query: SETTINGS_QUERY, fetchPolicy: 'network-only' });
|
|
263
|
+
this.settings = answered(response, 'aiAgentSettings');
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
this.loadFailed = messageOf(error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
render() {
|
|
270
|
+
return html `
|
|
271
|
+
<setting-let>
|
|
272
|
+
<div slot="title">${t('title', 'AI agents')}</div>
|
|
273
|
+
<div slot="content">${this.renderContent()}</div>
|
|
274
|
+
</setting-let>
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
renderContent() {
|
|
278
|
+
if (this.loadFailed) {
|
|
279
|
+
return html `
|
|
280
|
+
<div class="head">
|
|
281
|
+
<span class="status off">${t('load-failed', 'Could not read the AI agents.')} ${this.loadFailed}</span>
|
|
282
|
+
<button type="button" @click=${() => this.load()}><md-icon>refresh</md-icon>${t('retry', 'Try again')}</button>
|
|
283
|
+
</div>
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
if (!this.settings)
|
|
287
|
+
return html `<span class="status">${t('loading', 'Reading…')}</span>`;
|
|
288
|
+
const groups = groupByOwner(this.settings);
|
|
289
|
+
return html `
|
|
290
|
+
<div class="head">
|
|
291
|
+
${this.renderStatus()}
|
|
292
|
+
<button type="button" ?disabled=${!!this.draft || this.saving} @click=${() => this.startNew()}>
|
|
293
|
+
<md-icon>add</md-icon>${t('add', 'Agent')}
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
${this.draft && !this.draft.id ? this.renderEditor() : nothing}
|
|
297
|
+
${groups.map(group => html `
|
|
298
|
+
${groups.length > 1 || group.inherited
|
|
299
|
+
? html `<div class="group-title">
|
|
300
|
+
${group.inherited ? t('inherited-from', 'Inherited from {path}', { path: group.path }) : t('this-domain', 'This domain')}
|
|
301
|
+
</div>`
|
|
302
|
+
: nothing}
|
|
303
|
+
${group.agents.map(agent => html `${this.renderRow(agent)} ${this.draft?.id === agent.id ? this.renderEditor() : nothing}
|
|
304
|
+
${this.keyFor === agent.id ? this.renderKeyOverride(agent) : nothing}`)}
|
|
305
|
+
`)}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
renderStatus() {
|
|
309
|
+
const settings = this.settings;
|
|
310
|
+
if (settings.state === 'configured') {
|
|
311
|
+
const usable = settings.agents.filter(one => one.usable).length;
|
|
312
|
+
const chosen = settings.agents.find(one => one.id === settings.defaultAgentId);
|
|
313
|
+
return html `<span class="status">
|
|
314
|
+
${t('status-on', 'AI is on here · {count} agents to pick · default {name}', { count: usable, name: chosen?.name ?? '' })}
|
|
315
|
+
</span>`;
|
|
316
|
+
}
|
|
317
|
+
const why = settings.reason === 'no-agent-key'
|
|
318
|
+
? t('status-no-key', 'No agent has an API key, so AI is off here.')
|
|
319
|
+
: settings.reason === 'no-usable-agent'
|
|
320
|
+
? t('status-all-off', 'Every agent is turned off, so AI is off here.')
|
|
321
|
+
: t('status-no-agent', 'There is no AI agent, so AI is off here.');
|
|
322
|
+
return html `<span class="status off">${why}</span>`;
|
|
323
|
+
}
|
|
324
|
+
renderRow(agent) {
|
|
325
|
+
const settings = this.settings;
|
|
326
|
+
const editable = !agent.inherited;
|
|
327
|
+
const muted = !agent.usable;
|
|
328
|
+
return html `
|
|
329
|
+
<div class="row ${muted ? 'muted' : ''}">
|
|
330
|
+
<input
|
|
331
|
+
type="radio"
|
|
332
|
+
name="default"
|
|
333
|
+
aria-label=${t('default', 'Default')}
|
|
334
|
+
title=${t('default-hint', 'The agent a person gets before picking one')}
|
|
335
|
+
.checked=${agent.id === settings.defaultAgentId}
|
|
336
|
+
?disabled=${!agent.usable || this.saving}
|
|
337
|
+
@change=${() => this.save({ kind: 'default', agentId: agent.id })} />
|
|
338
|
+
<span class="name" title=${agent.name}>${agent.name}</span>
|
|
339
|
+
<span>${providerLabel(agent.provider)}</span>
|
|
340
|
+
<span class="model" title=${agent.model}>${agent.model}</span>
|
|
341
|
+
${this.renderKey(agent)}
|
|
342
|
+
${editable
|
|
343
|
+
? html `<label class="use">
|
|
344
|
+
<input type="checkbox" .checked=${agent.active} ?disabled=${this.saving} @change=${(e) => this.onActive(agent, e.target.checked)} />
|
|
345
|
+
${t('use', 'Use')}
|
|
346
|
+
</label>`
|
|
347
|
+
: html `<label class="use">
|
|
348
|
+
<input type="checkbox" .checked=${!agent.turnedOff} ?disabled=${this.saving} @change=${(e) => this.save({ kind: 'turn-off', agentId: agent.id, off: !e.target.checked })} />
|
|
349
|
+
${t('use-here', 'Use here')}
|
|
350
|
+
</label>`}
|
|
351
|
+
${editable && agent.usedBelow.length
|
|
352
|
+
? html `<button type="button" class="below" title=${agent.usedBelow.map(one => one.name).join(', ')} @click=${() => this.showUsers(agent)}>
|
|
353
|
+
${t('used-below', '{count} below', { count: agent.usedBelow.length })}
|
|
354
|
+
</button>`
|
|
355
|
+
: html `<span></span>`}
|
|
356
|
+
${this.renderMenu(agent)}
|
|
357
|
+
</div>
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
renderKey(agent) {
|
|
361
|
+
switch (agent.keyState) {
|
|
362
|
+
case 'own':
|
|
363
|
+
return html `<span class="key">${agent.keyHint}</span>`;
|
|
364
|
+
case 'overridden':
|
|
365
|
+
return html `<span class="key">${t('key-this-domain', 'This domain\'s key {hint}', { hint: agent.keyHint ?? '' })}</span>`;
|
|
366
|
+
case 'inherited':
|
|
367
|
+
return html `<span class="key">${t('key-of', '{domain} key', { domain: agent.keyDomain?.name ?? '' })}</span>`;
|
|
368
|
+
default:
|
|
369
|
+
return html `<span class="key missing">${t('key-missing', 'No key')}</span>`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** The row's actions in the platform's popup menu - one at a time, closed by a press outside or Escape. */
|
|
373
|
+
renderMenu(agent) {
|
|
374
|
+
return html `<button
|
|
375
|
+
type="button"
|
|
376
|
+
class="more-menu"
|
|
377
|
+
aria-label=${t('more', 'More')}
|
|
378
|
+
title=${t('more', 'More')}
|
|
379
|
+
?disabled=${this.saving}
|
|
380
|
+
@click=${(e) => this.openMenu(e, agent)}>⋯</button>`;
|
|
381
|
+
}
|
|
382
|
+
openMenu(event, agent) {
|
|
383
|
+
const at = event.currentTarget.getBoundingClientRect();
|
|
384
|
+
OxPopupMenu.open({
|
|
385
|
+
template: agent.inherited
|
|
386
|
+
? html `
|
|
387
|
+
<ox-popup-menuitem
|
|
388
|
+
label=${agent.keyState === 'overridden' ? t('replace-own-key', "Replace this domain's key") : t('use-own-key', "Use this domain's key")}
|
|
389
|
+
@select=${() => this.startKeyOverride(agent)}></ox-popup-menuitem>
|
|
390
|
+
${agent.keyState === 'overridden'
|
|
391
|
+
? html `<ox-popup-menuitem
|
|
392
|
+
label=${t('back-to-key', 'Back to the {domain} key', { domain: agent.owner.name })}
|
|
393
|
+
@select=${() => this.clearKeyOverride(agent)}></ox-popup-menuitem>`
|
|
394
|
+
: nothing}
|
|
395
|
+
`
|
|
396
|
+
: html `
|
|
397
|
+
${this.draft ? nothing : html `<ox-popup-menuitem label=${t('edit', 'Edit')} @select=${() => this.startEdit(agent)}></ox-popup-menuitem>`}
|
|
398
|
+
<ox-popup-menuitem label=${t('delete', 'Delete')} @select=${() => this.deleteAgent(agent)}></ox-popup-menuitem>
|
|
399
|
+
`,
|
|
400
|
+
top: at.bottom,
|
|
401
|
+
right: window.innerWidth - at.right,
|
|
402
|
+
parent: document.body
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
renderKeyOverride(agent) {
|
|
406
|
+
return html `
|
|
407
|
+
<div class="key-override">
|
|
408
|
+
<input
|
|
409
|
+
type="password"
|
|
410
|
+
autocomplete="new-password"
|
|
411
|
+
placeholder=${t('key-placeholder', 'API key of this domain')}
|
|
412
|
+
.value=${this.keyDraft}
|
|
413
|
+
@input=${(e) => (this.keyDraft = e.target.value)} />
|
|
414
|
+
<button type="button" @click=${() => ((this.keyFor = undefined), (this.keyDraft = ''))}>${t('cancel', 'Cancel')}</button>
|
|
415
|
+
<button type="button" class="primary" ?disabled=${!this.keyDraft.trim() || this.saving} @click=${() => this.save({ kind: 'key-override', agentId: agent.id, apiKey: this.keyDraft.trim() })}>
|
|
416
|
+
${t('save', 'Save')}
|
|
417
|
+
</button>
|
|
418
|
+
</div>
|
|
419
|
+
${this.saveError ? html `<div class="error">${this.saveError}</div>` : nothing}
|
|
420
|
+
`;
|
|
421
|
+
}
|
|
422
|
+
renderEditor() {
|
|
423
|
+
const draft = this.draft;
|
|
424
|
+
const stored = this.settings.agents.find(one => one.id === draft.id);
|
|
425
|
+
const hasStoredKey = !!stored && stored.keyState === 'own' && draft.provider === stored.provider;
|
|
426
|
+
const modelLocked = !draft.apiKey.trim() && !hasStoredKey;
|
|
427
|
+
const offered = this.models.state === 'ready' ? this.models.models : [];
|
|
428
|
+
const modelGone = this.models.state === 'ready' && !!draft.model && !offered.includes(draft.model);
|
|
429
|
+
const canSave = !!draft.name.trim() && !!draft.model && !modelLocked && !modelGone && this.models.state === 'ready' && !this.saving;
|
|
430
|
+
return html `
|
|
431
|
+
<form class="editor" @submit=${(e) => (e.preventDefault(), canSave && this.save({ kind: 'upsert', draft }))}>
|
|
432
|
+
<label>
|
|
433
|
+
${t('name', 'Name')}
|
|
434
|
+
<input .value=${draft.name} @input=${(e) => this.patch({ name: e.target.value })} />
|
|
435
|
+
</label>
|
|
436
|
+
<label>
|
|
437
|
+
${t('provider', 'Provider')}
|
|
438
|
+
<select @change=${(e) => this.onProvider(e.target.value)}>
|
|
439
|
+
${PROVIDERS.map(one => html `<option value=${one} ?selected=${one === draft.provider}>${providerLabel(one)}</option>`)}
|
|
440
|
+
</select>
|
|
441
|
+
</label>
|
|
442
|
+
<label>
|
|
443
|
+
${t('api-key', 'API key')}
|
|
444
|
+
<input
|
|
445
|
+
type="password"
|
|
446
|
+
autocomplete="new-password"
|
|
447
|
+
placeholder=${hasStoredKey ? t('key-stored', 'Stored key {hint} - enter only to change it', { hint: stored?.keyHint ?? '' }) : ''}
|
|
448
|
+
.value=${draft.apiKey}
|
|
449
|
+
@input=${(e) => this.patch({ apiKey: e.target.value })}
|
|
450
|
+
@change=${() => this.loadModels()} />
|
|
451
|
+
</label>
|
|
452
|
+
<label>
|
|
453
|
+
${t('model', 'Model')}
|
|
454
|
+
<span class="with-action">
|
|
455
|
+
<select ?disabled=${modelLocked || this.models.state !== 'ready'} @change=${(e) => this.patch({ model: e.target.value })}>
|
|
456
|
+
${!draft.model || modelGone ? html `<option value="" selected disabled>${modelGone ? draft.model : t('pick-model', 'Pick a model')}</option>` : nothing}
|
|
457
|
+
${offered.map(one => html `<option value=${one} ?selected=${one === draft.model}>${one}</option>`)}
|
|
458
|
+
</select>
|
|
459
|
+
<button type="button" aria-label=${t('reload-models', 'Read the models again')} title=${t('reload-models', 'Read the models again')} ?disabled=${modelLocked || this.models.state === 'loading'} @click=${() => this.loadModels()}>
|
|
460
|
+
<md-icon>refresh</md-icon>
|
|
461
|
+
</button>
|
|
462
|
+
</span>
|
|
463
|
+
${modelLocked
|
|
464
|
+
? html `<span class="hint">${t('enter-key-first', 'Enter the API key to pick a model.')}</span>`
|
|
465
|
+
: this.models.state === 'loading'
|
|
466
|
+
? html `<span class="hint">${t('reading-models', 'Reading the provider\'s models…')}</span>`
|
|
467
|
+
: this.models.state === 'failed'
|
|
468
|
+
? html `<span class="error">${this.models.reason}</span>`
|
|
469
|
+
: modelGone
|
|
470
|
+
? html `<span class="error">${t('model-gone', 'The provider no longer offers this model. Pick another.')}</span>`
|
|
471
|
+
: nothing}
|
|
472
|
+
</label>
|
|
473
|
+
<details class="advanced" ?open=${!!(draft.baseURL || draft.organization)}>
|
|
474
|
+
<summary>${t('advanced', 'Advanced')}</summary>
|
|
475
|
+
<div>
|
|
476
|
+
<label>
|
|
477
|
+
${t('base-url', 'Base URL')}
|
|
478
|
+
<input .value=${draft.baseURL} placeholder="https://" @input=${(e) => this.patch({ baseURL: e.target.value })} @change=${() => this.loadModels()} />
|
|
479
|
+
</label>
|
|
480
|
+
${draft.provider === 'openai'
|
|
481
|
+
? html `<label>
|
|
482
|
+
${t('organization', 'Organization')}
|
|
483
|
+
<input .value=${draft.organization} @input=${(e) => this.patch({ organization: e.target.value })} @change=${() => this.loadModels()} />
|
|
484
|
+
</label>`
|
|
485
|
+
: nothing}
|
|
486
|
+
</div>
|
|
487
|
+
</details>
|
|
488
|
+
${this.saveError ? html `<div class="error" style="grid-column: 1 / -1">${this.saveError}</div>` : nothing}
|
|
489
|
+
<div class="actions">
|
|
490
|
+
<button type="button" @click=${() => this.cancelEdit()}>${t('cancel', 'Cancel')}</button>
|
|
491
|
+
<button type="submit" class="primary" ?disabled=${!canSave}>${t('save', 'Save')}</button>
|
|
492
|
+
</div>
|
|
493
|
+
</form>
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
startNew() {
|
|
497
|
+
this.saveError = undefined;
|
|
498
|
+
this.draft = draftOf();
|
|
499
|
+
this.models = { state: 'idle', models: [] };
|
|
500
|
+
}
|
|
501
|
+
startEdit(agent) {
|
|
502
|
+
this.saveError = undefined;
|
|
503
|
+
this.draft = draftOf(agent);
|
|
504
|
+
this.loadModels();
|
|
505
|
+
}
|
|
506
|
+
cancelEdit() {
|
|
507
|
+
this.draft = undefined;
|
|
508
|
+
this.saveError = undefined;
|
|
509
|
+
this.models = { state: 'idle', models: [] };
|
|
510
|
+
}
|
|
511
|
+
patch(change) {
|
|
512
|
+
this.draft = { ...this.draft, ...change };
|
|
513
|
+
}
|
|
514
|
+
/** A provider decides the model list, so changing it empties the model. */
|
|
515
|
+
onProvider(provider) {
|
|
516
|
+
this.patch({ provider, model: '', organization: provider === 'openai' ? this.draft.organization : '' });
|
|
517
|
+
this.models = { state: 'idle', models: [] };
|
|
518
|
+
this.loadModels();
|
|
519
|
+
}
|
|
520
|
+
async loadModels() {
|
|
521
|
+
const draft = this.draft;
|
|
522
|
+
if (!draft)
|
|
523
|
+
return;
|
|
524
|
+
const stored = this.settings?.agents.find(one => one.id === draft.id && !one.inherited);
|
|
525
|
+
const useStored = !draft.apiKey.trim() && stored?.keyState === 'own' && stored.provider === draft.provider;
|
|
526
|
+
if (!draft.apiKey.trim() && !useStored) {
|
|
527
|
+
this.models = { state: 'idle', models: [] };
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const token = ++this.modelsToken;
|
|
531
|
+
this.models = { state: 'loading', models: [] };
|
|
532
|
+
try {
|
|
533
|
+
const response = await client.query({
|
|
534
|
+
query: MODELS_QUERY,
|
|
535
|
+
fetchPolicy: 'network-only',
|
|
536
|
+
variables: {
|
|
537
|
+
provider: draft.provider,
|
|
538
|
+
...(draft.apiKey.trim() ? { apiKey: draft.apiKey.trim() } : { agentId: draft.id }),
|
|
539
|
+
...(draft.baseURL.trim() && { baseURL: draft.baseURL.trim() }),
|
|
540
|
+
...(draft.organization.trim() && { organization: draft.organization.trim() })
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
if (token !== this.modelsToken)
|
|
544
|
+
return;
|
|
545
|
+
this.models = { state: 'ready', models: answered(response, 'aiProviderModels') ?? [] };
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (token !== this.modelsToken)
|
|
549
|
+
return;
|
|
550
|
+
this.models = { state: 'failed', models: [], reason: messageOf(error) };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
startKeyOverride(agent) {
|
|
554
|
+
this.saveError = undefined;
|
|
555
|
+
this.keyDraft = '';
|
|
556
|
+
this.keyFor = agent.id;
|
|
557
|
+
}
|
|
558
|
+
async clearKeyOverride(agent) {
|
|
559
|
+
const accepted = await confirm(t('back-to-key-title', 'Go back to the {domain} key?', { domain: agent.owner.name }), t('back-to-key-text', 'This domain\'s key for "{name}" is deleted, and the key of {domain} answers again.', {
|
|
560
|
+
name: agent.name,
|
|
561
|
+
domain: agent.owner.name
|
|
562
|
+
}));
|
|
563
|
+
if (accepted)
|
|
564
|
+
await this.save({ kind: 'key-clear', agentId: agent.id });
|
|
565
|
+
}
|
|
566
|
+
/** Turning an agent off reaches every domain below that uses it - they are named before it happens. */
|
|
567
|
+
async onActive(agent, active) {
|
|
568
|
+
if (!active && agent.usedBelow.length) {
|
|
569
|
+
const accepted = await confirm(t('turn-off-title', 'Turn "{name}" off?', { name: agent.name }), t('turn-off-text', 'It stops answering here and in {domains}.', { domains: agent.usedBelow.map(one => one.name).join(', ') }));
|
|
570
|
+
if (!accepted)
|
|
571
|
+
return this.requestUpdate();
|
|
572
|
+
}
|
|
573
|
+
await this.save({ kind: 'active', agentId: agent.id, active });
|
|
574
|
+
}
|
|
575
|
+
async deleteAgent(agent) {
|
|
576
|
+
const below = agent.usedBelow.length
|
|
577
|
+
? ' ' + t('delete-below', 'It also stops answering in {domains}.', { domains: agent.usedBelow.map(one => one.name).join(', ') })
|
|
578
|
+
: '';
|
|
579
|
+
const accepted = await confirm(t('delete-title', 'Delete "{name}"?', { name: agent.name }), t('delete-text', 'Its API key is deleted with it. Usage already recorded stays.') + below);
|
|
580
|
+
if (accepted)
|
|
581
|
+
await this.save({ kind: 'delete', agentId: agent.id });
|
|
582
|
+
}
|
|
583
|
+
async showUsers(agent) {
|
|
584
|
+
await OxPrompt.open({
|
|
585
|
+
type: 'info',
|
|
586
|
+
title: t('used-below-title', 'Domains below using "{name}"', { name: agent.name }),
|
|
587
|
+
text: agent.usedBelow.map(one => one.name).join(', '),
|
|
588
|
+
confirmButton: { text: t('close', 'Close') }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
async save(change) {
|
|
592
|
+
this.saving = true;
|
|
593
|
+
this.saveError = undefined;
|
|
594
|
+
try {
|
|
595
|
+
answered(await client.mutate({ mutation: SAVE_MUTATION, variables: { input: saveInput(this.settings, change) } }), 'saveAiAgents');
|
|
596
|
+
if (change.kind === 'upsert')
|
|
597
|
+
this.cancelEdit();
|
|
598
|
+
if (change.kind === 'key-override')
|
|
599
|
+
((this.keyFor = undefined), (this.keyDraft = ''));
|
|
600
|
+
await this.load();
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
this.saveError = messageOf(error);
|
|
604
|
+
if (change.kind !== 'upsert' && change.kind !== 'key-override') {
|
|
605
|
+
await OxPrompt.open({ type: 'error', title: t('save-failed', 'Not saved'), text: this.saveError, confirmButton: { text: t('close', 'Close') } });
|
|
606
|
+
await this.load();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
this.saving = false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
__decorate([
|
|
615
|
+
state(),
|
|
616
|
+
__metadata("design:type", Object)
|
|
617
|
+
], AiAgentSettingLet.prototype, "settings", void 0);
|
|
618
|
+
__decorate([
|
|
619
|
+
state(),
|
|
620
|
+
__metadata("design:type", String)
|
|
621
|
+
], AiAgentSettingLet.prototype, "loadFailed", void 0);
|
|
622
|
+
__decorate([
|
|
623
|
+
state(),
|
|
624
|
+
__metadata("design:type", Object)
|
|
625
|
+
], AiAgentSettingLet.prototype, "draft", void 0);
|
|
626
|
+
__decorate([
|
|
627
|
+
state(),
|
|
628
|
+
__metadata("design:type", Object)
|
|
629
|
+
], AiAgentSettingLet.prototype, "models", void 0);
|
|
630
|
+
__decorate([
|
|
631
|
+
state(),
|
|
632
|
+
__metadata("design:type", Object)
|
|
633
|
+
], AiAgentSettingLet.prototype, "saving", void 0);
|
|
634
|
+
__decorate([
|
|
635
|
+
state(),
|
|
636
|
+
__metadata("design:type", String)
|
|
637
|
+
], AiAgentSettingLet.prototype, "saveError", void 0);
|
|
638
|
+
__decorate([
|
|
639
|
+
state(),
|
|
640
|
+
__metadata("design:type", String)
|
|
641
|
+
], AiAgentSettingLet.prototype, "keyFor", void 0);
|
|
642
|
+
__decorate([
|
|
643
|
+
state(),
|
|
644
|
+
__metadata("design:type", Object)
|
|
645
|
+
], AiAgentSettingLet.prototype, "keyDraft", void 0);
|
|
646
|
+
AiAgentSettingLet = __decorate([
|
|
647
|
+
customElement('ai-agent-setting-let')
|
|
648
|
+
], AiAgentSettingLet);
|
|
649
|
+
export { AiAgentSettingLet };
|
|
650
|
+
/**
|
|
651
|
+
* The data of a query or mutation. This client answers a refusal in `errors` (errorPolicy 'all') instead of
|
|
652
|
+
* throwing, so it is turned into a throw here - the server's sentence, already in the person's language.
|
|
653
|
+
*/
|
|
654
|
+
function answered(result, field) {
|
|
655
|
+
if (result?.errors?.length)
|
|
656
|
+
throw new Error(result.errors[0].message);
|
|
657
|
+
return result?.data?.[field];
|
|
658
|
+
}
|
|
659
|
+
async function confirm(title, text) {
|
|
660
|
+
return !!(await OxPrompt.open({
|
|
661
|
+
type: 'warning',
|
|
662
|
+
title,
|
|
663
|
+
text,
|
|
664
|
+
confirmButton: { text: t('confirm', 'Confirm') },
|
|
665
|
+
cancelButton: { text: t('cancel', 'Cancel') }
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
/** The server's sentence for a refusal - already in the person's language. */
|
|
669
|
+
function messageOf(error) {
|
|
670
|
+
return error?.graphQLErrors?.[0]?.message ?? error?.message ?? String(error);
|
|
671
|
+
}
|
|
672
|
+
//# sourceMappingURL=ai-agent-setting-let.js.map
|