worksona-js 0.2.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/worksona.js ADDED
@@ -0,0 +1,2241 @@
1
+ /**
2
+ * Worksona.js - LLM Agent Management API
3
+ * Version: 0.1.2
4
+ *
5
+ * A lightweight, single-file solution for deploying and managing AI agents
6
+ * with distinct personalities across multiple LLM providers.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ // Agent class for managing agent state and history
12
+ class Agent {
13
+ constructor(config) {
14
+ this.id = config.id;
15
+ this.name = config.name;
16
+ this.description = config.description;
17
+
18
+ // Normalize config to prevent nesting problems
19
+ if (config.config) {
20
+ // If there's a nested config, flatten it
21
+ this.config = {
22
+ ...config,
23
+ ...config.config,
24
+ };
25
+ // Remove the nested config to avoid circular structure
26
+ delete this.config.config;
27
+ } else {
28
+ this.config = config;
29
+ }
30
+
31
+ // Store system prompt and examples directly
32
+ this.systemPrompt = this.config.systemPrompt;
33
+ this.examples = this.config.examples || [];
34
+
35
+ // If traits are in config, move them to top level
36
+ if (this.config.traits) {
37
+ this.traits = this.config.traits;
38
+ }
39
+
40
+ this.transactions = [];
41
+ this.metrics = {
42
+ totalQueries: 0,
43
+ avgResponseTime: 0,
44
+ lastActive: null,
45
+ successRate: 1.0,
46
+ errorCount: 0
47
+ };
48
+ this.state = {
49
+ isActive: true,
50
+ currentProvider: this.config.provider || 'openai',
51
+ currentModel: this.config.model,
52
+ lastError: null
53
+ };
54
+ }
55
+
56
+ addTransaction(transaction) {
57
+ this.transactions.push(transaction);
58
+ this.metrics.totalQueries++;
59
+ this.metrics.lastActive = new Date();
60
+
61
+ // Update average response time
62
+ if (transaction.duration) {
63
+ const totalTime = this.metrics.avgResponseTime * (this.metrics.totalQueries - 1) + transaction.duration;
64
+ this.metrics.avgResponseTime = totalTime / this.metrics.totalQueries;
65
+ }
66
+
67
+ // Update success rate if there was an error
68
+ if (transaction.error) {
69
+ this.metrics.errorCount++;
70
+ this.metrics.successRate = (this.metrics.totalQueries - this.metrics.errorCount) / this.metrics.totalQueries;
71
+ this.state.lastError = transaction.error;
72
+ }
73
+
74
+ // Keep only last 100 transactions to manage memory
75
+ if (this.transactions.length > 100) {
76
+ this.transactions = this.transactions.slice(-100);
77
+ }
78
+ }
79
+
80
+ getHistory() {
81
+ return this.transactions;
82
+ }
83
+
84
+ getMetrics() {
85
+ return this.metrics;
86
+ }
87
+
88
+ getState() {
89
+ return this.state;
90
+ }
91
+ }
92
+
93
+ (function(global) {
94
+ 'use strict';
95
+
96
+ class Worksona {
97
+ constructor(options = {}) {
98
+ this.options = {
99
+ debug: false,
100
+ defaultProvider: 'openai',
101
+ defaultModel: 'gpt-3.5-turbo',
102
+ apiKeys: {},
103
+ ...options
104
+ };
105
+
106
+ this.agents = new Map();
107
+ this.activeProvider = null;
108
+ this.controlPanelId = null;
109
+ this.eventHandlers = {};
110
+
111
+ this._initializeProviders();
112
+
113
+ // Initialize control panel if enabled
114
+ if (options.controlPanel !== false) {
115
+ // Create floating control panel by default
116
+ this.createFloatingControlPanel();
117
+ }
118
+ }
119
+
120
+ // Initialize API clients for different providers
121
+ _initializeProviders() {
122
+ this.providers = {
123
+ openai: this.options.apiKeys.openai ? {
124
+ chat: async (agent, message) => {
125
+ try {
126
+ // Determine if this is a vision request
127
+ const isVisionRequest = message.content && message.content.type === 'image';
128
+ const modelName = (agent.config.model || this.options.defaultModel || 'gpt-4o').trim();
129
+
130
+ this._log(`Making OpenAI request with model: ${modelName}`, 'info');
131
+
132
+ let messages;
133
+ // Determine if this is a vision request - check both formats for backward compatibility
134
+ if (isVisionRequest || (message && message.type === 'image') || (message && message.content && typeof message.content === 'object' && message.content.type === 'image')) {
135
+ // Format vision-specific message
136
+ // Handle different possible formats for image data
137
+ let imageData;
138
+
139
+ if (isVisionRequest) {
140
+ imageData = message.content;
141
+ } else if (message.type === 'image') {
142
+ imageData = message;
143
+ } else if (message.content && message.content.type === 'image') {
144
+ imageData = message.content;
145
+ }
146
+
147
+ // Ensure we have valid image data
148
+ if (!imageData || !imageData.imageUrl) {
149
+ throw new Error('Invalid image data: Missing required imageUrl property');
150
+ }
151
+
152
+ messages = [
153
+ {
154
+ role: 'system',
155
+ content: agent.config.systemPrompt || 'You are a helpful vision analysis assistant.'
156
+ },
157
+ {
158
+ role: 'user',
159
+ content: [
160
+ {
161
+ type: 'text',
162
+ text: imageData.prompt || 'Please analyze this image.'
163
+ },
164
+ {
165
+ type: 'image_url',
166
+ image_url: {
167
+ url: imageData.imageUrl,
168
+ detail: imageData.detail || 'high' // 'auto', 'low', or 'high'
169
+ }
170
+ }
171
+ ]
172
+ }
173
+ ];
174
+
175
+ // Log the message structure for debugging
176
+ this._log(`Vision message structure: ${JSON.stringify(messages)}`, 'info');
177
+ } else {
178
+ // Standard chat message format
179
+ // Ensure we have a valid message content
180
+ let userContent = '';
181
+
182
+ if (typeof message === 'string') {
183
+ userContent = message;
184
+ } else if (message && typeof message.content === 'string') {
185
+ userContent = message.content;
186
+ } else if (message && message.content) {
187
+ // If content is an object, convert it to a string
188
+ try {
189
+ userContent = JSON.stringify(message.content);
190
+ } catch (e) {
191
+ userContent = 'Unable to process message content';
192
+ }
193
+ } else if (message) {
194
+ // If message is an object but doesn't have content property
195
+ try {
196
+ userContent = JSON.stringify(message);
197
+ } catch (e) {
198
+ userContent = 'Unable to process message';
199
+ }
200
+ } else {
201
+ userContent = 'No message provided';
202
+ }
203
+
204
+ // Log the processed content for debugging
205
+ this._log(`Processed user content: ${userContent.substring(0, 100)}${userContent.length > 100 ? '...' : ''}`, 'info');
206
+
207
+ messages = [
208
+ {
209
+ role: 'system',
210
+ content: agent.config.systemPrompt || 'You are a helpful assistant.'
211
+ },
212
+ ...(agent.config.examples || []).flatMap(ex => [
213
+ { role: 'user', content: ex.user },
214
+ { role: 'assistant', content: ex.assistant }
215
+ ]),
216
+ {
217
+ role: 'user',
218
+ content: userContent
219
+ }
220
+ ];
221
+ }
222
+
223
+ const requestBody = {
224
+ model: modelName,
225
+ messages: messages,
226
+ temperature: agent.config.temperature || 0.7,
227
+ max_tokens: agent.config.maxTokens || 500,
228
+ top_p: agent.config.topP || 1,
229
+ frequency_penalty: agent.config.frequencyPenalty || 0,
230
+ presence_penalty: agent.config.presencePenalty || 0,
231
+ stream: false
232
+ };
233
+
234
+ // Use the appropriate API endpoint based on the model
235
+ const apiEndpoint = isVisionRequest ?
236
+ 'https://api.openai.com/v1/chat/completions' :
237
+ 'https://api.openai.com/v1/chat/completions';
238
+
239
+ const response = await fetch(apiEndpoint, {
240
+ method: 'POST',
241
+ headers: {
242
+ 'Content-Type': 'application/json',
243
+ 'Authorization': `Bearer ${this.options.apiKeys.openai}`,
244
+ 'OpenAI-Organization': agent.config.organization || ''
245
+ },
246
+ body: JSON.stringify(requestBody)
247
+ });
248
+
249
+ const data = await response.json();
250
+
251
+ if (!response.ok) {
252
+ this._log(`OpenAI API error: ${JSON.stringify(data)}`, 'error');
253
+ throw new Error(data.error?.message || `OpenAI API error: ${response.status}`);
254
+ }
255
+
256
+ // Log successful vision processing
257
+ if (isVisionRequest) {
258
+ this._log(`Successfully processed vision request with model: ${modelName}`);
259
+ }
260
+
261
+ return data.choices[0].message.content;
262
+ } catch (error) {
263
+ this._log(`OpenAI error details: ${error.message}`, 'error');
264
+ this._handleError(error, 'PROVIDER_ERROR', 'OpenAI request failed');
265
+ }
266
+ },
267
+ defaultModels: {
268
+ chat: 'gpt-4o',
269
+ vision: 'gpt-4o'
270
+ }
271
+ } : null,
272
+
273
+ anthropic: this.options.apiKeys.anthropic ? {
274
+ chat: async (agent, message) => {
275
+ try {
276
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
277
+ method: 'POST',
278
+ headers: {
279
+ 'Content-Type': 'application/json',
280
+ 'x-api-key': this.options.apiKeys.anthropic,
281
+ 'anthropic-version': '2023-06-01'
282
+ },
283
+ body: JSON.stringify({
284
+ model: agent.config.model || 'claude-3-opus-20240229',
285
+ max_tokens: agent.config.maxTokens || 500,
286
+ temperature: agent.config.temperature || 0.7,
287
+ system: agent.config.systemPrompt,
288
+ messages: [
289
+ ...(agent.config.examples || []).flatMap(ex => [
290
+ { role: 'user', content: ex.user },
291
+ { role: 'assistant', content: ex.assistant }
292
+ ]),
293
+ { role: 'user', content: message }
294
+ ],
295
+ top_p: agent.config.topP || 1,
296
+ top_k: agent.config.topK || 50,
297
+ metadata: {
298
+ user_id: agent.id
299
+ }
300
+ })
301
+ });
302
+
303
+ const data = await response.json();
304
+ if (!response.ok) throw new Error(data.error?.message || 'Anthropic API error');
305
+ return data.content[0].text;
306
+ } catch (error) {
307
+ this._handleError(error, 'PROVIDER_ERROR', 'Anthropic request failed');
308
+ }
309
+ },
310
+ defaultModels: {
311
+ chat: 'claude-3-opus-20240229',
312
+ completion: 'claude-3-sonnet-20240229'
313
+ }
314
+ } : null,
315
+
316
+ google: this.options.apiKeys.google ? {
317
+ chat: async (agent, message) => {
318
+ try {
319
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${agent.config.model || 'gemini-pro'}:generateContent?key=${this.options.apiKeys.google}`, {
320
+ method: 'POST',
321
+ headers: {
322
+ 'Content-Type': 'application/json'
323
+ },
324
+ body: JSON.stringify({
325
+ contents: [
326
+ {
327
+ role: 'user',
328
+ parts: [
329
+ {
330
+ text: agent.config.systemPrompt
331
+ }
332
+ ]
333
+ },
334
+ ...(agent.config.examples || []).flatMap(ex => [
335
+ {
336
+ role: 'user',
337
+ parts: [{ text: ex.user }]
338
+ },
339
+ {
340
+ role: 'model',
341
+ parts: [{ text: ex.assistant }]
342
+ }
343
+ ]),
344
+ {
345
+ role: 'user',
346
+ parts: [{ text: message }]
347
+ }
348
+ ],
349
+ generationConfig: {
350
+ temperature: agent.config.temperature || 0.7,
351
+ maxOutputTokens: agent.config.maxTokens || 500,
352
+ topP: agent.config.topP || 1,
353
+ topK: agent.config.topK || 40,
354
+ candidateCount: 1
355
+ },
356
+ safetySettings: [
357
+ {
358
+ category: 'HARM_CATEGORY_HARASSMENT',
359
+ threshold: 'BLOCK_MEDIUM_AND_ABOVE'
360
+ },
361
+ {
362
+ category: 'HARM_CATEGORY_HATE_SPEECH',
363
+ threshold: 'BLOCK_MEDIUM_AND_ABOVE'
364
+ }
365
+ ]
366
+ })
367
+ });
368
+
369
+ const data = await response.json();
370
+ if (!response.ok) throw new Error(data.error?.message || 'Google API error');
371
+ return data.candidates[0].content.parts[0].text;
372
+ } catch (error) {
373
+ this._handleError(error, 'PROVIDER_ERROR', 'Google request failed');
374
+ }
375
+ },
376
+ defaultModels: {
377
+ chat: 'gemini-pro',
378
+ vision: 'gemini-pro-vision'
379
+ }
380
+ } : null
381
+ };
382
+ }
383
+
384
+ // Helper function to format messages based on provider
385
+ _formatMessages(provider, agent, message) {
386
+ switch (provider) {
387
+ case 'openai':
388
+ return [
389
+ { role: 'system', content: agent.config.systemPrompt },
390
+ ...(agent.config.examples || []).flatMap(ex => [
391
+ { role: 'user', content: ex.user },
392
+ { role: 'assistant', content: ex.assistant }
393
+ ]),
394
+ { role: 'user', content: message }
395
+ ];
396
+
397
+ case 'anthropic':
398
+ return [
399
+ ...(agent.config.examples || []).flatMap(ex => [
400
+ { role: 'user', content: ex.user },
401
+ { role: 'assistant', content: ex.assistant }
402
+ ]),
403
+ { role: 'user', content: message }
404
+ ];
405
+
406
+ case 'google':
407
+ return [
408
+ {
409
+ role: 'user',
410
+ parts: [{ text: agent.config.systemPrompt }]
411
+ },
412
+ ...(agent.config.examples || []).flatMap(ex => [
413
+ {
414
+ role: 'user',
415
+ parts: [{ text: ex.user }]
416
+ },
417
+ {
418
+ role: 'model',
419
+ parts: [{ text: ex.assistant }]
420
+ }
421
+ ]),
422
+ {
423
+ role: 'user',
424
+ parts: [{ text: message }]
425
+ }
426
+ ];
427
+
428
+ default:
429
+ throw new Error(`Unsupported provider: ${provider}`);
430
+ }
431
+ }
432
+
433
+ // Load an agent from configuration
434
+ async loadAgent(config) {
435
+ if (!config.id || !config.name) {
436
+ this._handleError(new Error('Invalid agent configuration'), 'CONFIG_ERROR');
437
+ return null;
438
+ }
439
+
440
+ try {
441
+ // Create new agent instance - Agent constructor now handles nested config
442
+ const agent = new Agent(config);
443
+
444
+ // Store agent
445
+ this.agents.set(agent.id, agent);
446
+
447
+ // Emit event
448
+ this._emit('agent-loaded', {
449
+ agentId: agent.id,
450
+ name: agent.name,
451
+ description: agent.description,
452
+ provider: agent.state.currentProvider,
453
+ model: agent.state.currentModel
454
+ });
455
+
456
+ // Update the control panel to show the new agent
457
+ this.updateControlPanel();
458
+
459
+ this._log(`Agent loaded: ${agent.name} (${agent.id})`);
460
+ return agent;
461
+ } catch (error) {
462
+ this._handleError(error, 'AGENT_LOAD_ERROR', `Failed to load agent: ${config.id}`);
463
+ return null;
464
+ }
465
+ }
466
+
467
+ // Send message to agent and get response
468
+ async chat(agentId, message, options = {}) {
469
+ const agent = this.agents.get(agentId);
470
+ if (!agent) {
471
+ this._handleError(new Error(`Agent not found: ${agentId}`), 'AGENT_NOT_FOUND');
472
+ return null;
473
+ }
474
+
475
+ // Always use the agent's configured provider first, then fallback to options or default
476
+ const provider = agent.config.provider || options.provider || this.options.defaultProvider;
477
+
478
+ if (!this.providers[provider]) {
479
+ this._handleError(new Error(`Provider not available: ${provider}`), 'PROVIDER_ERROR');
480
+ return null;
481
+ }
482
+
483
+ // Update agent state with current provider and model
484
+ agent.state.currentProvider = provider;
485
+ agent.state.currentModel = agent.config.model || this.options.defaultModel;
486
+
487
+ // Create transaction record
488
+ const transaction = {
489
+ timestamp: new Date(),
490
+ query: message,
491
+ response: null,
492
+ duration: 0,
493
+ error: null,
494
+ provider,
495
+ model: agent.state.currentModel
496
+ };
497
+
498
+ this._emit('chat-start', { agentId, message });
499
+ this._log(`Chat request to ${agentId}: ${message}`);
500
+
501
+ const startTime = Date.now();
502
+ try {
503
+ const response = await this.providers[provider].chat(agent, message);
504
+ transaction.duration = Date.now() - startTime;
505
+ transaction.response = response;
506
+
507
+ // Add transaction to agent history
508
+ agent.addTransaction(transaction);
509
+
510
+ // Update control panel to reflect the new transaction
511
+ this.updateControlPanel();
512
+
513
+ this._emit('chat-complete', {
514
+ agentId,
515
+ message,
516
+ response,
517
+ duration: transaction.duration
518
+ });
519
+
520
+ this._log(`Chat response from ${agentId}: ${response}`);
521
+ return response;
522
+ } catch (error) {
523
+ transaction.error = error;
524
+ transaction.duration = Date.now() - startTime;
525
+
526
+ // Add failed transaction to history
527
+ agent.addTransaction(transaction);
528
+
529
+ // Update control panel to reflect the failed transaction
530
+ this.updateControlPanel();
531
+
532
+ this._handleError(error, 'CHAT_ERROR', `Chat failed with ${agentId}`);
533
+ return null;
534
+ }
535
+ }
536
+
537
+ // Get agent history
538
+ getAgentHistory(agentId) {
539
+ const agent = this.agents.get(agentId);
540
+ return agent ? agent.getHistory() : [];
541
+ }
542
+
543
+ // Get agent metrics
544
+ getAgentMetrics(agentId) {
545
+ const agent = this.agents.get(agentId);
546
+ return agent ? agent.getMetrics() : null;
547
+ }
548
+
549
+ // Get agent state
550
+ getAgentState(agentId) {
551
+ const agent = this.agents.get(agentId);
552
+ return agent ? agent.getState() : null;
553
+ }
554
+
555
+ // Get agent by ID
556
+ getAgent(agentId) {
557
+ return this.agents.get(agentId);
558
+ }
559
+
560
+ // Get all loaded agents
561
+ getAllAgents() {
562
+ return Array.from(this.agents.values());
563
+ }
564
+
565
+ // Remove agent by ID
566
+ removeAgent(agentId) {
567
+ const removed = this.agents.delete(agentId);
568
+ if (removed) {
569
+ this._emit('agent-removed', agentId);
570
+ this._log(`Agent removed: ${agentId}`);
571
+
572
+ // Update the control panel to reflect the removed agent
573
+ this.updateControlPanel();
574
+ }
575
+ return removed;
576
+ }
577
+
578
+ // Event handling
579
+ on(event, handler) {
580
+ if (!this.eventHandlers[event]) {
581
+ this.eventHandlers[event] = [];
582
+ }
583
+ this.eventHandlers[event].push(handler);
584
+ }
585
+
586
+ off(event, handler) {
587
+ if (this.eventHandlers[event]) {
588
+ this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler);
589
+ }
590
+ }
591
+
592
+ _emit(event, data) {
593
+ if (this.eventHandlers[event]) {
594
+ this.eventHandlers[event].forEach(handler => handler(data));
595
+ }
596
+ }
597
+
598
+ // Error handling
599
+ _handleError(error, code, message) {
600
+ let errorMessage = message || error.message;
601
+
602
+ switch (code) {
603
+ case 'IMAGE_PROCESSING_ERROR':
604
+ if (error.message.includes('image_too_large')) {
605
+ errorMessage = 'Image size exceeds maximum allowed size. Please reduce the image size.';
606
+ } else if (error.message.includes('invalid_image_format')) {
607
+ errorMessage = 'Invalid image format. Supported formats are: JPEG, PNG, WEBP, GIF.';
608
+ } else if (error.message.includes('vision_model_unavailable')) {
609
+ errorMessage = 'Vision model is currently unavailable. Please try again later.';
610
+ }
611
+
612
+ // OpenAI-specific errors
613
+ if (error.message.includes('invalid_api_key')) {
614
+ errorMessage = 'Invalid OpenAI API key. Please check your credentials.';
615
+ } else if (error.message.includes('insufficient_quota')) {
616
+ errorMessage = 'OpenAI API quota exceeded. Please check your usage limits.';
617
+ } else if (error.message.includes('rate_limit_exceeded')) {
618
+ errorMessage = 'OpenAI API rate limit exceeded. Please try again later.';
619
+ }
620
+ break;
621
+ case 'PROVIDER_ERROR':
622
+ if (error.message && error.message.includes('401')) {
623
+ errorMessage = 'API key is invalid or missing. Please check your API key in the control panel.';
624
+ } else if (error.message && error.message.includes('invalid_api_key')) {
625
+ errorMessage = 'Invalid API key format. Please check your API key in the control panel.';
626
+ } else if (error.message && error.message.includes('model')) {
627
+ errorMessage = `Model error: ${error.message}. Please try a different model in the control panel.`;
628
+ }
629
+ break;
630
+ case 'CONFIG_ERROR':
631
+ errorMessage = 'Invalid agent configuration. Please check your agent configuration and try again.';
632
+ break;
633
+ case 'AGENT_LOAD_ERROR':
634
+ errorMessage = `Failed to load agent: ${error.message}. Please check the agent configuration and try again.`;
635
+ break;
636
+ case 'AGENT_NOT_FOUND':
637
+ errorMessage = `Agent not found: ${error.message}. Please check the agent ID and try again.`;
638
+ break;
639
+ case 'CHAT_ERROR':
640
+ errorMessage = `Chat failed with ${error.message}. Please check the chat request and try again.`;
641
+ break;
642
+ default:
643
+ errorMessage = error.message || 'An unknown error occurred. Please try again later.';
644
+ }
645
+
646
+ const worksonaError = {
647
+ message: errorMessage,
648
+ code,
649
+ originalError: error
650
+ };
651
+ this._emit('error', worksonaError);
652
+ this._log(`Error [${code}]: ${worksonaError.message}`, 'error');
653
+ throw worksonaError;
654
+ }
655
+
656
+ // Logging
657
+ _log(message, level = 'info') {
658
+ if (this.options.debug) {
659
+ console[level](`[Worksona] ${message}`);
660
+ }
661
+ }
662
+
663
+ // Create control panel for development
664
+ createControlPanel(containerId) {
665
+ this.controlPanelId = containerId;
666
+ const container = document.getElementById(containerId);
667
+ if (!container) {
668
+ this._log('Control panel container not found', 'error');
669
+ return false;
670
+ }
671
+
672
+ // Store the container reference for other methods to use
673
+ this.controlPanelContainer = container;
674
+
675
+ // Create basic structure
676
+ container.innerHTML = `
677
+ <div class="worksona-control-panel">
678
+ <div class="worksona-panel-header">
679
+ <h2>Worksona Agents Control Panel</h2>
680
+ <div class="worksona-header-buttons">
681
+ <button class="worksona-expand-button" title="Toggle full screen">⛶</button>
682
+ <button class="worksona-close-button">×</button>
683
+ </div>
684
+ </div>
685
+
686
+ <div class="worksona-llm-status-bar">
687
+ <div class="worksona-status-label">LLM Status</div>
688
+ <div class="worksona-status-indicators">
689
+ <div class="worksona-status-item">
690
+ <div class="worksona-status-dot" id="worksona-openai-status"></div>
691
+ <span>OpenAI</span>
692
+ </div>
693
+ <div class="worksona-status-item">
694
+ <div class="worksona-status-dot" id="worksona-anthropic-status"></div>
695
+ <span>Anthropic</span>
696
+ </div>
697
+ <div class="worksona-status-item">
698
+ <div class="worksona-status-dot" id="worksona-google-status"></div>
699
+ <span>Google</span>
700
+ </div>
701
+ </div>
702
+ </div>
703
+
704
+ <div class="worksona-tabs">
705
+ <button class="worksona-tab" data-tab="api-keys">API Keys</button>
706
+ <button class="worksona-tab active" data-tab="agents">Agents</button>
707
+ </div>
708
+
709
+ <div class="worksona-content">
710
+ <div class="worksona-tab-content" id="worksona-api-keys-tab">
711
+ <h3 class="worksona-section-title">LLM Provider API Keys</h3>
712
+
713
+ <div class="worksona-key-input">
714
+ <label for="worksona-openai-key">OpenAI API Key</label>
715
+ <div class="worksona-input-group">
716
+ <input type="password" id="worksona-openai-key" placeholder="sk-...">
717
+ <button class="worksona-toggle-visibility">👁️</button>
718
+ </div>
719
+ </div>
720
+
721
+ <div class="worksona-key-input">
722
+ <label for="worksona-anthropic-key">Anthropic API Key</label>
723
+ <div class="worksona-input-group">
724
+ <input type="password" id="worksona-anthropic-key" placeholder="sk-ant-...">
725
+ <button class="worksona-toggle-visibility">👁️</button>
726
+ </div>
727
+ </div>
728
+
729
+ <div class="worksona-key-input">
730
+ <label for="worksona-google-key">Google API Key</label>
731
+ <div class="worksona-input-group">
732
+ <input type="password" id="worksona-google-key" placeholder="AIza...">
733
+ <button class="worksona-toggle-visibility">👁️</button>
734
+ </div>
735
+ </div>
736
+
737
+ <div class="worksona-button-group">
738
+ <button id="worksona-save-keys" class="worksona-primary-button">Save API Keys</button>
739
+ <button id="worksona-test-connections" class="worksona-secondary-button">Test Connections</button>
740
+ </div>
741
+ </div>
742
+
743
+ <div class="worksona-tab-content active" id="worksona-agents-tab">
744
+ <div id="worksona-agent-list"></div>
745
+ </div>
746
+ </div>
747
+ </div>
748
+ `;
749
+
750
+ // Add styles
751
+ const styles = `
752
+ .worksona-control-panel {
753
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
754
+ color: #333;
755
+ background: white;
756
+ border-radius: 8px;
757
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
758
+ overflow: hidden;
759
+ width: 100%;
760
+ max-width: 800px;
761
+ margin: 0 auto;
762
+ position: relative;
763
+ }
764
+
765
+ .worksona-panel-header {
766
+ display: flex;
767
+ justify-content: space-between;
768
+ align-items: center;
769
+ padding: 15px 20px;
770
+ border-bottom: 1px solid #eee;
771
+ }
772
+
773
+ .worksona-panel-header h2 {
774
+ margin: 0;
775
+ color: #1a56db;
776
+ font-size: 18px;
777
+ font-weight: 600;
778
+ }
779
+
780
+ .worksona-header-buttons {
781
+ display: flex;
782
+ gap: 10px;
783
+ align-items: center;
784
+ }
785
+
786
+ .worksona-expand-button {
787
+ background: none;
788
+ border: none;
789
+ font-size: 20px;
790
+ cursor: pointer;
791
+ color: #64748b;
792
+ padding: 4px;
793
+ display: flex;
794
+ align-items: center;
795
+ justify-content: center;
796
+ transition: color 0.2s;
797
+ }
798
+
799
+ .worksona-expand-button:hover {
800
+ color: #334155;
801
+ }
802
+
803
+ .worksona-llm-status-bar {
804
+ display: flex;
805
+ justify-content: space-between;
806
+ align-items: center;
807
+ padding: 10px 20px;
808
+ background: #111827;
809
+ color: white;
810
+ }
811
+
812
+ .worksona-status-indicators {
813
+ display: flex;
814
+ gap: 20px;
815
+ }
816
+
817
+ .worksona-status-item {
818
+ display: flex;
819
+ align-items: center;
820
+ gap: 5px;
821
+ font-size: 14px;
822
+ }
823
+
824
+ .worksona-status-dot {
825
+ width: 8px;
826
+ height: 8px;
827
+ border-radius: 50%;
828
+ background: #6b7280;
829
+ }
830
+
831
+ .worksona-status-dot.active {
832
+ background: #10b981;
833
+ }
834
+
835
+ .worksona-tabs {
836
+ display: flex;
837
+ padding: 0 20px;
838
+ background: #f9fafb;
839
+ border-bottom: 1px solid #e5e7eb;
840
+ }
841
+
842
+ .worksona-tab {
843
+ padding: 15px 20px;
844
+ background: none;
845
+ border: none;
846
+ cursor: pointer;
847
+ color: #6b7280;
848
+ font-weight: 500;
849
+ position: relative;
850
+ }
851
+
852
+ .worksona-tab:hover {
853
+ color: #4b5563;
854
+ }
855
+
856
+ .worksona-tab.active {
857
+ color: #2563eb;
858
+ }
859
+
860
+ .worksona-tab.active::after {
861
+ content: '';
862
+ position: absolute;
863
+ bottom: 0;
864
+ left: 0;
865
+ right: 0;
866
+ height: 2px;
867
+ background: #2563eb;
868
+ }
869
+
870
+ .worksona-content {
871
+ padding: 20px;
872
+ }
873
+
874
+ .worksona-tab-content {
875
+ display: none;
876
+ }
877
+
878
+ .worksona-tab-content.active {
879
+ display: block;
880
+ }
881
+
882
+ .worksona-section-title {
883
+ font-size: 16px;
884
+ font-weight: 600;
885
+ margin-top: 0;
886
+ margin-bottom: 20px;
887
+ color: #111827;
888
+ }
889
+
890
+ .worksona-key-input {
891
+ margin-bottom: 15px;
892
+ }
893
+
894
+ .worksona-key-input label {
895
+ display: block;
896
+ margin-bottom: 5px;
897
+ font-weight: 500;
898
+ color: #374151;
899
+ }
900
+
901
+ .worksona-input-group {
902
+ display: flex;
903
+ align-items: center;
904
+ }
905
+
906
+ .worksona-input-group input {
907
+ flex: 1;
908
+ padding: 10px 12px;
909
+ border: 1px solid #d1d5db;
910
+ border-radius: 6px 0 0 6px;
911
+ font-size: 14px;
912
+ }
913
+
914
+ .worksona-toggle-visibility {
915
+ padding: 10px 12px;
916
+ background: #f9fafb;
917
+ border: 1px solid #d1d5db;
918
+ border-left: none;
919
+ border-radius: 0 6px 6px 0;
920
+ cursor: pointer;
921
+ }
922
+
923
+ .worksona-button-group {
924
+ display: flex;
925
+ gap: 10px;
926
+ margin-top: 20px;
927
+ }
928
+
929
+ .worksona-primary-button {
930
+ padding: 8px 16px;
931
+ background: #2563eb;
932
+ color: white;
933
+ border: none;
934
+ border-radius: 6px;
935
+ font-weight: 500;
936
+ cursor: pointer;
937
+ }
938
+
939
+ .worksona-primary-button:hover {
940
+ background: #1d4ed8;
941
+ }
942
+
943
+ .worksona-secondary-button {
944
+ padding: 8px 16px;
945
+ background: #f3f4f6;
946
+ color: #374151;
947
+ border: 1px solid #d1d5db;
948
+ border-radius: 6px;
949
+ font-weight: 500;
950
+ cursor: pointer;
951
+ }
952
+
953
+ .worksona-secondary-button:hover {
954
+ background: #e5e7eb;
955
+ }
956
+
957
+ .worksona-agent-card {
958
+ background: white;
959
+ border: 1px solid #e5e7eb;
960
+ border-radius: 8px;
961
+ padding: 15px;
962
+ margin-bottom: 15px;
963
+ transition: box-shadow 0.2s;
964
+ }
965
+
966
+ .worksona-agent-card:hover {
967
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
968
+ }
969
+
970
+ .worksona-agent-header {
971
+ display: flex;
972
+ justify-content: space-between;
973
+ align-items: center;
974
+ margin-bottom: 10px;
975
+ cursor: pointer;
976
+ position: relative;
977
+ }
978
+
979
+ .worksona-expand-icon {
980
+ font-size: 12px;
981
+ color: #6b7280;
982
+ transition: transform 0.2s;
983
+ }
984
+
985
+ .worksona-expand-icon.rotated {
986
+ transform: rotate(180deg);
987
+ }
988
+
989
+ .worksona-agent-details.active + .worksona-expand-icon {
990
+ transform: rotate(180deg);
991
+ }
992
+
993
+ .worksona-agent-name {
994
+ font-size: 16px;
995
+ font-weight: 600;
996
+ color: #111827;
997
+ margin: 0;
998
+ }
999
+
1000
+ .worksona-agent-id {
1001
+ color: #6b7280;
1002
+ font-size: 12px;
1003
+ font-family: monospace;
1004
+ }
1005
+
1006
+ .worksona-agent-status {
1007
+ display: flex;
1008
+ align-items: center;
1009
+ gap: 8px;
1010
+ }
1011
+
1012
+ .worksona-status-label {
1013
+ font-size: 12px;
1014
+ padding: 2px 6px;
1015
+ border-radius: 10px;
1016
+ background: #f3f4f6;
1017
+ }
1018
+
1019
+ .worksona-agent-status.active .worksona-status-label {
1020
+ background: #dcfce7;
1021
+ color: #166534;
1022
+ }
1023
+
1024
+ .worksona-agent-status.inactive .worksona-status-label {
1025
+ background: #fee2e2;
1026
+ color: #991b1b;
1027
+ }
1028
+
1029
+ .worksona-agent-description {
1030
+ color: #4b5563;
1031
+ margin-bottom: 15px;
1032
+ font-size: 14px;
1033
+ }
1034
+
1035
+ .worksona-agent-details {
1036
+ display: none;
1037
+ overflow: hidden;
1038
+ border-top: 1px solid #e5e7eb;
1039
+ margin-top: 10px;
1040
+ padding-top: 15px;
1041
+ }
1042
+
1043
+ .worksona-agent-details.active {
1044
+ display: block;
1045
+ }
1046
+
1047
+ .worksona-agent-tabs {
1048
+ display: flex;
1049
+ border-bottom: 1px solid #e5e7eb;
1050
+ margin-bottom: 15px;
1051
+ }
1052
+
1053
+ .worksona-agent-tab {
1054
+ padding: 8px 16px;
1055
+ border: none;
1056
+ background: none;
1057
+ cursor: pointer;
1058
+ color: #6b7280;
1059
+ font-weight: 500;
1060
+ position: relative;
1061
+ font-size: 14px;
1062
+ }
1063
+
1064
+ .worksona-agent-tab:hover {
1065
+ color: #111827;
1066
+ }
1067
+
1068
+ .worksona-agent-tab.active {
1069
+ color: #2563eb;
1070
+ border-bottom: 2px solid #2563eb;
1071
+ }
1072
+
1073
+ .worksona-agent-tab-content {
1074
+ display: none;
1075
+ }
1076
+
1077
+ .worksona-agent-tab-content.active {
1078
+ display: block;
1079
+ }
1080
+
1081
+ .worksona-agent-tab-content h4 {
1082
+ font-size: 14px;
1083
+ font-weight: 600;
1084
+ margin: 0 0 10px;
1085
+ color: #111827;
1086
+ }
1087
+
1088
+ .worksona-config-details,
1089
+ .worksona-traits,
1090
+ .worksona-metrics {
1091
+ background: #f9fafb;
1092
+ border-radius: 6px;
1093
+ padding: 12px;
1094
+ margin-bottom: 15px;
1095
+ font-size: 13px;
1096
+ line-height: 1.5;
1097
+ }
1098
+
1099
+ .worksona-prompt-box {
1100
+ background: #f9fafb;
1101
+ border-radius: 6px;
1102
+ padding: 12px;
1103
+ margin-bottom: 15px;
1104
+ font-size: 13px;
1105
+ line-height: 1.5;
1106
+ white-space: pre-wrap;
1107
+ font-family: monospace;
1108
+ max-height: 200px;
1109
+ overflow-y: auto;
1110
+ }
1111
+
1112
+ .worksona-examples {
1113
+ display: flex;
1114
+ flex-direction: column;
1115
+ gap: 10px;
1116
+ margin-bottom: 20px;
1117
+ }
1118
+
1119
+ .worksona-example {
1120
+ background: #f9fafb;
1121
+ border-radius: 6px;
1122
+ padding: 12px;
1123
+ font-size: 13px;
1124
+ }
1125
+
1126
+ .worksona-example-user {
1127
+ margin-bottom: 8px;
1128
+ color: #4b5563;
1129
+ }
1130
+
1131
+ .worksona-example-assistant {
1132
+ color: #1f2937;
1133
+ }
1134
+
1135
+ .worksona-history {
1136
+ display: flex;
1137
+ flex-direction: column;
1138
+ gap: 10px;
1139
+ margin-bottom: 15px;
1140
+ max-height: 300px;
1141
+ overflow-y: auto;
1142
+ }
1143
+
1144
+ .worksona-history-item {
1145
+ background: #f9fafb;
1146
+ border-radius: 6px;
1147
+ padding: 12px;
1148
+ font-size: 13px;
1149
+ }
1150
+
1151
+ .worksona-history-time {
1152
+ font-size: 11px;
1153
+ color: #6b7280;
1154
+ margin-bottom: 5px;
1155
+ }
1156
+
1157
+ .worksona-history-query {
1158
+ margin-bottom: 8px;
1159
+ color: #4b5563;
1160
+ }
1161
+
1162
+ .worksona-history-response {
1163
+ color: #1f2937;
1164
+ margin-bottom: 8px;
1165
+ }
1166
+
1167
+ .worksona-history-meta {
1168
+ font-size: 11px;
1169
+ color: #6b7280;
1170
+ }
1171
+
1172
+ .worksona-no-agents {
1173
+ text-align: center;
1174
+ padding: 40px 20px;
1175
+ color: #6b7280;
1176
+ background: #f9fafb;
1177
+ border-radius: 8px;
1178
+ }
1179
+
1180
+ .worksona-no-examples {
1181
+ color: #6b7280;
1182
+ font-style: italic;
1183
+ background: #f9fafb;
1184
+ padding: 12px;
1185
+ border-radius: 6px;
1186
+ margin: 10px 0;
1187
+ }
1188
+
1189
+ .worksona-control-panel {
1190
+ box-shadow: 0 5px 20px rgba(0,0,0,0.2);
1191
+ max-height: 80vh;
1192
+ overflow: auto;
1193
+ background: white;
1194
+ border-radius: 8px;
1195
+ }
1196
+
1197
+ .worksona-json-display {
1198
+ background: #f8fafc;
1199
+ border-radius: 6px;
1200
+ padding: 15px;
1201
+ margin: 0;
1202
+ font-family: monospace;
1203
+ font-size: 13px;
1204
+ line-height: 1.5;
1205
+ overflow: auto;
1206
+ max-height: 500px;
1207
+ white-space: pre-wrap;
1208
+ color: #334155;
1209
+ border: 1px solid #e2e8f0;
1210
+ }
1211
+
1212
+ /* Ensure the json tab content has proper height */
1213
+ #worksona-agent-tab-content[id$="-json"] {
1214
+ max-height: 500px;
1215
+ overflow: auto;
1216
+ }
1217
+
1218
+ /* JSON syntax highlighting */
1219
+ .worksona-json-key {
1220
+ color: #0f766e;
1221
+ }
1222
+
1223
+ .worksona-json-string {
1224
+ color: #b91c1c;
1225
+ }
1226
+
1227
+ .worksona-json-number {
1228
+ color: #1d4ed8;
1229
+ }
1230
+
1231
+ .worksona-json-boolean {
1232
+ color: #7e22ce;
1233
+ }
1234
+
1235
+ .worksona-json-null {
1236
+ color: #64748b;
1237
+ }
1238
+
1239
+ /* Ensure the modal is scrollable on smaller screens */
1240
+ @media (max-height: 768px) {
1241
+ #worksona-modal-container {
1242
+ max-height: 90vh;
1243
+ }
1244
+
1245
+ .worksona-control-panel {
1246
+ max-height: 90vh;
1247
+ }
1248
+ }
1249
+ `;
1250
+
1251
+ const styleSheet = document.createElement('style');
1252
+ styleSheet.textContent = styles;
1253
+ document.head.appendChild(styleSheet);
1254
+
1255
+ // Add event listeners
1256
+ this._setupEventListeners(container);
1257
+
1258
+ // Initial update
1259
+ this.updateControlPanel();
1260
+
1261
+ return true;
1262
+ }
1263
+
1264
+ _setupEventListeners(container) {
1265
+ // Use the provided container or fall back to the stored container reference
1266
+ container = container || this.controlPanelContainer;
1267
+
1268
+ if (!container) {
1269
+ this._log('No container available for event listeners', 'error');
1270
+ return;
1271
+ }
1272
+
1273
+ // Tab switching
1274
+ container.querySelectorAll('.worksona-tab').forEach(tab => {
1275
+ tab.addEventListener('click', () => {
1276
+ container.querySelectorAll('.worksona-tab').forEach(t => t.classList.remove('active'));
1277
+ container.querySelectorAll('.worksona-tab-content').forEach(c => c.classList.remove('active'));
1278
+ tab.classList.add('active');
1279
+ document.getElementById(`worksona-${tab.dataset.tab}-tab`).classList.add('active');
1280
+ });
1281
+ });
1282
+
1283
+ // Password visibility toggle
1284
+ container.querySelectorAll('.worksona-toggle-visibility').forEach(button => {
1285
+ button.addEventListener('click', () => {
1286
+ const input = button.previousElementSibling;
1287
+ if (input.type === 'password') {
1288
+ input.type = 'text';
1289
+ button.textContent = '🔒';
1290
+ } else {
1291
+ input.type = 'password';
1292
+ button.textContent = '👁️';
1293
+ }
1294
+ });
1295
+ });
1296
+
1297
+ // Save API keys
1298
+ const saveButton = document.getElementById('worksona-save-keys');
1299
+ if (saveButton) {
1300
+ // Remove any existing event listeners
1301
+ const newSaveButton = saveButton.cloneNode(true);
1302
+ saveButton.parentNode.replaceChild(newSaveButton, saveButton);
1303
+
1304
+ newSaveButton.addEventListener('click', () => {
1305
+ const openaiKey = document.getElementById('worksona-openai-key').value;
1306
+ const anthropicKey = document.getElementById('worksona-anthropic-key').value;
1307
+ const googleKey = document.getElementById('worksona-google-key').value;
1308
+
1309
+ // Save to localStorage first
1310
+ if (openaiKey) localStorage.setItem('openai_api_key', openaiKey);
1311
+ if (anthropicKey) localStorage.setItem('anthropic_api_key', anthropicKey);
1312
+ if (googleKey) localStorage.setItem('google_api_key', googleKey);
1313
+
1314
+ // Update API keys in options
1315
+ this.options.apiKeys = {
1316
+ ...this.options.apiKeys,
1317
+ ...(openaiKey && { openai: openaiKey }),
1318
+ ...(anthropicKey && { anthropic: anthropicKey }),
1319
+ ...(googleKey && { google: googleKey })
1320
+ };
1321
+
1322
+ // Reinitialize providers with new keys
1323
+ this._initializeProviders();
1324
+
1325
+ // Update control panel
1326
+ this.updateControlPanel();
1327
+
1328
+ // Emit event
1329
+ this._emit('api-keys-updated', { providers: Object.keys(this.options.apiKeys) });
1330
+
1331
+ // Show success message only once
1332
+ alert('API keys saved successfully!');
1333
+ });
1334
+ }
1335
+
1336
+ // Test connections
1337
+ const testButton = document.getElementById('worksona-test-connections');
1338
+ if (testButton) {
1339
+ // Remove any existing event listeners
1340
+ const newTestButton = testButton.cloneNode(true);
1341
+ testButton.parentNode.replaceChild(newTestButton, testButton);
1342
+
1343
+ newTestButton.addEventListener('click', async () => {
1344
+ newTestButton.disabled = true;
1345
+ newTestButton.textContent = 'Testing...';
1346
+
1347
+ try {
1348
+ await this._testProviderConnections();
1349
+ this.updateControlPanel();
1350
+ } finally {
1351
+ newTestButton.disabled = false;
1352
+ newTestButton.textContent = 'Test Connections';
1353
+ }
1354
+ });
1355
+ }
1356
+
1357
+ // Close button
1358
+ const closeButton = container.querySelector('.worksona-close-button');
1359
+ if (closeButton) {
1360
+ // Remove any existing event listeners
1361
+ const newCloseButton = closeButton.cloneNode(true);
1362
+ closeButton.parentNode.replaceChild(newCloseButton, closeButton);
1363
+
1364
+ newCloseButton.addEventListener('click', () => {
1365
+ // If the control panel is in a modal or overlay, hide it
1366
+ const panel = container.querySelector('.worksona-control-panel');
1367
+ if (panel) {
1368
+ panel.style.display = 'none';
1369
+ }
1370
+ });
1371
+ }
1372
+
1373
+ // Temperature slider inputs
1374
+ container.querySelectorAll('input[type="range"]').forEach(slider => {
1375
+ const valueDisplay = slider.nextElementSibling;
1376
+
1377
+ slider.addEventListener('input', () => {
1378
+ if (valueDisplay) {
1379
+ valueDisplay.textContent = slider.value;
1380
+ }
1381
+ });
1382
+
1383
+ // Initialize with current value
1384
+ if (valueDisplay && slider.value) {
1385
+ valueDisplay.textContent = slider.value;
1386
+ }
1387
+ });
1388
+ }
1389
+
1390
+ async _testProviderConnections() {
1391
+ const providers = ['openai', 'anthropic', 'google'];
1392
+
1393
+ for (const provider of providers) {
1394
+ const statusDot = document.getElementById(`worksona-${provider}-status`);
1395
+ if (!statusDot) continue;
1396
+
1397
+ if (!this.options.apiKeys[provider] || this.options.apiKeys[provider].trim() === '') {
1398
+ statusDot.className = 'worksona-status-dot';
1399
+ continue;
1400
+ }
1401
+
1402
+ try {
1403
+ // Simple validation for each provider based on key format
1404
+ let isValid = false;
1405
+
1406
+ switch (provider) {
1407
+ case 'openai':
1408
+ isValid = this.options.apiKeys.openai && this.options.apiKeys.openai.startsWith('sk-');
1409
+ break;
1410
+ case 'anthropic':
1411
+ isValid = this.options.apiKeys.anthropic && this.options.apiKeys.anthropic.startsWith('sk-ant-');
1412
+ break;
1413
+ case 'google':
1414
+ isValid = this.options.apiKeys.google && this.options.apiKeys.google.length > 10;
1415
+ break;
1416
+ }
1417
+
1418
+ statusDot.className = isValid ? 'worksona-status-dot active' : 'worksona-status-dot';
1419
+ this._log(`Provider ${provider} validation result: ${isValid ? 'valid' : 'invalid'}`);
1420
+
1421
+ } catch (error) {
1422
+ statusDot.className = 'worksona-status-dot';
1423
+ console.error(`Error testing ${provider} connection:`, error);
1424
+ }
1425
+ }
1426
+ }
1427
+
1428
+ updateControlPanel() {
1429
+ if (!this.controlPanelId) return;
1430
+
1431
+ // Update provider status dots
1432
+ this._updateProviderStatus();
1433
+
1434
+ // Fill existing API key inputs
1435
+ Object.entries(this.options.apiKeys).forEach(([provider, key]) => {
1436
+ const input = document.getElementById(`worksona-${provider}-key`);
1437
+ if (input && key) {
1438
+ input.value = key;
1439
+ }
1440
+ });
1441
+
1442
+ // Update agent list
1443
+ this._updateAgentList();
1444
+
1445
+ // Register for agent changes
1446
+ this._setupAgentEventListeners();
1447
+
1448
+ // Ensure all event listeners are properly set up
1449
+ if (this.controlPanelContainer) {
1450
+ this._setupEventListeners(this.controlPanelContainer);
1451
+ }
1452
+ }
1453
+
1454
+ _updateProviderStatus() {
1455
+ const providers = ['openai', 'anthropic', 'google'];
1456
+
1457
+ providers.forEach(provider => {
1458
+ const statusDot = document.getElementById(`worksona-${provider}-status`);
1459
+ if (!statusDot) return;
1460
+
1461
+ const isConfigured = !!this.options.apiKeys[provider];
1462
+ statusDot.className = isConfigured ? 'worksona-status-dot active' : 'worksona-status-dot';
1463
+ });
1464
+ }
1465
+
1466
+ _updateAgentList() {
1467
+ const agentList = document.getElementById('worksona-agent-list');
1468
+ if (!agentList) return;
1469
+
1470
+ const agents = this.getAgents();
1471
+
1472
+ if (agents.length === 0) {
1473
+ agentList.innerHTML = `
1474
+ <div class="worksona-no-agents">
1475
+ <p>No agents have been loaded yet.</p>
1476
+ <p>Agents will appear here when they are loaded by your application.</p>
1477
+ </div>
1478
+ `;
1479
+ return;
1480
+ }
1481
+
1482
+ agentList.innerHTML = agents.map(agent => {
1483
+ const metrics = agent.getMetrics();
1484
+ const state = agent.getState();
1485
+ const history = agent.getHistory();
1486
+
1487
+ // Format history items
1488
+ const historyHtml = history.length > 0
1489
+ ? history.slice(-5).map(item => `
1490
+ <div class="worksona-history-item">
1491
+ <div class="worksona-history-time">${new Date(item.timestamp).toLocaleTimeString()}</div>
1492
+ <div class="worksona-history-query"><strong>Query:</strong> ${this._escapeHtml(item.query)}</div>
1493
+ <div class="worksona-history-response"><strong>Response:</strong> ${this._escapeHtml(item.response || 'Error: ' + (item.error?.message || 'Unknown error'))}</div>
1494
+ <div class="worksona-history-meta">
1495
+ <span>Provider: ${item.provider}</span> |
1496
+ <span>Model: ${item.model}</span> |
1497
+ <span>Duration: ${item.duration}ms</span>
1498
+ </div>
1499
+ </div>
1500
+ `).join('')
1501
+ : '<p>No interaction history available yet.</p>';
1502
+
1503
+ // Get system prompt and examples - ensure we're getting them from the right places with thorough checks
1504
+ const getSystemPrompt = (agent) => {
1505
+ // Check all possible locations for system prompt
1506
+ return agent.systemPrompt ||
1507
+ agent.config?.systemPrompt ||
1508
+ (agent.config?.config?.systemPrompt) ||
1509
+ '';
1510
+ };
1511
+
1512
+ const getExamples = (agent) => {
1513
+ // Check all possible locations for examples, including deeper nesting
1514
+ const examples = agent.examples ||
1515
+ agent.config?.examples ||
1516
+ agent.config?.config?.examples ||
1517
+ [];
1518
+ console.log('Agent:', agent.id, 'Examples:', JSON.stringify(examples));
1519
+ return examples;
1520
+ };
1521
+
1522
+ const systemPrompt = getSystemPrompt(agent);
1523
+ const promptDisplay = systemPrompt
1524
+ ? `<div class="worksona-prompt-box">${this._escapeHtml(systemPrompt)}</div>`
1525
+ : `<p class="worksona-no-examples">No system prompt has been defined for this agent. A system prompt helps establish the agent's behavior and capabilities.</p>`;
1526
+
1527
+ const examples = getExamples(agent);
1528
+
1529
+ const examplesHtml = examples && examples.length > 0
1530
+ ? examples.map(ex => `
1531
+ <div class="worksona-example">
1532
+ <div class="worksona-example-user"><strong>User:</strong> ${this._escapeHtml(ex.user || '')}</div>
1533
+ <div class="worksona-example-assistant"><strong>Assistant:</strong> ${this._escapeHtml(ex.assistant || '')}</div>
1534
+ </div>
1535
+ `).join('')
1536
+ : '<p class="worksona-no-examples">No examples have been defined for this agent. Examples help demonstrate the expected conversation flow.</p>';
1537
+
1538
+ // Format configuration details - ensure we're showing all available config settings with proper null checks
1539
+ const configDetails = `
1540
+ <div class="worksona-config-details">
1541
+ <h4>Model Settings</h4>
1542
+ <div><strong>Provider:</strong> ${agent.config?.provider || agent.state?.currentProvider || 'default'}</div>
1543
+ <div><strong>Model:</strong> ${agent.config?.model || agent.state?.currentModel || 'default'}</div>
1544
+ <div><strong>Temperature:</strong> ${agent.config?.temperature !== undefined ? agent.config.temperature : (agent.config?.config?.temperature !== undefined ? agent.config.config.temperature : 'default')}</div>
1545
+ <div><strong>Max Tokens:</strong> ${agent.config?.maxTokens !== undefined ? agent.config.maxTokens : (agent.config?.config?.maxTokens !== undefined ? agent.config.config.maxTokens : 'default')}</div>
1546
+ ${agent.config?.topP !== undefined || agent.config?.config?.topP !== undefined ? `<div><strong>Top P:</strong> ${agent.config?.topP !== undefined ? agent.config.topP : agent.config?.config?.topP}</div>` : ''}
1547
+ ${agent.config?.frequencyPenalty !== undefined || agent.config?.config?.frequencyPenalty !== undefined ? `<div><strong>Frequency Penalty:</strong> ${agent.config?.frequencyPenalty !== undefined ? agent.config.frequencyPenalty : agent.config?.config?.frequencyPenalty}</div>` : ''}
1548
+ ${agent.config?.presencePenalty !== undefined || agent.config?.config?.presencePenalty !== undefined ? `<div><strong>Presence Penalty:</strong> ${agent.config?.presencePenalty !== undefined ? agent.config.presencePenalty : agent.config?.config?.presencePenalty}</div>` : ''}
1549
+ </div>
1550
+ `;
1551
+
1552
+ // Format JSON for this specific agent with complete and properly formatted information
1553
+ const agentJson = JSON.stringify({
1554
+ id: agent.id,
1555
+ name: agent.name,
1556
+ description: agent.description,
1557
+ config: {
1558
+ provider: agent.config.provider,
1559
+ model: agent.config.model,
1560
+ temperature: agent.config.temperature,
1561
+ maxTokens: agent.config.maxTokens,
1562
+ topP: agent.config.topP,
1563
+ frequencyPenalty: agent.config.frequencyPenalty,
1564
+ presencePenalty: agent.config.presencePenalty,
1565
+ },
1566
+ systemPrompt: getSystemPrompt(agent),
1567
+ examples: getExamples(agent),
1568
+ traits: agent.traits || {},
1569
+ metrics: agent.getMetrics(),
1570
+ state: agent.getState(),
1571
+ transactions: agent.getHistory().slice(-5)
1572
+ }, null, 2);
1573
+
1574
+ // Trait information
1575
+ const traits = agent.traits || {};
1576
+ const traitsHtml = `
1577
+ <div class="worksona-traits">
1578
+ ${traits.personality ? `<div><strong>Personality:</strong> ${traits.personality.join(', ')}</div>` : ''}
1579
+ ${traits.knowledge ? `<div><strong>Knowledge:</strong> ${traits.knowledge.join(', ')}</div>` : ''}
1580
+ ${traits.tone ? `<div><strong>Tone:</strong> ${traits.tone}</div>` : ''}
1581
+ ${traits.background ? `<div><strong>Background:</strong> ${traits.background}</div>` : ''}
1582
+ </div>
1583
+ `;
1584
+
1585
+ return `
1586
+ <div class="worksona-agent-card">
1587
+ <div class="worksona-agent-header" onclick="document.getElementById('worksona-agent-details-${agent.id}').classList.toggle('active'); this.querySelector('.worksona-expand-icon').classList.toggle('rotated')">
1588
+ <h3 class="worksona-agent-name">${agent.name}</h3>
1589
+ <div class="worksona-agent-status ${state.isActive ? 'active' : 'inactive'}">
1590
+ <span class="worksona-agent-id">${agent.id}</span>
1591
+ <span class="worksona-status-label">${state.isActive ? 'Active' : 'Inactive'}</span>
1592
+ </div>
1593
+ <div class="worksona-expand-icon">▼</div>
1594
+ </div>
1595
+ <p class="worksona-agent-description">${agent.description || 'No description provided'}</p>
1596
+
1597
+ <div id="worksona-agent-details-${agent.id}" class="worksona-agent-details">
1598
+ <div class="worksona-agent-tabs">
1599
+ <button class="worksona-agent-tab active" data-agent="${agent.id}" data-tab="agent">Agent Details</button>
1600
+ <button class="worksona-agent-tab" data-agent="${agent.id}" data-tab="model">Model Settings</button>
1601
+ <button class="worksona-agent-tab" data-agent="${agent.id}" data-tab="prompt">System Prompt</button>
1602
+ <button class="worksona-agent-tab" data-agent="${agent.id}" data-tab="history">History</button>
1603
+ <button class="worksona-agent-tab" data-agent="${agent.id}" data-tab="json">Raw JSON</button>
1604
+ </div>
1605
+
1606
+ <div class="worksona-agent-tab-content active" id="worksona-agent-${agent.id}-agent">
1607
+ <h4>Agent Configuration</h4>
1608
+ <div class="worksona-config-details">
1609
+ <div><strong>ID:</strong> ${agent.id}</div>
1610
+ <div><strong>Name:</strong> ${agent.name}</div>
1611
+ <div><strong>Description:</strong> ${agent.description || 'No description provided'}</div>
1612
+ </div>
1613
+ <h4>Traits</h4>
1614
+ ${traitsHtml}
1615
+ </div>
1616
+
1617
+ <div class="worksona-agent-tab-content" id="worksona-agent-${agent.id}-model">
1618
+ <h4>Model Settings</h4>
1619
+ ${configDetails}
1620
+ </div>
1621
+
1622
+ <div class="worksona-agent-tab-content" id="worksona-agent-${agent.id}-prompt">
1623
+ <h4>System Prompt</h4>
1624
+ ${promptDisplay}
1625
+
1626
+ <h4>Examples</h4>
1627
+ <div class="worksona-examples">
1628
+ ${examplesHtml}
1629
+ </div>
1630
+ </div>
1631
+
1632
+ <div class="worksona-agent-tab-content" id="worksona-agent-${agent.id}-history">
1633
+ <h4>Recent Interactions</h4>
1634
+ <div class="worksona-history">
1635
+ ${historyHtml}
1636
+ </div>
1637
+ <div class="worksona-metrics">
1638
+ <div><strong>Total Queries:</strong> ${metrics.totalQueries}</div>
1639
+ <div><strong>Avg Response Time:</strong> ${Math.round(metrics.avgResponseTime)}ms</div>
1640
+ <div><strong>Success Rate:</strong> ${(metrics.successRate * 100).toFixed(1)}%</div>
1641
+ <div><strong>Last Active:</strong> ${metrics.lastActive ? new Date(metrics.lastActive).toLocaleTimeString() : 'Never'}</div>
1642
+ </div>
1643
+ </div>
1644
+
1645
+ <div class="worksona-agent-tab-content" id="worksona-agent-${agent.id}-json">
1646
+ <h4>Agent Configuration JSON</h4>
1647
+ <div class="worksona-json-display">${this._escapeHtml(agentJson)}</div>
1648
+ </div>
1649
+ </div>
1650
+ </div>
1651
+ `;
1652
+ }).join('');
1653
+
1654
+ // Remove any existing agent tab event listeners before adding new ones
1655
+ if (this.agentTabEventListeners) {
1656
+ this.agentTabEventListeners.forEach(({ element, type, listener }) => {
1657
+ element.removeEventListener(type, listener);
1658
+ });
1659
+ }
1660
+
1661
+ // Initialize array to store new event listeners
1662
+ this.agentTabEventListeners = [];
1663
+
1664
+ // Add click handlers for agent tabs
1665
+ agentList.querySelectorAll('.worksona-agent-tab').forEach(tab => {
1666
+ const clickHandler = (e) => {
1667
+ const agentId = tab.dataset.agent;
1668
+ const tabName = tab.dataset.tab;
1669
+
1670
+ // Deactivate all tabs for this agent
1671
+ document.querySelectorAll(`.worksona-agent-tab[data-agent="${agentId}"]`).forEach(t => {
1672
+ t.classList.remove('active');
1673
+ });
1674
+
1675
+ // Deactivate all tab contents for this agent
1676
+ document.querySelectorAll(`[id^="worksona-agent-${agentId}-"]`).forEach(c => {
1677
+ c.classList.remove('active');
1678
+ });
1679
+
1680
+ // Activate clicked tab and content
1681
+ tab.classList.add('active');
1682
+ document.getElementById(`worksona-agent-${agentId}-${tabName}`).classList.add('active');
1683
+ };
1684
+
1685
+ // Store reference to event listener
1686
+ this.agentTabEventListeners.push({
1687
+ element: tab,
1688
+ type: 'click',
1689
+ listener: clickHandler
1690
+ });
1691
+
1692
+ // Add the event listener
1693
+ tab.addEventListener('click', clickHandler);
1694
+ });
1695
+ }
1696
+
1697
+ // Helper to escape HTML for safe display
1698
+ _escapeHtml(unsafe) {
1699
+ if (!unsafe) return '';
1700
+ return unsafe
1701
+ .toString()
1702
+ .replace(/&/g, "&amp;")
1703
+ .replace(/</g, "&lt;")
1704
+ .replace(/>/g, "&gt;")
1705
+ .replace(/"/g, "&quot;")
1706
+ .replace(/'/g, "&#039;");
1707
+ }
1708
+
1709
+ _setupAgentEventListeners() {
1710
+ // Remove existing listeners to avoid duplicates
1711
+ this.off('agent-loaded', this._handleAgentLoaded);
1712
+ this.off('agent-removed', this._handleAgentRemoved);
1713
+
1714
+ // Add listeners for agent changes
1715
+ this.on('agent-loaded', this._handleAgentLoaded.bind(this));
1716
+ this.on('agent-removed', this._handleAgentRemoved.bind(this));
1717
+ }
1718
+
1719
+ _handleAgentLoaded() {
1720
+ this._updateAgentList();
1721
+ }
1722
+
1723
+ _handleAgentRemoved() {
1724
+ this._updateAgentList();
1725
+ }
1726
+
1727
+ // Get all loaded agents
1728
+ getAgents() {
1729
+ return Array.from(this.agents.values());
1730
+ }
1731
+
1732
+ // Create a floating control panel with button in the bottom right
1733
+ createFloatingControlPanel() {
1734
+ // Ensure DOM is ready
1735
+ if (!document.body) {
1736
+ // Wait for DOM to be ready
1737
+ if (document.readyState === 'loading') {
1738
+ document.addEventListener('DOMContentLoaded', () => this.createFloatingControlPanel());
1739
+ return;
1740
+ } else {
1741
+ // Fallback: wait a bit and try again
1742
+ setTimeout(() => this.createFloatingControlPanel(), 100);
1743
+ return;
1744
+ }
1745
+ }
1746
+
1747
+ try {
1748
+ // Create a wrapper for the button and panel
1749
+ const wrapper = document.createElement('div');
1750
+ wrapper.id = 'worksona-floating-control';
1751
+
1752
+ // Create floating button
1753
+ const button = document.createElement('button');
1754
+ button.id = 'worksona-floating-button';
1755
+ button.innerHTML = '⚙️';
1756
+ button.setAttribute('aria-label', 'Open Worksona Control Panel');
1757
+ wrapper.appendChild(button);
1758
+
1759
+ // Create control panel container
1760
+ const panelContainer = document.createElement('div');
1761
+ panelContainer.id = 'worksona-modal-container';
1762
+
1763
+ // Create overlay
1764
+ const overlay = document.createElement('div');
1765
+ overlay.id = 'worksona-overlay';
1766
+
1767
+ // Add panel and overlay to wrapper
1768
+ wrapper.appendChild(overlay);
1769
+ wrapper.appendChild(panelContainer);
1770
+
1771
+ // Append wrapper to body first to ensure all elements are in DOM
1772
+ document.body.appendChild(wrapper);
1773
+
1774
+ // Small delay to ensure DOM is updated, then set up the control panel
1775
+ setTimeout(() => {
1776
+ this.createControlPanel('worksona-modal-container');
1777
+
1778
+ // Verify the control panel was created successfully
1779
+ const controlPanel = panelContainer.querySelector('.worksona-control-panel');
1780
+ if (!controlPanel) {
1781
+ this._log('Failed to create control panel content, retrying...', 'warn');
1782
+ // Retry after a short delay
1783
+ setTimeout(() => {
1784
+ this.createControlPanel('worksona-modal-container');
1785
+ }, 100);
1786
+ }
1787
+ }, 10);
1788
+
1789
+ // Helper function to close the panel
1790
+ const closePanel = () => {
1791
+ overlay.classList.remove('active');
1792
+ panelContainer.classList.remove('active');
1793
+ const panel = panelContainer.querySelector('.worksona-control-panel');
1794
+ if (panel) {
1795
+ panel.style.display = 'none';
1796
+ }
1797
+ };
1798
+
1799
+ // Add styles for floating button and modal
1800
+ const floatingStyles = document.createElement('style');
1801
+ floatingStyles.textContent = `
1802
+ #worksona-floating-button {
1803
+ position: fixed;
1804
+ bottom: 20px;
1805
+ right: 20px;
1806
+ width: 50px;
1807
+ height: 50px;
1808
+ border-radius: 50%;
1809
+ background: #2563eb;
1810
+ color: white;
1811
+ border: none;
1812
+ font-size: 24px;
1813
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
1814
+ cursor: pointer;
1815
+ z-index: 9998;
1816
+ display: flex;
1817
+ align-items: center;
1818
+ justify-content: center;
1819
+ transition: all 0.2s ease-in-out;
1820
+ }
1821
+
1822
+ #worksona-floating-button:hover {
1823
+ transform: scale(1.1);
1824
+ background: #1d4ed8;
1825
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
1826
+ }
1827
+
1828
+ #worksona-floating-button:active {
1829
+ transform: scale(0.95);
1830
+ }
1831
+
1832
+ @media (max-width: 768px) {
1833
+ #worksona-floating-button {
1834
+ width: 45px;
1835
+ height: 45px;
1836
+ font-size: 20px;
1837
+ bottom: 15px;
1838
+ right: 15px;
1839
+ }
1840
+ }
1841
+
1842
+ #worksona-modal-container {
1843
+ position: fixed;
1844
+ top: 50%;
1845
+ left: 50%;
1846
+ transform: translate(-50%, -50%);
1847
+ z-index: 10000;
1848
+ width: 90%;
1849
+ max-width: 800px;
1850
+ max-height: 80vh;
1851
+ overflow: auto;
1852
+ display: none;
1853
+ background: white;
1854
+ border-radius: 8px;
1855
+ }
1856
+
1857
+ #worksona-modal-container.active {
1858
+ display: block;
1859
+ }
1860
+
1861
+ #worksona-overlay {
1862
+ position: fixed;
1863
+ top: 0;
1864
+ left: 0;
1865
+ width: 100%;
1866
+ height: 100%;
1867
+ background: rgba(0,0,0,0.5);
1868
+ z-index: 9999;
1869
+ display: none;
1870
+ }
1871
+
1872
+ #worksona-overlay.active {
1873
+ display: block;
1874
+ }
1875
+
1876
+ .worksona-control-panel {
1877
+ box-shadow: 0 5px 20px rgba(0,0,0,0.2);
1878
+ max-height: 80vh;
1879
+ overflow: auto;
1880
+ background: white;
1881
+ border-radius: 8px;
1882
+ transition: all 0.3s ease;
1883
+ }
1884
+
1885
+ .worksona-panel-header {
1886
+ display: flex;
1887
+ justify-content: space-between;
1888
+ align-items: center;
1889
+ padding: 15px 20px;
1890
+ border-bottom: 1px solid #eee;
1891
+ background: #fff;
1892
+ position: sticky;
1893
+ top: 0;
1894
+ z-index: 1;
1895
+ }
1896
+
1897
+ .worksona-header-buttons {
1898
+ display: flex;
1899
+ gap: 10px;
1900
+ align-items: center;
1901
+ }
1902
+
1903
+ .worksona-expand-button {
1904
+ background: none;
1905
+ border: none;
1906
+ font-size: 20px;
1907
+ cursor: pointer;
1908
+ color: #64748b;
1909
+ padding: 4px;
1910
+ display: flex;
1911
+ align-items: center;
1912
+ justify-content: center;
1913
+ transition: color 0.2s;
1914
+ }
1915
+
1916
+ .worksona-expand-button:hover {
1917
+ color: #334155;
1918
+ }
1919
+
1920
+ .worksona-control-panel.expanded {
1921
+ position: fixed;
1922
+ top: 0;
1923
+ left: 0;
1924
+ width: 100% !important;
1925
+ height: 100vh !important;
1926
+ max-height: 100vh !important;
1927
+ max-width: 100% !important;
1928
+ border-radius: 0;
1929
+ z-index: 10001;
1930
+ }
1931
+
1932
+ #worksona-modal-container.expanded {
1933
+ width: 100%;
1934
+ max-width: 100%;
1935
+ height: 100vh;
1936
+ max-height: 100vh;
1937
+ top: 0;
1938
+ left: 0;
1939
+ transform: none;
1940
+ border-radius: 0;
1941
+ }
1942
+ `;
1943
+ document.head.appendChild(floatingStyles);
1944
+
1945
+ // Set up additional event listeners specific to floating panel
1946
+ button.addEventListener('click', () => {
1947
+ overlay.classList.add('active');
1948
+ panelContainer.classList.add('active');
1949
+ // Ensure the control panel is visible
1950
+ const panel = panelContainer.querySelector('.worksona-control-panel');
1951
+ if (panel) {
1952
+ panel.style.display = 'block';
1953
+ } else {
1954
+ // If panel doesn't exist, try to recreate it
1955
+ this._log('Control panel not found, attempting to recreate', 'warn');
1956
+ const success = this.createControlPanel('worksona-modal-container');
1957
+ if (success !== false) {
1958
+ // Wait a moment for DOM to update, then try again
1959
+ setTimeout(() => {
1960
+ const newPanel = panelContainer.querySelector('.worksona-control-panel');
1961
+ if (newPanel) {
1962
+ newPanel.style.display = 'block';
1963
+ this._log('Control panel recreated successfully', 'info');
1964
+ } else {
1965
+ this._log('Failed to recreate control panel - panel element not found after creation', 'error');
1966
+ // Try to update the control panel content
1967
+ this.updateControlPanel();
1968
+ }
1969
+ }, 50);
1970
+ } else {
1971
+ this._log('Failed to recreate control panel - container not found', 'error');
1972
+ }
1973
+ }
1974
+ });
1975
+
1976
+ // Add expand/contract functionality
1977
+ const expandButton = panelContainer.querySelector('.worksona-expand-button');
1978
+ if (expandButton) {
1979
+ expandButton.addEventListener('click', () => {
1980
+ const panel = panelContainer.querySelector('.worksona-control-panel');
1981
+ const isExpanded = panel.classList.contains('expanded');
1982
+
1983
+ if (isExpanded) {
1984
+ panel.classList.remove('expanded');
1985
+ panelContainer.classList.remove('expanded');
1986
+ expandButton.innerHTML = '⛶'; // Expand icon
1987
+ expandButton.title = 'Expand to full screen';
1988
+ } else {
1989
+ panel.classList.add('expanded');
1990
+ panelContainer.classList.add('expanded');
1991
+ expandButton.innerHTML = '⛾'; // Contract icon
1992
+ expandButton.title = 'Return to normal size';
1993
+ }
1994
+ });
1995
+ }
1996
+
1997
+ // Close panel when clicking overlay
1998
+ overlay.addEventListener('click', closePanel);
1999
+
2000
+ // Close panel when clicking close button
2001
+ const closeButton = panelContainer.querySelector('.worksona-close-button');
2002
+ if (closeButton) {
2003
+ closeButton.addEventListener('click', (e) => {
2004
+ e.preventDefault();
2005
+ e.stopPropagation();
2006
+ closePanel();
2007
+ });
2008
+ }
2009
+
2010
+ // Add keyboard event listener for Escape key
2011
+ document.addEventListener('keydown', (e) => {
2012
+ if (e.key === 'Escape' && overlay.classList.contains('active')) {
2013
+ closePanel();
2014
+ }
2015
+ });
2016
+
2017
+ } catch (error) {
2018
+ this._log(`Error creating floating control panel: ${error.message}`, 'error');
2019
+ // Retry after a delay
2020
+ setTimeout(() => this.createFloatingControlPanel(), 1000);
2021
+ }
2022
+ }
2023
+
2024
+ // Add method to check if control panel is ready
2025
+ isControlPanelReady() {
2026
+ const container = document.getElementById(this.controlPanelId);
2027
+ const panel = container?.querySelector('.worksona-control-panel');
2028
+ return !!(container && panel);
2029
+ }
2030
+
2031
+ /**
2032
+ * Process (analyze) an image using the agent's provider (gpt-4o for OpenAI)
2033
+ */
2034
+ async processImage(agentId, imageData, options = {}) {
2035
+ const agent = this.agents.get(agentId);
2036
+ if (!agent) {
2037
+ this._handleError(new Error(`Agent not found: ${agentId}`), 'AGENT_NOT_FOUND');
2038
+ return null;
2039
+ }
2040
+ const provider = agent.config.provider || this.options.defaultProvider;
2041
+ this._emit('image-analysis-start', { agentId, provider, imageData, options });
2042
+ try {
2043
+ if (provider === 'openai') {
2044
+ // Use OpenAI GPT-4o for vision analysis
2045
+ const modelName = (agent.config.model || 'gpt-4o').trim();
2046
+ const messages = [
2047
+ { role: 'system', content: agent.config.systemPrompt || 'You are a helpful vision analysis assistant.' },
2048
+ { role: 'user', content: [
2049
+ { type: 'text', text: options.prompt || 'Please analyze this image.' },
2050
+ { type: 'image_url', image_url: { url: imageData, detail: options.detail || 'high' } }
2051
+ ] }
2052
+ ];
2053
+ const requestBody = {
2054
+ model: modelName,
2055
+ messages,
2056
+ temperature: agent.config.temperature || 0.7,
2057
+ max_tokens: agent.config.maxTokens || 500,
2058
+ top_p: agent.config.topP || 1,
2059
+ frequency_penalty: agent.config.frequencyPenalty || 0,
2060
+ presence_penalty: agent.config.presencePenalty || 0,
2061
+ stream: false
2062
+ };
2063
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
2064
+ method: 'POST',
2065
+ headers: {
2066
+ 'Content-Type': 'application/json',
2067
+ 'Authorization': `Bearer ${this.options.apiKeys.openai}`
2068
+ },
2069
+ body: JSON.stringify(requestBody)
2070
+ });
2071
+ const data = await response.json();
2072
+ if (!response.ok) throw new Error(data.error?.message || 'OpenAI image analysis error');
2073
+ this._emit('image-analysis-complete', { agentId, provider, imageData, result: data });
2074
+ return data.choices[0].message.content;
2075
+ } else {
2076
+ throw new Error(`Provider ${provider} does not support image analysis`);
2077
+ }
2078
+ } catch (error) {
2079
+ this._emit('image-processing-error', { agentId, error });
2080
+ this._handleError(error, 'IMAGE_PROCESSING_ERROR', 'Failed to analyze image');
2081
+ return null;
2082
+ }
2083
+ }
2084
+
2085
+ // Alias method for analyzeImage that calls processImage
2086
+ async analyzeImage(agentId, imageData, options = {}) {
2087
+ return this.processImage(agentId, imageData, options);
2088
+ }
2089
+
2090
+ /**
2091
+ * Generate an image from a text prompt using the agent's provider (OpenAI DALL-E/gpt-4o)
2092
+ * Supports models: 'dalle-3', 'gpt-4o', 'gpt-4o-vision-preview'.
2093
+ * Model selection order: options.model > agent.config.model > 'dalle-3'
2094
+ */
2095
+ async generateImage(agentId, prompt, options = {}) {
2096
+ const agent = this.agents.get(agentId);
2097
+ if (!agent) {
2098
+ this._handleError(new Error(`Agent not found: ${agentId}`), 'AGENT_NOT_FOUND');
2099
+ return null;
2100
+ }
2101
+ const provider = agent.config.provider || this.options.defaultProvider;
2102
+ this._emit('image-generation-start', { agentId, provider, prompt, options });
2103
+ try {
2104
+ if (provider === 'openai') {
2105
+ // Model selection logic
2106
+ const modelName = (options.model || agent.config.imageGenerationModel || 'dall-e-3').trim();
2107
+ // DALL-E endpoint supports model param for dalle-3
2108
+ const requestBody = {
2109
+ prompt,
2110
+ n: options.n || 1,
2111
+ size: options.size || '1024x1024',
2112
+ response_format: options.response_format || 'url',
2113
+ user: agentId,
2114
+ model: modelName
2115
+ };
2116
+ const response = await fetch('https://api.openai.com/v1/images/generations', {
2117
+ method: 'POST',
2118
+ headers: {
2119
+ 'Content-Type': 'application/json',
2120
+ 'Authorization': `Bearer ${this.options.apiKeys.openai}`
2121
+ },
2122
+ body: JSON.stringify(requestBody)
2123
+ });
2124
+ const data = await response.json();
2125
+ if (!response.ok) throw new Error(data.error?.message || 'OpenAI image generation error');
2126
+ this._emit('image-generation-complete', { agentId, provider, prompt, result: data });
2127
+ return data.data[0].url;
2128
+ } else {
2129
+ throw new Error(`Provider ${provider} does not support image generation`);
2130
+ }
2131
+ } catch (error) {
2132
+ this._emit('image-generation-error', { agentId, error });
2133
+ this._handleError(error, 'IMAGE_GENERATION_ERROR', 'Failed to generate image');
2134
+ return null;
2135
+ }
2136
+ }
2137
+
2138
+ /**
2139
+ * Edit an image based on a prompt using the agent's provider (OpenAI DALL-E)
2140
+ * Supports models: 'dalle-3', 'gpt-4o', 'gpt-4o-vision-preview'.
2141
+ * Model selection order: options.model > agent.config.model > 'dalle-3'
2142
+ */
2143
+ async editImage(agentId, imageData, prompt, options = {}) {
2144
+ const agent = this.agents.get(agentId);
2145
+ if (!agent) {
2146
+ this._handleError(new Error(`Agent not found: ${agentId}`), 'AGENT_NOT_FOUND');
2147
+ return null;
2148
+ }
2149
+ const provider = agent.config.provider || this.options.defaultProvider;
2150
+ this._emit('image-edit-start', { agentId, provider, prompt, options });
2151
+ try {
2152
+ if (provider === 'openai') {
2153
+ // Model selection logic
2154
+ const modelName = (options.model || agent.config.model || 'dalle-3').trim();
2155
+ const requestBody = {
2156
+ image: imageData,
2157
+ prompt,
2158
+ n: options.n || 1,
2159
+ size: options.size || '1024x1024',
2160
+ response_format: options.response_format || 'url',
2161
+ user: agentId,
2162
+ model: modelName
2163
+ };
2164
+ const response = await fetch('https://api.openai.com/v1/images/edits', {
2165
+ method: 'POST',
2166
+ headers: {
2167
+ 'Content-Type': 'application/json',
2168
+ 'Authorization': `Bearer ${this.options.apiKeys.openai}`
2169
+ },
2170
+ body: JSON.stringify(requestBody)
2171
+ });
2172
+ const data = await response.json();
2173
+ if (!response.ok) throw new Error(data.error?.message || 'OpenAI image edit error');
2174
+ this._emit('image-edit-complete', { agentId, provider, prompt, result: data });
2175
+ return data.data[0].url;
2176
+ } else {
2177
+ throw new Error(`Provider ${provider} does not support image editing`);
2178
+ }
2179
+ } catch (error) {
2180
+ this._emit('image-edit-error', { agentId, error });
2181
+ this._handleError(error, 'IMAGE_EDIT_ERROR', 'Failed to edit image');
2182
+ return null;
2183
+ }
2184
+ }
2185
+
2186
+ /**
2187
+ * Create a variation of an image using the agent's provider (OpenAI DALL-E)
2188
+ * Supports models: 'dalle-3', 'gpt-4o', 'gpt-4o-vision-preview'.
2189
+ * Model selection order: options.model > agent.config.model > 'dalle-3'
2190
+ */
2191
+ async variationImage(agentId, imageData, options = {}) {
2192
+ const agent = this.agents.get(agentId);
2193
+ if (!agent) {
2194
+ this._handleError(new Error(`Agent not found: ${agentId}`), 'AGENT_NOT_FOUND');
2195
+ return null;
2196
+ }
2197
+ const provider = agent.config.provider || this.options.defaultProvider;
2198
+ this._emit('image-variation-start', { agentId, provider, options });
2199
+ try {
2200
+ if (provider === 'openai') {
2201
+ // Model selection logic
2202
+ const modelName = (options.model || agent.config.model || 'dalle-3').trim();
2203
+ const requestBody = {
2204
+ image: imageData,
2205
+ n: options.n || 1,
2206
+ size: options.size || '1024x1024',
2207
+ response_format: options.response_format || 'url',
2208
+ user: agentId,
2209
+ model: modelName
2210
+ };
2211
+ const response = await fetch('https://api.openai.com/v1/images/variations', {
2212
+ method: 'POST',
2213
+ headers: {
2214
+ 'Content-Type': 'application/json',
2215
+ 'Authorization': `Bearer ${this.options.apiKeys.openai}`
2216
+ },
2217
+ body: JSON.stringify(requestBody)
2218
+ });
2219
+ const data = await response.json();
2220
+ if (!response.ok) throw new Error(data.error?.message || 'OpenAI image variation error');
2221
+ this._emit('image-variation-complete', { agentId, provider, result: data });
2222
+ return data.data[0].url;
2223
+ } else {
2224
+ throw new Error(`Provider ${provider} does not support image variation`);
2225
+ }
2226
+ } catch (error) {
2227
+ this._emit('image-variation-error', { agentId, error });
2228
+ this._handleError(error, 'IMAGE_VARIATION_ERROR', 'Failed to create image variation');
2229
+ return null;
2230
+ }
2231
+ }
2232
+ }
2233
+
2234
+ // Export to global scope
2235
+ if (typeof module !== 'undefined' && module.exports) {
2236
+ module.exports = Worksona;
2237
+ } else {
2238
+ global.Worksona = Worksona;
2239
+ }
2240
+
2241
+ })(typeof window !== 'undefined' ? window : global);