@kispace-io/extension-ai-system 0.8.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.
- package/package.json +20 -0
- package/src/agents/agent-registry.ts +65 -0
- package/src/agents/index.ts +4 -0
- package/src/agents/message-processor.ts +50 -0
- package/src/agents/prompt-builder.ts +167 -0
- package/src/ai-system-extension.ts +104 -0
- package/src/aisystem.json +154 -0
- package/src/chat-provider-contributions.ts +95 -0
- package/src/core/constants.ts +23 -0
- package/src/core/index.ts +6 -0
- package/src/core/interfaces.ts +137 -0
- package/src/core/types.ts +126 -0
- package/src/general-assistant-prompt.txt +14 -0
- package/src/i18n.json +11 -0
- package/src/index.ts +13 -0
- package/src/prompt-enhancer-contributions.ts +29 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/ollama-provider.ts +13 -0
- package/src/providers/openai-provider.ts +12 -0
- package/src/providers/provider-factory.ts +36 -0
- package/src/providers/provider.ts +156 -0
- package/src/providers/streaming/ollama-parser.ts +114 -0
- package/src/providers/streaming/sse-parser.ts +152 -0
- package/src/providers/streaming/stream-parser.ts +16 -0
- package/src/register.ts +16 -0
- package/src/service/ai-service.ts +744 -0
- package/src/service/token-usage-tracker.ts +139 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/tool-call-accumulator.ts +81 -0
- package/src/tools/tool-executor.ts +174 -0
- package/src/tools/tool-registry.ts +70 -0
- package/src/translation.ts +3 -0
- package/src/utils/token-estimator.ts +87 -0
- package/src/utils/tool-detector.ts +144 -0
- package/src/view/agent-group-manager.ts +146 -0
- package/src/view/components/ai-agent-response-card.ts +198 -0
- package/src/view/components/ai-agent-response-group.ts +220 -0
- package/src/view/components/ai-chat-input.ts +131 -0
- package/src/view/components/ai-chat-message.ts +615 -0
- package/src/view/components/ai-empty-state.ts +52 -0
- package/src/view/components/ai-loading-indicator.ts +91 -0
- package/src/view/components/index.ts +7 -0
- package/src/view/components/k-ai-config-editor.ts +828 -0
- package/src/view/index.ts +6 -0
- package/src/view/k-aiview.ts +901 -0
- package/src/view/k-token-usage.ts +220 -0
- package/src/view/provider-manager.ts +196 -0
- package/src/view/session-manager.ts +255 -0
- package/src/view/stream-manager.ts +123 -0
- package/src/workflows/conditional-workflow.ts +98 -0
- package/src/workflows/index.ts +6 -0
- package/src/workflows/parallel-workflow.ts +45 -0
- package/src/workflows/sequential-workflow.ts +95 -0
- package/src/workflows/workflow-engine.ts +63 -0
- package/src/workflows/workflow-strategy.ts +21 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
import { css, html, TemplateResult } from 'lit';
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
3
|
+
import { repeat } from 'lit/directives/repeat.js';
|
|
4
|
+
import { when } from 'lit/directives/when.js';
|
|
5
|
+
import { KPart } from '@kispace-io/core';
|
|
6
|
+
import { EditorInput } from '@kispace-io/core';
|
|
7
|
+
import { appSettings, TOPIC_SETTINGS_CHANGED } from '@kispace-io/core';
|
|
8
|
+
import { KEY_AI_CONFIG, TOPIC_AICONFIG_CHANGED, CID_CHAT_PROVIDERS } from '../../core/constants';
|
|
9
|
+
import { subscribe } from '@kispace-io/core';
|
|
10
|
+
import { confirmDialog } from '@kispace-io/core';
|
|
11
|
+
import { contributionRegistry, commandRegistry } from '@kispace-io/core';
|
|
12
|
+
import { ProviderFactory } from '../../providers/provider-factory';
|
|
13
|
+
import type { AIConfig, ChatProvider } from '../../core/types';
|
|
14
|
+
import type { ChatProviderContribution } from '../../core/interfaces';
|
|
15
|
+
import { t } from '../../translation';
|
|
16
|
+
|
|
17
|
+
@customElement('k-ai-config-editor')
|
|
18
|
+
export class KAIConfigEditor extends KPart {
|
|
19
|
+
@property({ attribute: false })
|
|
20
|
+
public input?: EditorInput;
|
|
21
|
+
|
|
22
|
+
@state()
|
|
23
|
+
private aiConfig?: AIConfig;
|
|
24
|
+
|
|
25
|
+
@state()
|
|
26
|
+
private providers: ChatProvider[] = [];
|
|
27
|
+
|
|
28
|
+
@state()
|
|
29
|
+
private defaultProvider: string = '';
|
|
30
|
+
|
|
31
|
+
@state()
|
|
32
|
+
private editingCell: { rowIndex: number; field: string } | null = null;
|
|
33
|
+
|
|
34
|
+
@state()
|
|
35
|
+
private editingValue: string = '';
|
|
36
|
+
|
|
37
|
+
@state()
|
|
38
|
+
private hasChanges: boolean = false;
|
|
39
|
+
|
|
40
|
+
@state()
|
|
41
|
+
private availableModels: Array<{ id: string; name?: string }> = [];
|
|
42
|
+
|
|
43
|
+
@state()
|
|
44
|
+
private loadingModels: boolean = false;
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@state()
|
|
48
|
+
private requireToolApproval: boolean = true;
|
|
49
|
+
|
|
50
|
+
@state()
|
|
51
|
+
private toolApprovalAllowlist: string[] = [];
|
|
52
|
+
|
|
53
|
+
@state()
|
|
54
|
+
private smartToolDetection: boolean = false;
|
|
55
|
+
|
|
56
|
+
@state()
|
|
57
|
+
private availableCommands: Array<{ id: string; name: string; description?: string }> = [];
|
|
58
|
+
|
|
59
|
+
private providerFactory: ProviderFactory = new ProviderFactory();
|
|
60
|
+
|
|
61
|
+
protected async doInitUI() {
|
|
62
|
+
await this.loadAvailableCommands();
|
|
63
|
+
await this.loadConfig();
|
|
64
|
+
subscribe(TOPIC_AICONFIG_CHANGED, () => {
|
|
65
|
+
this.loadConfig();
|
|
66
|
+
});
|
|
67
|
+
subscribe(TOPIC_SETTINGS_CHANGED, () => {
|
|
68
|
+
this.loadConfig();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async loadConfig() {
|
|
73
|
+
const config = await appSettings.get(KEY_AI_CONFIG) as AIConfig | undefined;
|
|
74
|
+
this.aiConfig = config;
|
|
75
|
+
|
|
76
|
+
// Merge contributed providers with config providers
|
|
77
|
+
const contributedProviders = this.getContributedProviders();
|
|
78
|
+
const configProviders = config?.providers || [];
|
|
79
|
+
this.providers = this.mergeProviders(configProviders, contributedProviders);
|
|
80
|
+
|
|
81
|
+
const savedDefaultProvider = config?.defaultProvider || '';
|
|
82
|
+
// Validate that the default provider still exists in the providers list
|
|
83
|
+
if (savedDefaultProvider && !this.providers.find(p => p.name === savedDefaultProvider)) {
|
|
84
|
+
this.defaultProvider = '';
|
|
85
|
+
} else {
|
|
86
|
+
this.defaultProvider = savedDefaultProvider;
|
|
87
|
+
}
|
|
88
|
+
// Load requireToolApproval from config, default to true if missing
|
|
89
|
+
if (config?.requireToolApproval !== undefined) {
|
|
90
|
+
this.requireToolApproval = config.requireToolApproval;
|
|
91
|
+
} else {
|
|
92
|
+
this.requireToolApproval = true; // Default to true if missing
|
|
93
|
+
}
|
|
94
|
+
this.toolApprovalAllowlist = config?.toolApprovalAllowlist || [];
|
|
95
|
+
// Load smartToolDetection from config, default to false if missing
|
|
96
|
+
if (config?.smartToolDetection !== undefined) {
|
|
97
|
+
this.smartToolDetection = config.smartToolDetection;
|
|
98
|
+
} else {
|
|
99
|
+
this.smartToolDetection = false; // Default to false if missing
|
|
100
|
+
}
|
|
101
|
+
this.hasChanges = false;
|
|
102
|
+
this.markDirty(false);
|
|
103
|
+
this.editingCell = null;
|
|
104
|
+
|
|
105
|
+
// Ensure checkboxes are properly synced after render
|
|
106
|
+
await this.updateComplete;
|
|
107
|
+
this.syncCheckboxStates();
|
|
108
|
+
this.syncToolApprovalCheckbox();
|
|
109
|
+
this.syncSmartToolDetectionCheckbox();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async loadAvailableCommands() {
|
|
113
|
+
const allCommands = commandRegistry.listCommands();
|
|
114
|
+
this.availableCommands = Object.entries(allCommands).map(([id, cmd]: [string, any]) => ({
|
|
115
|
+
id,
|
|
116
|
+
name: cmd.name || id,
|
|
117
|
+
description: cmd.description
|
|
118
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private getContributedProviders(): ChatProvider[] {
|
|
122
|
+
const contributions = contributionRegistry.getContributions(CID_CHAT_PROVIDERS) as ChatProviderContribution[];
|
|
123
|
+
return contributions.map(contrib => contrib.provider);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private mergeProviders(existing: ChatProvider[], contributed: ChatProvider[]): ChatProvider[] {
|
|
127
|
+
const existingNames = new Set(existing.map(p => p.name));
|
|
128
|
+
const missing = contributed.filter(provider => !existingNames.has(provider.name));
|
|
129
|
+
return missing.length > 0 ? [...existing, ...missing] : existing;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
private syncCheckboxStates() {
|
|
134
|
+
const rows = this.shadowRoot?.querySelectorAll('tbody tr') as NodeListOf<HTMLElement>;
|
|
135
|
+
if (rows && this.providers) {
|
|
136
|
+
rows.forEach((row, index) => {
|
|
137
|
+
const checkbox = row.querySelector('td:first-child wa-checkbox') as any;
|
|
138
|
+
const provider = this.providers[index];
|
|
139
|
+
if (checkbox && provider) {
|
|
140
|
+
checkbox.checked = this.defaultProvider === provider.name;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private syncToolApprovalCheckbox() {
|
|
147
|
+
const checkbox = this.shadowRoot?.querySelector('.tool-approval-controls wa-checkbox') as any;
|
|
148
|
+
if (checkbox) {
|
|
149
|
+
checkbox.checked = this.requireToolApproval;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private syncSmartToolDetectionCheckbox() {
|
|
154
|
+
const checkbox = this.shadowRoot?.querySelector('.tool-detection-section wa-checkbox') as any;
|
|
155
|
+
if (checkbox) {
|
|
156
|
+
checkbox.checked = this.smartToolDetection;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async saveConfig() {
|
|
161
|
+
if (!this.aiConfig) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const updatedConfig: AIConfig = {
|
|
166
|
+
...this.aiConfig,
|
|
167
|
+
defaultProvider: this.defaultProvider,
|
|
168
|
+
providers: this.providers,
|
|
169
|
+
requireToolApproval: this.requireToolApproval,
|
|
170
|
+
toolApprovalAllowlist: this.toolApprovalAllowlist,
|
|
171
|
+
smartToolDetection: this.smartToolDetection
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
await appSettings.set(KEY_AI_CONFIG, updatedConfig);
|
|
175
|
+
this.hasChanges = false;
|
|
176
|
+
this.markDirty(false);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async save() {
|
|
180
|
+
if (!this.hasChanges) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await this.saveConfig();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('Failed to save AI config:', error);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async startCellEditing(rowIndex: number, field: string) {
|
|
192
|
+
const provider = this.providers[rowIndex];
|
|
193
|
+
if (!provider) return;
|
|
194
|
+
|
|
195
|
+
const value = this.getProviderFieldValue(provider, field);
|
|
196
|
+
|
|
197
|
+
this.editingCell = { rowIndex, field };
|
|
198
|
+
this.editingValue = value;
|
|
199
|
+
|
|
200
|
+
// Special handling for model field - fetch available models
|
|
201
|
+
// Do this after setting editing state so the spinner can render
|
|
202
|
+
if (field === 'model') {
|
|
203
|
+
await this.updateComplete; // Wait for editing state to render
|
|
204
|
+
await this.fetchModels(provider);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async fetchModels(provider: ChatProvider): Promise<void> {
|
|
209
|
+
this.loadingModels = true;
|
|
210
|
+
this.availableModels = []; // Clear previous models
|
|
211
|
+
await this.updateComplete; // Wait for the update to complete so spinner can render
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
// Get the provider instance - it handles model listing itself
|
|
215
|
+
const providerInstance = this.providerFactory.getProvider(provider);
|
|
216
|
+
|
|
217
|
+
if (providerInstance.getAvailableModels) {
|
|
218
|
+
const models = await providerInstance.getAvailableModels(provider);
|
|
219
|
+
this.availableModels = Array.isArray(models) ? models : [];
|
|
220
|
+
} else {
|
|
221
|
+
// Provider doesn't support model listing
|
|
222
|
+
this.availableModels = [];
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.availableModels = [];
|
|
226
|
+
} finally {
|
|
227
|
+
this.loadingModels = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private cancelCellEditing() {
|
|
232
|
+
this.editingCell = null;
|
|
233
|
+
this.editingValue = '';
|
|
234
|
+
this.availableModels = [];
|
|
235
|
+
this.loadingModels = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private saveCellEditing() {
|
|
239
|
+
if (!this.editingCell) return;
|
|
240
|
+
|
|
241
|
+
const { rowIndex, field } = this.editingCell;
|
|
242
|
+
this.updateProviderField(rowIndex, field as keyof ChatProvider, this.editingValue);
|
|
243
|
+
this.cancelCellEditing();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private markDirtyAndUpdate() {
|
|
247
|
+
this.hasChanges = true;
|
|
248
|
+
this.markDirty(true);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private getProviderFieldValue(provider: ChatProvider, field: string): string {
|
|
252
|
+
const fieldMap: Record<string, keyof ChatProvider> = {
|
|
253
|
+
'name': 'name',
|
|
254
|
+
'model': 'model',
|
|
255
|
+
'chatApiEndpoint': 'chatApiEndpoint',
|
|
256
|
+
'apiKey': 'apiKey',
|
|
257
|
+
'ocrApiEndpoint': 'ocrApiEndpoint',
|
|
258
|
+
'ocrModel': 'ocrModel'
|
|
259
|
+
};
|
|
260
|
+
const key = fieldMap[field];
|
|
261
|
+
if (!key) return '';
|
|
262
|
+
const value = provider[key];
|
|
263
|
+
return typeof value === 'string' ? value : '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private updateProviderField(index: number, field: keyof ChatProvider, value: any) {
|
|
267
|
+
this.providers = this.providers.map((p, i) => {
|
|
268
|
+
if (i === index) {
|
|
269
|
+
const updated = { ...p };
|
|
270
|
+
if (field === 'ocrApiEndpoint' || field === 'ocrModel') {
|
|
271
|
+
updated[field] = value || undefined;
|
|
272
|
+
} else {
|
|
273
|
+
(updated as any)[field] = value;
|
|
274
|
+
}
|
|
275
|
+
return updated;
|
|
276
|
+
}
|
|
277
|
+
return p;
|
|
278
|
+
});
|
|
279
|
+
this.markDirtyAndUpdate();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private updateProviderParameter(index: number, paramKey: string, value: any) {
|
|
283
|
+
this.providers = this.providers.map((p, i) => {
|
|
284
|
+
if (i === index) {
|
|
285
|
+
const parameters = { ...(p.parameters || {}), [paramKey]: value };
|
|
286
|
+
return { ...p, parameters };
|
|
287
|
+
}
|
|
288
|
+
return p;
|
|
289
|
+
});
|
|
290
|
+
this.markDirtyAndUpdate();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async deleteProvider(index: number) {
|
|
294
|
+
const providerToDelete = this.providers[index];
|
|
295
|
+
const confirmed = await confirmDialog(t('DELETE_PROVIDER_CONFIRM', { name: providerToDelete.name }));
|
|
296
|
+
if (confirmed) {
|
|
297
|
+
// Clear default provider if it's the one being deleted
|
|
298
|
+
if (this.defaultProvider === providerToDelete.name) {
|
|
299
|
+
this.defaultProvider = '';
|
|
300
|
+
}
|
|
301
|
+
this.providers = this.providers.filter((_, i) => i !== index);
|
|
302
|
+
this.markDirtyAndUpdate();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private addProvider() {
|
|
307
|
+
const newProvider: ChatProvider = {
|
|
308
|
+
name: 'new-provider',
|
|
309
|
+
model: '',
|
|
310
|
+
apiKey: '',
|
|
311
|
+
chatApiEndpoint: ''
|
|
312
|
+
};
|
|
313
|
+
this.providers = [...this.providers, newProvider];
|
|
314
|
+
this.markDirtyAndUpdate();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private setDefaultProvider(providerName: string) {
|
|
318
|
+
// Only set if it's different from current
|
|
319
|
+
if (this.defaultProvider !== providerName) {
|
|
320
|
+
this.defaultProvider = providerName;
|
|
321
|
+
this.markDirtyAndUpdate();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private createInputHandlers() {
|
|
326
|
+
return {
|
|
327
|
+
onInput: (e: Event) => {
|
|
328
|
+
const input = e.target as HTMLInputElement;
|
|
329
|
+
this.editingValue = input.value;
|
|
330
|
+
},
|
|
331
|
+
onKeydown: (e: KeyboardEvent) => {
|
|
332
|
+
if (e.key === 'Enter') {
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
this.saveCellEditing();
|
|
335
|
+
} else if (e.key === 'Escape') {
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
this.cancelCellEditing();
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
onBlur: () => {
|
|
341
|
+
this.saveCellEditing();
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private renderEditableCell(
|
|
347
|
+
index: number,
|
|
348
|
+
field: string,
|
|
349
|
+
displayValue: string | TemplateResult,
|
|
350
|
+
inputType: 'text' | 'password' = 'text',
|
|
351
|
+
placeholder?: string,
|
|
352
|
+
customInput?: TemplateResult
|
|
353
|
+
): TemplateResult {
|
|
354
|
+
const isEditing = this.editingCell?.rowIndex === index && this.editingCell?.field === field;
|
|
355
|
+
const handlers = this.createInputHandlers();
|
|
356
|
+
|
|
357
|
+
if (isEditing && customInput) {
|
|
358
|
+
return customInput;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (isEditing) {
|
|
362
|
+
return html`
|
|
363
|
+
<wa-input
|
|
364
|
+
type="${inputType}"
|
|
365
|
+
.value="${this.editingValue}"
|
|
366
|
+
placeholder="${placeholder || ''}"
|
|
367
|
+
@input="${handlers.onInput}"
|
|
368
|
+
@keydown="${handlers.onKeydown}"
|
|
369
|
+
@blur="${handlers.onBlur}"
|
|
370
|
+
autofocus>
|
|
371
|
+
</wa-input>
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return html`<span>${displayValue}</span>`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private renderModelCell(index: number, provider: ChatProvider): TemplateResult {
|
|
379
|
+
const isEditing = this.editingCell?.rowIndex === index && this.editingCell?.field === 'model';
|
|
380
|
+
|
|
381
|
+
if (!isEditing) {
|
|
382
|
+
return html`<span>${provider.model}</span>`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return html`
|
|
386
|
+
${when(this.loadingModels, () => html`
|
|
387
|
+
<wa-input
|
|
388
|
+
.value="${this.editingValue}"
|
|
389
|
+
placeholder="${t('LOADING_MODELS')}"
|
|
390
|
+
readonly>
|
|
391
|
+
<wa-animation name="spinner" play slot="start"></wa-animation>
|
|
392
|
+
</wa-input>
|
|
393
|
+
`, () => html`
|
|
394
|
+
${when(this.availableModels.length > 0, () => html`
|
|
395
|
+
<wa-dropdown
|
|
396
|
+
@wa-select="${(e: CustomEvent) => {
|
|
397
|
+
const selectedValue = e.detail.item.value;
|
|
398
|
+
if (selectedValue) {
|
|
399
|
+
this.editingValue = selectedValue;
|
|
400
|
+
this.saveCellEditing();
|
|
401
|
+
}
|
|
402
|
+
}}"
|
|
403
|
+
placement="bottom-start">
|
|
404
|
+
<wa-input
|
|
405
|
+
slot="trigger"
|
|
406
|
+
.value="${this.editingValue}"
|
|
407
|
+
placeholder="${t('SELECT_MODEL')}"
|
|
408
|
+
readonly
|
|
409
|
+
@keydown="${(e: KeyboardEvent) => {
|
|
410
|
+
if (e.key === 'Escape') {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
this.cancelCellEditing();
|
|
413
|
+
}
|
|
414
|
+
}}">
|
|
415
|
+
</wa-input>
|
|
416
|
+
${this.availableModels.map(model => html`
|
|
417
|
+
<wa-dropdown-item value="${model.id}">
|
|
418
|
+
${model.name || model.id}
|
|
419
|
+
</wa-dropdown-item>
|
|
420
|
+
`)}
|
|
421
|
+
</wa-dropdown>
|
|
422
|
+
`, () => html`
|
|
423
|
+
${this.renderEditableCell(index, 'model', provider.model)}
|
|
424
|
+
`)}
|
|
425
|
+
`)}
|
|
426
|
+
`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
render() {
|
|
430
|
+
return html`
|
|
431
|
+
<div class="ai-config-editor">
|
|
432
|
+
<div class="editor-header">
|
|
433
|
+
<h2>${t('PROVIDERS')}</h2>
|
|
434
|
+
<div class="header-actions">
|
|
435
|
+
<wa-button
|
|
436
|
+
variant="brand"
|
|
437
|
+
appearance="filled"
|
|
438
|
+
@click="${() => this.addProvider()}">
|
|
439
|
+
${t('ADD_PROVIDER')}
|
|
440
|
+
</wa-button>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<div class="table-container">
|
|
445
|
+
<table class="providers-table">
|
|
446
|
+
<thead>
|
|
447
|
+
<tr>
|
|
448
|
+
<th>${t('DEFAULT')}</th>
|
|
449
|
+
<th>${t('NAME')}</th>
|
|
450
|
+
<th>${t('MODEL')}</th>
|
|
451
|
+
<th>${t('API_ENDPOINT')}</th>
|
|
452
|
+
<th>${t('API_KEY')}</th>
|
|
453
|
+
<th>${t('OCR_ENDPOINT')}</th>
|
|
454
|
+
<th>${t('OCR_MODEL')}</th>
|
|
455
|
+
<th>${t('ACTIONS')}</th>
|
|
456
|
+
</tr>
|
|
457
|
+
</thead>
|
|
458
|
+
<tbody>
|
|
459
|
+
${repeat(this.providers, (provider, index) => index, (provider, index) => html`
|
|
460
|
+
<tr class="${this.editingCell?.rowIndex === index ? 'editing' : ''}">
|
|
461
|
+
<td>
|
|
462
|
+
<wa-checkbox
|
|
463
|
+
.checked="${this.defaultProvider === provider.name}"
|
|
464
|
+
@change="${async (e: Event) => {
|
|
465
|
+
const checkbox = e.target as any;
|
|
466
|
+
if (checkbox.checked) {
|
|
467
|
+
// When checking, uncheck all others and set this as default
|
|
468
|
+
this.setDefaultProvider(provider.name);
|
|
469
|
+
// Wait for update to complete, then ensure other default checkboxes are unchecked
|
|
470
|
+
await this.updateComplete;
|
|
471
|
+
// Select only checkboxes in the first column (Default column)
|
|
472
|
+
const rows = this.shadowRoot?.querySelectorAll('tbody tr') as NodeListOf<HTMLElement>;
|
|
473
|
+
if (rows) {
|
|
474
|
+
rows.forEach((row) => {
|
|
475
|
+
const cb = row.querySelector('td:first-child wa-checkbox') as any;
|
|
476
|
+
if (cb && cb !== checkbox) {
|
|
477
|
+
cb.checked = false;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
// Prevent unchecking if this is already the default
|
|
483
|
+
if (this.defaultProvider === provider.name) {
|
|
484
|
+
// Re-check the checkbox to prevent unsetting the default
|
|
485
|
+
checkbox.checked = true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}}">
|
|
489
|
+
</wa-checkbox>
|
|
490
|
+
</td>
|
|
491
|
+
<td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'name')}">
|
|
492
|
+
${this.renderEditableCell(index, 'name', provider.name)}
|
|
493
|
+
</td>
|
|
494
|
+
<td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'model')}">
|
|
495
|
+
${this.renderModelCell(index, provider)}
|
|
496
|
+
</td>
|
|
497
|
+
<td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'chatApiEndpoint')}">
|
|
498
|
+
${this.renderEditableCell(index, 'chatApiEndpoint', html`<span class="endpoint-text">${provider.chatApiEndpoint}</span>`)}
|
|
499
|
+
</td>
|
|
500
|
+
<td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'apiKey')}">
|
|
501
|
+
${this.renderEditableCell(index, 'apiKey', html`<span class="api-key-text">${provider.apiKey ? '••••••••' : ''}</span>`, 'password', t('API_KEY'))}
|
|
502
|
+
</td>
|
|
503
|
+
<td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'ocrApiEndpoint')}">
|
|
504
|
+
${this.renderEditableCell(index, 'ocrApiEndpoint', provider.ocrApiEndpoint || '-', 'text', t('OPTIONAL'))}
|
|
505
|
+
</td>
|
|
506
|
+
<td class="editable-cell" @dblclick="${() => this.startCellEditing(index, 'ocrModel')}">
|
|
507
|
+
${this.renderEditableCell(index, 'ocrModel', provider.ocrModel || '-', 'text', t('OPTIONAL'))}
|
|
508
|
+
</td>
|
|
509
|
+
<td>
|
|
510
|
+
<wa-button
|
|
511
|
+
variant="danger"
|
|
512
|
+
appearance="plain"
|
|
513
|
+
size="small"
|
|
514
|
+
@click="${() => this.deleteProvider(index)}">
|
|
515
|
+
${t('DELETE_PROVIDER')}
|
|
516
|
+
</wa-button>
|
|
517
|
+
</td>
|
|
518
|
+
</tr>
|
|
519
|
+
`)}
|
|
520
|
+
</tbody>
|
|
521
|
+
</table>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
${when(this.providers.length === 0, () => html`
|
|
525
|
+
<div class="empty-state">
|
|
526
|
+
<p>${t('NO_PROVIDERS_CONFIGURED')}</p>
|
|
527
|
+
</div>
|
|
528
|
+
`)}
|
|
529
|
+
|
|
530
|
+
<div class="tool-approval-section">
|
|
531
|
+
<h3>${t('TOOL_APPROVALS')}</h3>
|
|
532
|
+
<div class="tool-approval-controls">
|
|
533
|
+
<wa-checkbox
|
|
534
|
+
.checked="${this.requireToolApproval}"
|
|
535
|
+
@change="${(e: Event) => {
|
|
536
|
+
const checkbox = e.target as any;
|
|
537
|
+
this.requireToolApproval = checkbox.checked;
|
|
538
|
+
this.markDirtyAndUpdate();
|
|
539
|
+
}}">
|
|
540
|
+
${t('REQUIRE_APPROVAL_BEFORE_EXECUTING')}
|
|
541
|
+
</wa-checkbox>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<div class="tool-detection-section" style="margin-top: 1.5rem;">
|
|
545
|
+
<wa-checkbox
|
|
546
|
+
.checked="${this.smartToolDetection}"
|
|
547
|
+
@change="${(e: Event) => {
|
|
548
|
+
const checkbox = e.target as any;
|
|
549
|
+
this.smartToolDetection = checkbox.checked;
|
|
550
|
+
this.markDirtyAndUpdate();
|
|
551
|
+
}}">
|
|
552
|
+
${t('SMART_TOOL_DETECTION')}
|
|
553
|
+
</wa-checkbox>
|
|
554
|
+
<p class="hint" style="margin-top: 0.5rem; margin-left: 1.75rem; color: var(--wa-color-text-secondary, #666); font-size: 0.875rem;">
|
|
555
|
+
${t('SMART_TOOL_DETECTION_HINT')}
|
|
556
|
+
</p>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div class="allowlist-section">
|
|
560
|
+
<h4>
|
|
561
|
+
${t('APPROVED_COMMANDS')}
|
|
562
|
+
<span class="command-stats">
|
|
563
|
+
(${this.toolApprovalAllowlist.length}/${this.availableCommands.length})
|
|
564
|
+
</span>
|
|
565
|
+
</h4>
|
|
566
|
+
<p class="hint">
|
|
567
|
+
${this.requireToolApproval
|
|
568
|
+
? t('SELECT_COMMANDS_WITHOUT_APPROVAL')
|
|
569
|
+
: t('COMMANDS_AUTO_APPROVED')}
|
|
570
|
+
</p>
|
|
571
|
+
<div class="commands-list ${!this.requireToolApproval ? 'disabled' : ''}">
|
|
572
|
+
${this.availableCommands.map(cmd => html`
|
|
573
|
+
<div class="command-item">
|
|
574
|
+
<wa-checkbox
|
|
575
|
+
.checked="${this.toolApprovalAllowlist.includes(cmd.id)}"
|
|
576
|
+
?disabled="${!this.requireToolApproval}"
|
|
577
|
+
@change="${(e: Event) => {
|
|
578
|
+
const checkbox = e.target as any;
|
|
579
|
+
if (checkbox.checked) {
|
|
580
|
+
if (!this.toolApprovalAllowlist.includes(cmd.id)) {
|
|
581
|
+
this.toolApprovalAllowlist = [...this.toolApprovalAllowlist, cmd.id];
|
|
582
|
+
this.markDirtyAndUpdate();
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
this.toolApprovalAllowlist = this.toolApprovalAllowlist.filter(id => id !== cmd.id);
|
|
586
|
+
this.markDirtyAndUpdate();
|
|
587
|
+
}
|
|
588
|
+
}}">
|
|
589
|
+
<div class="command-label">
|
|
590
|
+
${cmd.name}
|
|
591
|
+
${cmd.description ? html`
|
|
592
|
+
<span class="command-description">${cmd.description}</span>
|
|
593
|
+
` : ''}
|
|
594
|
+
</div>
|
|
595
|
+
</wa-checkbox>
|
|
596
|
+
</div>
|
|
597
|
+
`)}
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
static styles = css`
|
|
606
|
+
:host {
|
|
607
|
+
display: block;
|
|
608
|
+
height: 100%;
|
|
609
|
+
overflow: auto;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.ai-config-editor {
|
|
613
|
+
display: flex;
|
|
614
|
+
flex-direction: column;
|
|
615
|
+
height: 100%;
|
|
616
|
+
padding: 1rem;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.editor-header {
|
|
620
|
+
display: flex;
|
|
621
|
+
justify-content: space-between;
|
|
622
|
+
align-items: center;
|
|
623
|
+
margin-bottom: 1rem;
|
|
624
|
+
padding-bottom: 1rem;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.editor-header h2 {
|
|
628
|
+
margin: 0;
|
|
629
|
+
font-size: 1.25rem;
|
|
630
|
+
font-weight: 600;
|
|
631
|
+
color: var(--wa-color-text-normal);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.header-actions {
|
|
635
|
+
display: flex;
|
|
636
|
+
gap: 0.5rem;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.table-container {
|
|
640
|
+
flex: 1;
|
|
641
|
+
overflow: auto;
|
|
642
|
+
border: solid var(--wa-border-width-s) var(--wa-color-neutral-border-loud);
|
|
643
|
+
border-radius: var(--wa-border-radius-m);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.providers-table {
|
|
647
|
+
width: 100%;
|
|
648
|
+
border-collapse: collapse;
|
|
649
|
+
background-color: var(--wa-color-surface-raised);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.providers-table thead {
|
|
653
|
+
position: sticky;
|
|
654
|
+
top: 0;
|
|
655
|
+
background-color: var(--wa-color-surface-raised);
|
|
656
|
+
z-index: 1;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.providers-table th {
|
|
660
|
+
padding: 0.75rem;
|
|
661
|
+
text-align: left;
|
|
662
|
+
font-weight: 600;
|
|
663
|
+
font-size: 0.875rem;
|
|
664
|
+
color: var(--wa-color-text-subtle);
|
|
665
|
+
border-bottom: solid var(--wa-border-width-s) var(--wa-color-neutral-border-loud);
|
|
666
|
+
white-space: nowrap;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.providers-table td {
|
|
670
|
+
padding: 0.75rem;
|
|
671
|
+
border-bottom: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
|
|
672
|
+
vertical-align: middle;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.providers-table tbody tr:hover {
|
|
676
|
+
background-color: var(--wa-color-surface-lowered);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.providers-table tbody tr.editing {
|
|
680
|
+
background-color: var(--wa-color-surface-brand-subtle);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.providers-table tbody tr:last-child td {
|
|
684
|
+
border-bottom: none;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.providers-table wa-input {
|
|
688
|
+
width: 100%;
|
|
689
|
+
min-width: 150px;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.providers-table wa-dropdown {
|
|
693
|
+
width: 100%;
|
|
694
|
+
min-width: 150px;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.providers-table wa-dropdown wa-input {
|
|
698
|
+
width: 100%;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.providers-table wa-checkbox {
|
|
702
|
+
display: flex;
|
|
703
|
+
justify-content: center;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.editable-cell {
|
|
707
|
+
cursor: pointer;
|
|
708
|
+
position: relative;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.editable-cell:hover {
|
|
712
|
+
background-color: var(--wa-color-surface-lowered);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.editable-cell span {
|
|
716
|
+
display: block;
|
|
717
|
+
min-height: 1.5rem;
|
|
718
|
+
padding: 0.25rem 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.endpoint-text {
|
|
722
|
+
font-family: var(--wa-font-mono);
|
|
723
|
+
font-size: 0.875rem;
|
|
724
|
+
color: var(--wa-color-text-subtle);
|
|
725
|
+
word-break: break-all;
|
|
726
|
+
max-width: 200px;
|
|
727
|
+
display: inline-block;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.api-key-text {
|
|
731
|
+
font-family: var(--wa-font-mono);
|
|
732
|
+
font-size: 0.875rem;
|
|
733
|
+
color: var(--wa-color-text-subtle);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.empty-state {
|
|
737
|
+
display: flex;
|
|
738
|
+
justify-content: center;
|
|
739
|
+
align-items: center;
|
|
740
|
+
padding: 3rem;
|
|
741
|
+
color: var(--wa-color-text-subtle);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.empty-state p {
|
|
745
|
+
margin: 0;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.tool-approval-section {
|
|
749
|
+
margin-top: 2rem;
|
|
750
|
+
padding-top: 2rem;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.tool-approval-section h3 {
|
|
754
|
+
margin: 0 0 1rem 0;
|
|
755
|
+
font-size: 1.125rem;
|
|
756
|
+
font-weight: 600;
|
|
757
|
+
color: var(--wa-color-text-normal);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.tool-approval-controls {
|
|
761
|
+
margin-bottom: 1rem;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.allowlist-section {
|
|
765
|
+
margin-top: 1.5rem;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.allowlist-section h4 {
|
|
769
|
+
margin: 0 0 0.5rem 0;
|
|
770
|
+
font-size: 1rem;
|
|
771
|
+
font-weight: 500;
|
|
772
|
+
color: var(--wa-color-text-normal);
|
|
773
|
+
display: flex;
|
|
774
|
+
align-items: center;
|
|
775
|
+
gap: 0.5rem;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.command-stats {
|
|
779
|
+
font-size: 0.875rem;
|
|
780
|
+
font-weight: normal;
|
|
781
|
+
color: var(--wa-color-text-subtle);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.allowlist-section .hint {
|
|
785
|
+
margin: 0 0 1rem 0;
|
|
786
|
+
font-size: 0.875rem;
|
|
787
|
+
color: var(--wa-color-text-subtle);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.commands-list {
|
|
791
|
+
display: flex;
|
|
792
|
+
flex-direction: column;
|
|
793
|
+
gap: 0.5rem;
|
|
794
|
+
max-height: 300px;
|
|
795
|
+
overflow-y: auto;
|
|
796
|
+
padding: 0.5rem;
|
|
797
|
+
border: solid var(--wa-border-width-s) var(--wa-color-neutral-border-subtle);
|
|
798
|
+
border-radius: var(--wa-border-radius-m);
|
|
799
|
+
background-color: var(--wa-color-surface-raised);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.commands-list.disabled {
|
|
803
|
+
opacity: 0.6;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.command-item {
|
|
807
|
+
padding: 0.25rem 0;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.command-item wa-checkbox {
|
|
811
|
+
width: 100%;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.command-label {
|
|
815
|
+
display: flex;
|
|
816
|
+
flex-direction: column;
|
|
817
|
+
gap: 0.125rem;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.command-description {
|
|
821
|
+
font-size: 0.75rem;
|
|
822
|
+
color: var(--wa-color-text-subtle);
|
|
823
|
+
font-weight: normal;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
`;
|
|
827
|
+
}
|
|
828
|
+
|