nowaikit-utils 1.1.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/LICENSE +21 -0
- package/README.md +152 -0
- package/ai-window.html +598 -0
- package/background/service-worker.js +398 -0
- package/cli.mjs +65 -0
- package/content/ai-sidebar.js +1198 -0
- package/content/code-templates.js +843 -0
- package/content/content.js +2527 -0
- package/content/integration-bridge.js +627 -0
- package/content/main-panel.js +592 -0
- package/content/styles.css +1609 -0
- package/icons/README.txt +1 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-16.png +0 -0
- package/icons/icon-48.png +0 -0
- package/icons/icon.svg +16 -0
- package/manifest.json +63 -0
- package/options/options.html +434 -0
- package/package.json +49 -0
- package/popup/popup.html +663 -0
- package/popup/popup.js +414 -0
|
@@ -0,0 +1,1198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NowAIKit Utils — AI Sidebar
|
|
3
|
+
* BYOK AI assistant for ServiceNow. Context-aware chat with quick actions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ─── Shared Helpers (from content.js via window globals) ─────────
|
|
7
|
+
function _showToast(msg, type) {
|
|
8
|
+
if (typeof window.showToast === 'function') window.showToast(msg, type);
|
|
9
|
+
}
|
|
10
|
+
function _escapeHtml(str) {
|
|
11
|
+
if (typeof window.escapeHtml === 'function') return window.escapeHtml(str);
|
|
12
|
+
var div = document.createElement('div');
|
|
13
|
+
div.textContent = str;
|
|
14
|
+
return div.innerHTML;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── API Key Encryption ─────────────────────────────────────────
|
|
18
|
+
// Encrypts API keys before storing in chrome.storage.local using AES-GCM.
|
|
19
|
+
// Key is derived from the extension ID via PBKDF2 — prevents casual snooping
|
|
20
|
+
// of stored credentials while remaining transparent to the user.
|
|
21
|
+
|
|
22
|
+
async function _deriveEncryptionKey() {
|
|
23
|
+
var keyMaterial = await crypto.subtle.importKey(
|
|
24
|
+
'raw',
|
|
25
|
+
new TextEncoder().encode(chrome.runtime.id || 'nowaikit-utils'),
|
|
26
|
+
'PBKDF2',
|
|
27
|
+
false,
|
|
28
|
+
['deriveKey']
|
|
29
|
+
);
|
|
30
|
+
return crypto.subtle.deriveKey(
|
|
31
|
+
{ name: 'PBKDF2', salt: new TextEncoder().encode('nowaikit-utils-v1'), iterations: 100000, hash: 'SHA-256' },
|
|
32
|
+
keyMaterial,
|
|
33
|
+
{ name: 'AES-GCM', length: 256 },
|
|
34
|
+
false,
|
|
35
|
+
['encrypt', 'decrypt']
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function encryptApiKey(plaintext) {
|
|
40
|
+
if (!plaintext) return '';
|
|
41
|
+
try {
|
|
42
|
+
var key = await _deriveEncryptionKey();
|
|
43
|
+
var iv = crypto.getRandomValues(new Uint8Array(12));
|
|
44
|
+
var encrypted = await crypto.subtle.encrypt(
|
|
45
|
+
{ name: 'AES-GCM', iv: iv },
|
|
46
|
+
key,
|
|
47
|
+
new TextEncoder().encode(plaintext)
|
|
48
|
+
);
|
|
49
|
+
var combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
50
|
+
combined.set(iv);
|
|
51
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
52
|
+
return 'enc:' + btoa(String.fromCharCode.apply(null, combined));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// Fallback: return plaintext if crypto unavailable
|
|
55
|
+
return plaintext;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function decryptApiKey(stored) {
|
|
60
|
+
if (!stored) return '';
|
|
61
|
+
// Not encrypted (legacy or fallback)
|
|
62
|
+
if (!stored.startsWith('enc:')) return stored;
|
|
63
|
+
try {
|
|
64
|
+
var key = await _deriveEncryptionKey();
|
|
65
|
+
var combined = Uint8Array.from(atob(stored.slice(4)), function(c) { return c.charCodeAt(0); });
|
|
66
|
+
var iv = combined.slice(0, 12);
|
|
67
|
+
var ciphertext = combined.slice(12);
|
|
68
|
+
var decrypted = await crypto.subtle.decrypt(
|
|
69
|
+
{ name: 'AES-GCM', iv: iv },
|
|
70
|
+
key,
|
|
71
|
+
ciphertext
|
|
72
|
+
);
|
|
73
|
+
return new TextDecoder().decode(decrypted);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Decryption failed — corrupted or wrong extension context
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
81
|
+
let aiSidebarOpen = false;
|
|
82
|
+
let aiSidebarEl = null;
|
|
83
|
+
let aiConversation = []; // Array of { role: 'user'|'assistant', content: string }
|
|
84
|
+
let aiPort = null; // chrome.runtime port for streaming
|
|
85
|
+
let aiStreaming = false;
|
|
86
|
+
|
|
87
|
+
// ─── System Prompt ──────────────────────────────────────────────
|
|
88
|
+
const SN_SYSTEM_PROMPT = `You are NowAIKit Utils — ServiceNow AI Assistant, an expert ServiceNow platform assistant embedded directly in the user's browser.
|
|
89
|
+
|
|
90
|
+
You have deep expertise in:
|
|
91
|
+
- GlideRecord, GlideAjax, GlideSystem, GlideAggregate, GlideDateTime, GlideQuery APIs
|
|
92
|
+
- Business Rules (before, after, async), Client Scripts (onChange, onLoad, onSubmit, onCellEdit)
|
|
93
|
+
- Script Includes, UI Policies, UI Actions, UI Pages, Processors
|
|
94
|
+
- ACLs, roles, security best practices, and scoped application development
|
|
95
|
+
- Flow Designer, IntegrationHub, Scripted REST APIs
|
|
96
|
+
- Service Portal (sp_widget), Now Experience (UIB), Angular providers
|
|
97
|
+
- CMDB, ITSM, ITOM, CSM, HR, SecOps modules
|
|
98
|
+
- Performance optimization (query efficiency, indexing, caching)
|
|
99
|
+
- Upgrade best practices, technical debt identification
|
|
100
|
+
|
|
101
|
+
Context:
|
|
102
|
+
- You are running inside the user's ServiceNow browser session
|
|
103
|
+
- You can see the current page context (table, record, fields)
|
|
104
|
+
- When the user asks about "this record" or "this table", refer to the context provided
|
|
105
|
+
- Always provide code examples following ServiceNow best practices
|
|
106
|
+
- Use getValue/setValue in scoped apps, prefer GlideQuery where applicable
|
|
107
|
+
- Include null checks, setLimit, and proper error handling in generated code`;
|
|
108
|
+
|
|
109
|
+
// ─── Context Extraction ─────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function isBridgeConnected() {
|
|
112
|
+
return typeof getBridgeStatus === 'function' && getBridgeStatus().connected;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getBridgeInfo() {
|
|
116
|
+
if (!isBridgeConnected()) return null;
|
|
117
|
+
return getBridgeStatus();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function gatherPageContext() {
|
|
121
|
+
// Read shared state from content.js (exposed on window.nowaikitState)
|
|
122
|
+
var _ns = window.nowaikitState || {};
|
|
123
|
+
|
|
124
|
+
// Build a context object from the current ServiceNow page
|
|
125
|
+
const ctx = {
|
|
126
|
+
table: _ns.currentTable || '',
|
|
127
|
+
sysId: _ns.currentSysId || '',
|
|
128
|
+
instance: _ns.instanceUrl ? new URL(_ns.instanceUrl).hostname.split('.')[0] : '',
|
|
129
|
+
isForm: _ns.isForm || false,
|
|
130
|
+
isList: _ns.isList || false,
|
|
131
|
+
url: window.location.href,
|
|
132
|
+
fields: {},
|
|
133
|
+
scripts: [],
|
|
134
|
+
schemaInfo: null,
|
|
135
|
+
fullRecord: null,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Gather form field values if on a form
|
|
139
|
+
if (ctx.isForm && typeof g_form !== 'undefined') {
|
|
140
|
+
try {
|
|
141
|
+
// Get all field names from the form
|
|
142
|
+
const formFields = document.querySelectorAll('input[id^="sys_display."], input[name]:not([type="hidden"]), select[name], textarea[name]');
|
|
143
|
+
formFields.forEach(function(el) {
|
|
144
|
+
const name = el.name || el.id.replace('sys_display.', '');
|
|
145
|
+
if (name && !name.startsWith('ni.') && !name.startsWith('sys_display.')) {
|
|
146
|
+
ctx.fields[name] = (el.value || '').substring(0, 500);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// Silently fail — form may not be fully loaded
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Gather script content from any open script editors
|
|
155
|
+
try {
|
|
156
|
+
// CodeMirror instances
|
|
157
|
+
document.querySelectorAll('.CodeMirror').forEach(function(cm) {
|
|
158
|
+
if (cm.CodeMirror) {
|
|
159
|
+
const val = cm.CodeMirror.getValue();
|
|
160
|
+
if (val && val.trim().length > 0) {
|
|
161
|
+
ctx.scripts.push(val.substring(0, 5000));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// Textarea script fields
|
|
166
|
+
document.querySelectorAll('textarea[id*="script"], textarea[name="script"], textarea[name="condition"]').forEach(function(ta) {
|
|
167
|
+
if (ta.value && ta.value.trim().length > 0 && ctx.scripts.indexOf(ta.value.substring(0, 5000)) === -1) {
|
|
168
|
+
ctx.scripts.push(ta.value.substring(0, 5000));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// Silently fail
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Enrich context with bridge data when available
|
|
176
|
+
if (isBridgeConnected()) {
|
|
177
|
+
// Fetch table schema if we have a table name
|
|
178
|
+
if (ctx.table && typeof bridgeGetTableSchema === 'function') {
|
|
179
|
+
try {
|
|
180
|
+
ctx.schemaInfo = await bridgeGetTableSchema(ctx.table);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Bridge call failed — proceed with DOM-only context
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fetch full record data if we have a table and sys_id
|
|
187
|
+
if (ctx.table && ctx.sysId && typeof bridgeGetRecord === 'function') {
|
|
188
|
+
try {
|
|
189
|
+
ctx.fullRecord = await bridgeGetRecord(ctx.table, ctx.sysId);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// Bridge call failed — proceed with DOM-only context
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return ctx;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildContextMessage(ctx) {
|
|
200
|
+
// Build a context string to inject into the conversation
|
|
201
|
+
var parts = [];
|
|
202
|
+
if (ctx.instance) parts.push('Instance: ' + ctx.instance);
|
|
203
|
+
if (ctx.table) parts.push('Table: ' + ctx.table);
|
|
204
|
+
if (ctx.sysId) parts.push('sys_id: ' + ctx.sysId);
|
|
205
|
+
if (ctx.isForm) parts.push('Page type: Form');
|
|
206
|
+
if (ctx.isList) parts.push('Page type: List');
|
|
207
|
+
|
|
208
|
+
// Add table schema info from bridge (enriched context)
|
|
209
|
+
if (ctx.schemaInfo) {
|
|
210
|
+
try {
|
|
211
|
+
var columns = ctx.schemaInfo.columns || ctx.schemaInfo.fields || [];
|
|
212
|
+
var colCount = columns.length;
|
|
213
|
+
var keyColumns = columns.slice(0, 25).map(function(col) {
|
|
214
|
+
var name = col.name || col.element || col.column_label || '';
|
|
215
|
+
var type = col.type || col.internal_type || '';
|
|
216
|
+
return name + (type ? ' (' + type + ')' : '');
|
|
217
|
+
});
|
|
218
|
+
parts.push('Table Schema: ' + ctx.table + ' has ' + colCount + ' columns including: ' + keyColumns.join(', '));
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// Schema data in unexpected format — skip
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Add full record data from bridge (enriched context)
|
|
225
|
+
if (ctx.fullRecord) {
|
|
226
|
+
try {
|
|
227
|
+
var recordEntries = Object.entries(ctx.fullRecord);
|
|
228
|
+
if (recordEntries.length > 0) {
|
|
229
|
+
var recordLines = recordEntries
|
|
230
|
+
.filter(function(e) { return e[1] !== null && e[1] !== undefined && String(e[1]).length > 0; })
|
|
231
|
+
.slice(0, 40)
|
|
232
|
+
.map(function(e) { return ' ' + e[0] + ': ' + String(e[1]).substring(0, 500); });
|
|
233
|
+
if (recordLines.length > 0) {
|
|
234
|
+
parts.push('Full Record Data:\n' + recordLines.join('\n'));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// Record data in unexpected format — skip
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add key field values (limit to avoid context bloat)
|
|
243
|
+
var fieldEntries = Object.entries(ctx.fields);
|
|
244
|
+
if (fieldEntries.length > 0) {
|
|
245
|
+
var fieldLines = fieldEntries
|
|
246
|
+
.filter(function(e) { return e[1] && e[1].length > 0; })
|
|
247
|
+
.slice(0, 20)
|
|
248
|
+
.map(function(e) { return ' ' + e[0] + ': ' + e[1]; });
|
|
249
|
+
if (fieldLines.length > 0) {
|
|
250
|
+
parts.push('Fields:\n' + fieldLines.join('\n'));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add script content
|
|
255
|
+
if (ctx.scripts.length > 0) {
|
|
256
|
+
ctx.scripts.forEach(function(script, i) {
|
|
257
|
+
parts.push('Script ' + (i + 1) + ':\n```javascript\n' + script + '\n```');
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return parts.length > 0 ? '[Current Page Context]\n' + parts.join('\n') : '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── Quick Actions ──────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
const QUICK_ACTIONS = [
|
|
267
|
+
{ id: 'explain', icon: '\u{1F4A1}', label: 'Explain', prompt: 'Explain this script in detail. What does it do, what events trigger it, and are there any issues?' },
|
|
268
|
+
{ id: 'summarize', icon: '\u{1F4CB}', label: 'Summarize', prompt: 'Summarize this record. What is it about, what is its current state, and what are the key details?' },
|
|
269
|
+
{ id: 'review', icon: '\u{1F50D}', label: 'Review', prompt: 'Review this code for security issues, performance problems, and best practice violations. Provide specific fixes.' },
|
|
270
|
+
{ id: 'generate', icon: '\u26A1', label: 'Generate', prompt: null }, // opens sub-menu or input
|
|
271
|
+
{ id: 'fix', icon: '\u{1F527}', label: 'Fix Issues', prompt: 'Identify and fix all issues in this script. Return the corrected code with explanations of each change.' },
|
|
272
|
+
{ id: 'optimize', icon: '\u{1F680}', label: 'Optimize', prompt: 'Optimize this script for performance. Identify inefficient patterns and provide an optimized version.' },
|
|
273
|
+
{ id: 'query', icon: '\u{1F50D}', label: 'Query', prompt: 'Help me write a GlideRecord query for this table. Show me how to query {table} with common filters.' },
|
|
274
|
+
{ id: 'instance', icon: '\u{1F4CA}', label: 'Health', prompt: 'Show me the current instance health metrics and any issues.' },
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
// ─── Sidebar UI ─────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
var aiIsFloating = false;
|
|
280
|
+
|
|
281
|
+
function initAISidebar() {
|
|
282
|
+
// Create the sidebar element but don't show it yet
|
|
283
|
+
if (document.getElementById('nowaikit-ai-sidebar')) return;
|
|
284
|
+
|
|
285
|
+
aiSidebarEl = document.createElement('div');
|
|
286
|
+
aiSidebarEl.id = 'nowaikit-ai-sidebar';
|
|
287
|
+
aiSidebarEl.className = 'nowaikit-ai-sidebar';
|
|
288
|
+
aiSidebarEl.innerHTML = buildSidebarHTML();
|
|
289
|
+
|
|
290
|
+
// Add resize handle on left edge
|
|
291
|
+
var resizeHandle = document.createElement('div');
|
|
292
|
+
resizeHandle.className = 'nowaikit-ai-resize-handle';
|
|
293
|
+
aiSidebarEl.appendChild(resizeHandle);
|
|
294
|
+
initResizeHandle(resizeHandle);
|
|
295
|
+
|
|
296
|
+
document.body.appendChild(aiSidebarEl);
|
|
297
|
+
|
|
298
|
+
// Create the toggle button (floating)
|
|
299
|
+
var toggleBtn = document.createElement('button');
|
|
300
|
+
toggleBtn.id = 'nowaikit-ai-toggle';
|
|
301
|
+
toggleBtn.className = 'nowaikit-ai-toggle';
|
|
302
|
+
toggleBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
|
303
|
+
var _mod = (window.nowaikitState || {}).isMac ? 'Cmd' : 'Ctrl';
|
|
304
|
+
toggleBtn.title = 'NowAIKit Utils — AI Assistant (' + _mod + '+Shift+A)';
|
|
305
|
+
toggleBtn.addEventListener('click', toggleAISidebar);
|
|
306
|
+
document.body.appendChild(toggleBtn);
|
|
307
|
+
|
|
308
|
+
// Init floating mode drag
|
|
309
|
+
initFloatingDrag();
|
|
310
|
+
|
|
311
|
+
// Bind events
|
|
312
|
+
bindSidebarEvents();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function initResizeHandle(handle) {
|
|
316
|
+
var isResizing = false;
|
|
317
|
+
var startX = 0;
|
|
318
|
+
var startWidth = 0;
|
|
319
|
+
|
|
320
|
+
handle.addEventListener('mousedown', function(e) {
|
|
321
|
+
if (aiIsFloating) return;
|
|
322
|
+
isResizing = true;
|
|
323
|
+
startX = e.clientX;
|
|
324
|
+
startWidth = aiSidebarEl.offsetWidth;
|
|
325
|
+
handle.classList.add('active');
|
|
326
|
+
e.preventDefault();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
document.addEventListener('mousemove', function(e) {
|
|
330
|
+
if (!isResizing) return;
|
|
331
|
+
var diff = startX - e.clientX;
|
|
332
|
+
var newWidth = Math.max(360, Math.min(startWidth + diff, window.innerWidth * 0.8));
|
|
333
|
+
aiSidebarEl.style.width = newWidth + 'px';
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
document.addEventListener('mouseup', function() {
|
|
337
|
+
if (isResizing) {
|
|
338
|
+
isResizing = false;
|
|
339
|
+
handle.classList.remove('active');
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function initFloatingDrag() {
|
|
345
|
+
var isDragging = false;
|
|
346
|
+
var dragOffsetX = 0;
|
|
347
|
+
var dragOffsetY = 0;
|
|
348
|
+
|
|
349
|
+
document.addEventListener('mousedown', function(e) {
|
|
350
|
+
if (!aiIsFloating || !aiSidebarEl) return;
|
|
351
|
+
var header = aiSidebarEl.querySelector('.nowaikit-ai-header');
|
|
352
|
+
if (!header || !header.contains(e.target)) return;
|
|
353
|
+
// Don't drag if clicking buttons
|
|
354
|
+
if (e.target.closest('.nowaikit-ai-btn-icon')) return;
|
|
355
|
+
isDragging = true;
|
|
356
|
+
var rect = aiSidebarEl.getBoundingClientRect();
|
|
357
|
+
dragOffsetX = e.clientX - rect.left;
|
|
358
|
+
dragOffsetY = e.clientY - rect.top;
|
|
359
|
+
e.preventDefault();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
document.addEventListener('mousemove', function(e) {
|
|
363
|
+
if (!isDragging) return;
|
|
364
|
+
var x = Math.max(0, Math.min(e.clientX - dragOffsetX, window.innerWidth - 100));
|
|
365
|
+
var y = Math.max(0, Math.min(e.clientY - dragOffsetY, window.innerHeight - 100));
|
|
366
|
+
aiSidebarEl.style.left = x + 'px';
|
|
367
|
+
aiSidebarEl.style.top = y + 'px';
|
|
368
|
+
aiSidebarEl.style.right = 'auto';
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
document.addEventListener('mouseup', function() {
|
|
372
|
+
isDragging = false;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function toggleFloatingMode() {
|
|
377
|
+
if (!aiSidebarEl) return;
|
|
378
|
+
aiIsFloating = !aiIsFloating;
|
|
379
|
+
aiSidebarEl.classList.toggle('floating', aiIsFloating);
|
|
380
|
+
|
|
381
|
+
var floatBtn = document.getElementById('nowaikit-ai-float');
|
|
382
|
+
if (floatBtn) {
|
|
383
|
+
floatBtn.title = aiIsFloating ? 'Dock to side' : 'Float window';
|
|
384
|
+
floatBtn.innerHTML = aiIsFloating
|
|
385
|
+
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg>'
|
|
386
|
+
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><rect x="7" y="7" width="10" height="10" rx="1" fill="currentColor" opacity="0.15"/></svg>';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!aiIsFloating) {
|
|
390
|
+
// Reset to docked position
|
|
391
|
+
aiSidebarEl.style.left = '';
|
|
392
|
+
aiSidebarEl.style.top = '';
|
|
393
|
+
aiSidebarEl.style.right = '';
|
|
394
|
+
aiSidebarEl.style.width = '';
|
|
395
|
+
aiSidebarEl.style.height = '';
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function buildBridgeStatusHTML() {
|
|
400
|
+
var connected = isBridgeConnected();
|
|
401
|
+
var info = connected ? getBridgeInfo() : null;
|
|
402
|
+
var instanceName = (info && info.instanceName) ? info.instanceName : '';
|
|
403
|
+
var dotColor = connected ? '#00C853' : '#9e9e9e';
|
|
404
|
+
var label = connected ? ('Connected to ' + instanceName) : 'Standalone';
|
|
405
|
+
return '<div class="nowaikit-ai-bridge-status" id="nowaikit-ai-bridge-status">' +
|
|
406
|
+
'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dotColor + ';margin-right:6px;"></span>' +
|
|
407
|
+
'<span style="font-size:11px;opacity:.75;">' + label + '</span>' +
|
|
408
|
+
'</div>';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function buildSidebarHTML() {
|
|
412
|
+
return '' +
|
|
413
|
+
'<div class="nowaikit-ai-header">' +
|
|
414
|
+
'<div class="nowaikit-ai-header-left">' +
|
|
415
|
+
'<svg width="22" height="22" viewBox="0 0 512 512" style="flex-shrink:0"><defs><linearGradient id="nkAiHdrGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0C0"/><stop offset="50%" stop-color="#00D4AA"/><stop offset="100%" stop-color="#0F4C81"/></linearGradient></defs><rect width="512" height="512" rx="128" fill="url(#nkAiHdrGrad)"/><g transform="translate(256,256) scale(9.13) translate(-22,-23)"><path d="M5 39V7l15 27V7" stroke="#fff" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M34 4l2 7 6 2-6 2-2 7-2-7-6-2 6-2z" fill="#fff" opacity="0.9"><animate attributeName="opacity" values="0.9;0.4;0.9" dur="2s" repeatCount="indefinite"/></path><circle cx="34" cy="34" r="4.5" fill="#fff" opacity="0.8"><animate attributeName="opacity" values="0.8;0.4;0.8" dur="2s" repeatCount="indefinite" begin="0.3s"/></circle></g></svg>' +
|
|
416
|
+
'<span class="nowaikit-ai-title"><span class="nk-ai-logo-now">Now</span><span class="nk-ai-logo-ai">AI</span><span class="nk-ai-logo-kit">Kit</span><span class="nk-ai-logo-suffix"> Assistant</span></span>' +
|
|
417
|
+
'</div>' +
|
|
418
|
+
'<div class="nowaikit-ai-header-actions">' +
|
|
419
|
+
'<button class="nowaikit-ai-btn-icon" id="nowaikit-ai-theme" title="Toggle light/dark mode">' +
|
|
420
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>' +
|
|
421
|
+
'</button>' +
|
|
422
|
+
'<button class="nowaikit-ai-btn-icon" id="nowaikit-ai-popout" title="Open in new window">' +
|
|
423
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>' +
|
|
424
|
+
'</button>' +
|
|
425
|
+
'<button class="nowaikit-ai-btn-icon" id="nowaikit-ai-float" title="Float window">' +
|
|
426
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><rect x="7" y="7" width="10" height="10" rx="1" fill="currentColor" opacity="0.15"/></svg>' +
|
|
427
|
+
'</button>' +
|
|
428
|
+
'<button class="nowaikit-ai-btn-icon" id="nowaikit-ai-clear" title="Clear conversation">' +
|
|
429
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>' +
|
|
430
|
+
'</button>' +
|
|
431
|
+
'<button class="nowaikit-ai-btn-icon" id="nowaikit-ai-settings" title="AI Settings">' +
|
|
432
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>' +
|
|
433
|
+
'</button>' +
|
|
434
|
+
'<button class="nowaikit-ai-btn-icon" id="nowaikit-ai-close" title="Close (Esc)">' +
|
|
435
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' +
|
|
436
|
+
'</button>' +
|
|
437
|
+
'</div>' +
|
|
438
|
+
'</div>' +
|
|
439
|
+
buildBridgeStatusHTML() +
|
|
440
|
+
// Settings panel (hidden by default)
|
|
441
|
+
'<div class="nowaikit-ai-settings-panel" id="nowaikit-ai-settings-panel" style="display:none;">' +
|
|
442
|
+
'<div class="nowaikit-ai-setting-group">' +
|
|
443
|
+
'<label class="nowaikit-ai-label">AI Provider</label>' +
|
|
444
|
+
'<select id="nowaikit-ai-provider" class="nowaikit-ai-select">' +
|
|
445
|
+
'<option value="openai">OpenAI (GPT)</option>' +
|
|
446
|
+
'<option value="anthropic">Anthropic (Claude)</option>' +
|
|
447
|
+
'<option value="google">Google (Gemini)</option>' +
|
|
448
|
+
'<option value="openrouter">OpenRouter</option>' +
|
|
449
|
+
'<option value="ollama">Ollama (Local)</option>' +
|
|
450
|
+
'</select>' +
|
|
451
|
+
'</div>' +
|
|
452
|
+
'<div class="nowaikit-ai-setting-group">' +
|
|
453
|
+
'<label class="nowaikit-ai-label">API Key</label>' +
|
|
454
|
+
'<input type="password" id="nowaikit-ai-apikey" class="nowaikit-ai-input" placeholder="sk-... or paste your API key">' +
|
|
455
|
+
'<button id="nowaikit-ai-key-link" class="nowaikit-ai-key-link" style="background:none;border:none;color:#00D4AA;font-size:11px;cursor:pointer;padding:4px 0;text-decoration:underline;display:block;margin-top:4px;">Get your API key →</button>' +
|
|
456
|
+
'</div>' +
|
|
457
|
+
'<div class="nowaikit-ai-setting-group">' +
|
|
458
|
+
'<label class="nowaikit-ai-label">Model</label>' +
|
|
459
|
+
'<select id="nowaikit-ai-model" class="nowaikit-ai-select">' +
|
|
460
|
+
'<option value="gpt-5.4">GPT-5.4</option>' +
|
|
461
|
+
'<option value="gpt-5.4-mini">GPT-5.4 Mini</option>' +
|
|
462
|
+
'<option value="claude-sonnet-4-6">Claude Sonnet 4.6</option>' +
|
|
463
|
+
'<option value="claude-opus-4-6">Claude Opus 4.6</option>' +
|
|
464
|
+
'<option value="gemini-3.1-pro-preview">Gemini 3.1 Pro</option>' +
|
|
465
|
+
'<option value="gemini-2.5-flash">Gemini 2.5 Flash</option>' +
|
|
466
|
+
'</select>' +
|
|
467
|
+
'</div>' +
|
|
468
|
+
'<div class="nowaikit-ai-setting-group">' +
|
|
469
|
+
'<label class="nowaikit-ai-label">Ollama URL <span style="font-size:10px;opacity:.6;">(only for Ollama)</span></label>' +
|
|
470
|
+
'<input type="text" id="nowaikit-ai-ollama-url" class="nowaikit-ai-input" placeholder="http://localhost:11434" value="http://localhost:11434">' +
|
|
471
|
+
'</div>' +
|
|
472
|
+
'<button class="nowaikit-ai-save-btn" id="nowaikit-ai-save-settings">Save Settings</button>' +
|
|
473
|
+
'</div>' +
|
|
474
|
+
// Chat messages area
|
|
475
|
+
'<div class="nowaikit-ai-messages" id="nowaikit-ai-messages">' +
|
|
476
|
+
'<div class="nowaikit-ai-welcome">' +
|
|
477
|
+
'<div class="nowaikit-ai-welcome-icon">' +
|
|
478
|
+
'<svg width="32" height="32" viewBox="0 0 512 512"><defs><linearGradient id="naikGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0C0"/><stop offset="50%" stop-color="#00D4AA"/><stop offset="100%" stop-color="#0F4C81"/></linearGradient><linearGradient id="naikShim" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="white" stop-opacity="0"/><stop offset="50%" stop-color="white" stop-opacity="0.08"/><stop offset="100%" stop-color="white" stop-opacity="0"/></linearGradient><clipPath id="naikClip"><rect width="512" height="512" rx="128"/></clipPath></defs><rect width="512" height="512" rx="128" fill="url(#naikGrad)"/><g transform="translate(256,256) scale(9.13) translate(-22,-23)"><path d="M5 39V7l15 27V7" stroke="#fff" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M34 4l2 7 6 2-6 2-2 7-2-7-6-2 6-2z" fill="#fff" opacity="0.9"><animate attributeName="opacity" values="0.9;0.4;0.9" dur="2s" repeatCount="indefinite"/><animateTransform attributeName="transform" type="scale" values="1;1.15;1" dur="2s" repeatCount="indefinite" additive="sum"/></path><line x1="34" y1="19" x2="34" y2="28.5" stroke="#fff" stroke-width="1.8" opacity="0.5"><animate attributeName="opacity" values="0.5;0.2;0.5" dur="2s" repeatCount="indefinite"/></line><circle cx="34" cy="34" r="4.5" fill="#fff" opacity="0.8"><animate attributeName="opacity" values="0.8;0.4;0.8" dur="2s" repeatCount="indefinite" begin="0.3s"/><animate attributeName="r" values="4.5;5.5;4.5" dur="2s" repeatCount="indefinite" begin="0.3s"/></circle></g><rect x="-512" y="0" width="512" height="512" fill="url(#naikShim)" clip-path="url(#naikClip)"><animateTransform attributeName="transform" type="translate" values="0,0;1024,0" dur="4s" repeatCount="indefinite"/></rect></svg>' +
|
|
479
|
+
'</div>' +
|
|
480
|
+
'<div class="nowaikit-ai-welcome-title"><span class="nk-ai-logo-now">Now</span><span class="nk-ai-logo-ai">AI</span><span class="nk-ai-logo-kit">Kit</span><span class="nk-ai-logo-suffix"> Assistant</span></div>' +
|
|
481
|
+
'<div class="nowaikit-ai-welcome-sub">ServiceNow AI Assistant — Ask anything about this page, generate scripts, review code, or get best practices.</div>' +
|
|
482
|
+
'<div class="nowaikit-ai-welcome-hint">Tip: Configure your API key in Settings (gear icon) to get started.</div>' +
|
|
483
|
+
'</div>' +
|
|
484
|
+
'</div>' +
|
|
485
|
+
// Quick actions
|
|
486
|
+
'<div class="nowaikit-ai-quick-actions" id="nowaikit-ai-quick-actions">' +
|
|
487
|
+
QUICK_ACTIONS.map(function(a) {
|
|
488
|
+
return '<button class="nowaikit-ai-quick-btn" data-action-id="' + a.id + '" title="' + a.label + '">' +
|
|
489
|
+
'<span class="nowaikit-ai-quick-icon">' + a.icon + '</span>' +
|
|
490
|
+
'<span>' + a.label + '</span>' +
|
|
491
|
+
'</button>';
|
|
492
|
+
}).join('') +
|
|
493
|
+
'</div>' +
|
|
494
|
+
// Input area
|
|
495
|
+
'<div class="nowaikit-ai-input-area">' +
|
|
496
|
+
'<div class="nowaikit-ai-input-wrapper">' +
|
|
497
|
+
'<textarea id="nowaikit-ai-input" class="nowaikit-ai-chat-input" placeholder="Ask about this record, script, or table..." rows="1"></textarea>' +
|
|
498
|
+
'<button class="nowaikit-ai-send-btn" id="nowaikit-ai-send" title="Send (Enter)">' +
|
|
499
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' +
|
|
500
|
+
'</button>' +
|
|
501
|
+
'</div>' +
|
|
502
|
+
'<div class="nowaikit-ai-input-hint">Enter to send · Shift+Enter for new line · ' + ((window.nowaikitState || {}).isMac ? 'Cmd' : 'Ctrl') + '+Shift+A to toggle</div>' +
|
|
503
|
+
'</div>';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function bindSidebarEvents() {
|
|
507
|
+
// Close button
|
|
508
|
+
document.getElementById('nowaikit-ai-close').addEventListener('click', function() {
|
|
509
|
+
toggleAISidebar();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Theme toggle (dark/light)
|
|
513
|
+
document.getElementById('nowaikit-ai-theme').addEventListener('click', function() {
|
|
514
|
+
toggleAiTheme();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Apply saved theme on load (uses shared theme key)
|
|
518
|
+
chrome.storage.local.get({ nowaikitTheme: 'dark' }, function(result) {
|
|
519
|
+
applyAiTheme(result.nowaikitTheme || 'dark');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Float / Dock toggle
|
|
523
|
+
document.getElementById('nowaikit-ai-float').addEventListener('click', function() {
|
|
524
|
+
toggleFloatingMode();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Pop-out: open AI sidebar in a standalone browser window
|
|
528
|
+
document.getElementById('nowaikit-ai-popout').addEventListener('click', function() {
|
|
529
|
+
var w = 520, h = Math.round(window.innerHeight * 0.75);
|
|
530
|
+
var left = window.screenX + window.innerWidth - w - 40;
|
|
531
|
+
var top = window.screenY + 60;
|
|
532
|
+
window.open(
|
|
533
|
+
chrome.runtime.getURL('ai-window.html'),
|
|
534
|
+
'nowaikit-ai-window',
|
|
535
|
+
'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top + ',resizable=yes,scrollbars=no'
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Clear conversation
|
|
540
|
+
document.getElementById('nowaikit-ai-clear').addEventListener('click', function() {
|
|
541
|
+
aiConversation = [];
|
|
542
|
+
var msgsEl = document.getElementById('nowaikit-ai-messages');
|
|
543
|
+
msgsEl.innerHTML = buildWelcomeHTML();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Settings toggle
|
|
547
|
+
document.getElementById('nowaikit-ai-settings').addEventListener('click', function() {
|
|
548
|
+
var panel = document.getElementById('nowaikit-ai-settings-panel');
|
|
549
|
+
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Save settings (encrypt API key before storage)
|
|
553
|
+
document.getElementById('nowaikit-ai-save-settings').addEventListener('click', async function() {
|
|
554
|
+
var provider = document.getElementById('nowaikit-ai-provider').value;
|
|
555
|
+
var apiKey = document.getElementById('nowaikit-ai-apikey').value;
|
|
556
|
+
var model = document.getElementById('nowaikit-ai-model').value;
|
|
557
|
+
var ollamaUrl = document.getElementById('nowaikit-ai-ollama-url').value;
|
|
558
|
+
|
|
559
|
+
// Encrypt the API key before storing
|
|
560
|
+
var encryptedKey = await encryptApiKey(apiKey);
|
|
561
|
+
|
|
562
|
+
chrome.storage.local.set({
|
|
563
|
+
aiProvider: provider,
|
|
564
|
+
aiApiKey: encryptedKey,
|
|
565
|
+
aiModel: model,
|
|
566
|
+
aiOllamaUrl: ollamaUrl,
|
|
567
|
+
}, function() {
|
|
568
|
+
_showToast('AI settings saved securely', 'success');
|
|
569
|
+
document.getElementById('nowaikit-ai-settings-panel').style.display = 'none';
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Load saved settings (decrypt API key from storage)
|
|
574
|
+
chrome.storage.local.get({
|
|
575
|
+
aiProvider: 'openai',
|
|
576
|
+
aiApiKey: '',
|
|
577
|
+
aiModel: 'gpt-5.4',
|
|
578
|
+
aiOllamaUrl: 'http://localhost:11434',
|
|
579
|
+
}, async function(settings) {
|
|
580
|
+
document.getElementById('nowaikit-ai-provider').value = settings.aiProvider;
|
|
581
|
+
// Decrypt the API key for display
|
|
582
|
+
var decryptedKey = await decryptApiKey(settings.aiApiKey);
|
|
583
|
+
document.getElementById('nowaikit-ai-apikey').value = decryptedKey;
|
|
584
|
+
document.getElementById('nowaikit-ai-model').value = settings.aiModel;
|
|
585
|
+
document.getElementById('nowaikit-ai-ollama-url').value = settings.aiOllamaUrl;
|
|
586
|
+
updateModelOptions(settings.aiProvider);
|
|
587
|
+
updateApiKeyLink(settings.aiProvider);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Provider change updates model options and API key link
|
|
591
|
+
document.getElementById('nowaikit-ai-provider').addEventListener('change', function() {
|
|
592
|
+
updateModelOptions(this.value);
|
|
593
|
+
updateApiKeyLink(this.value);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Send message
|
|
597
|
+
document.getElementById('nowaikit-ai-send').addEventListener('click', sendMessage);
|
|
598
|
+
|
|
599
|
+
// Input: Enter to send, Shift+Enter for newline, auto-resize
|
|
600
|
+
var inputEl = document.getElementById('nowaikit-ai-input');
|
|
601
|
+
inputEl.addEventListener('keydown', function(e) {
|
|
602
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
603
|
+
e.preventDefault();
|
|
604
|
+
sendMessage();
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
inputEl.addEventListener('input', function() {
|
|
608
|
+
this.style.height = 'auto';
|
|
609
|
+
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Quick actions
|
|
613
|
+
document.querySelectorAll('.nowaikit-ai-quick-btn').forEach(function(btn) {
|
|
614
|
+
btn.addEventListener('click', function() {
|
|
615
|
+
var actionId = this.dataset.actionId;
|
|
616
|
+
var action = QUICK_ACTIONS.find(function(a) { return a.id === actionId; });
|
|
617
|
+
if (!action) return;
|
|
618
|
+
|
|
619
|
+
if (action.id === 'generate') {
|
|
620
|
+
// Focus input with a prompt prefix
|
|
621
|
+
var inputEl = document.getElementById('nowaikit-ai-input');
|
|
622
|
+
inputEl.value = 'Generate a ';
|
|
623
|
+
inputEl.focus();
|
|
624
|
+
inputEl.style.height = 'auto';
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Handle "Health" quick action — fetch live data from bridge if connected
|
|
629
|
+
if (action.id === 'instance') {
|
|
630
|
+
handleHealthQuickAction();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (action.prompt) {
|
|
635
|
+
// Replace {table} placeholder with the current table name
|
|
636
|
+
var _ns = window.nowaikitState || {};
|
|
637
|
+
var prompt = action.prompt.replace(/\{table\}/g, _ns.currentTable || 'the current table');
|
|
638
|
+
sendMessageWithText(prompt);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Escape to close
|
|
644
|
+
document.addEventListener('keydown', function(e) {
|
|
645
|
+
if (e.key === 'Escape' && aiSidebarOpen) {
|
|
646
|
+
toggleAISidebar();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function buildWelcomeHTML() {
|
|
652
|
+
return '' +
|
|
653
|
+
'<div class="nowaikit-ai-welcome">' +
|
|
654
|
+
'<div class="nowaikit-ai-welcome-icon">' +
|
|
655
|
+
'<svg width="32" height="32" viewBox="0 0 512 512"><defs><linearGradient id="naikGrad2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0C0"/><stop offset="50%" stop-color="#00D4AA"/><stop offset="100%" stop-color="#0F4C81"/></linearGradient><linearGradient id="naikShim2" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="white" stop-opacity="0"/><stop offset="50%" stop-color="white" stop-opacity="0.08"/><stop offset="100%" stop-color="white" stop-opacity="0"/></linearGradient><clipPath id="naikClip2"><rect width="512" height="512" rx="128"/></clipPath></defs><rect width="512" height="512" rx="128" fill="url(#naikGrad2)"/><g transform="translate(256,256) scale(9.13) translate(-22,-23)"><path d="M5 39V7l15 27V7" stroke="#fff" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M34 4l2 7 6 2-6 2-2 7-2-7-6-2 6-2z" fill="#fff" opacity="0.9"><animate attributeName="opacity" values="0.9;0.4;0.9" dur="2s" repeatCount="indefinite"/><animateTransform attributeName="transform" type="scale" values="1;1.15;1" dur="2s" repeatCount="indefinite" additive="sum"/></path><line x1="34" y1="19" x2="34" y2="28.5" stroke="#fff" stroke-width="1.8" opacity="0.5"><animate attributeName="opacity" values="0.5;0.2;0.5" dur="2s" repeatCount="indefinite"/></line><circle cx="34" cy="34" r="4.5" fill="#fff" opacity="0.8"><animate attributeName="opacity" values="0.8;0.4;0.8" dur="2s" repeatCount="indefinite" begin="0.3s"/><animate attributeName="r" values="4.5;5.5;4.5" dur="2s" repeatCount="indefinite" begin="0.3s"/></circle></g><rect x="-512" y="0" width="512" height="512" fill="url(#naikShim2)" clip-path="url(#naikClip2)"><animateTransform attributeName="transform" type="translate" values="0,0;1024,0" dur="4s" repeatCount="indefinite"/></rect></svg>' +
|
|
656
|
+
'</div>' +
|
|
657
|
+
'<div class="nowaikit-ai-welcome-title"><span class="nk-ai-logo-now">Now</span><span class="nk-ai-logo-ai">AI</span><span class="nk-ai-logo-kit">Kit</span><span class="nk-ai-logo-suffix"> Assistant</span></div>' +
|
|
658
|
+
'<div class="nowaikit-ai-welcome-sub">ServiceNow AI Assistant — Ask anything about this page, generate scripts, review code, or get best practices.</div>' +
|
|
659
|
+
'</div>';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ─── Model Options ──────────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
// API key generation links per provider
|
|
665
|
+
var AI_KEY_LINKS = {
|
|
666
|
+
openai: 'https://platform.openai.com/api-keys',
|
|
667
|
+
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
668
|
+
google: 'https://aistudio.google.com/apikey',
|
|
669
|
+
openrouter: 'https://openrouter.ai/keys',
|
|
670
|
+
ollama: '',
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
function updateModelOptions(provider) {
|
|
674
|
+
var modelSelect = document.getElementById('nowaikit-ai-model');
|
|
675
|
+
var models = {
|
|
676
|
+
openai: [
|
|
677
|
+
{ value: 'gpt-5.4', label: 'GPT-5.4' },
|
|
678
|
+
{ value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' },
|
|
679
|
+
{ value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano' },
|
|
680
|
+
],
|
|
681
|
+
anthropic: [
|
|
682
|
+
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
|
|
683
|
+
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
|
684
|
+
{ value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
|
|
685
|
+
],
|
|
686
|
+
google: [
|
|
687
|
+
{ value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro' },
|
|
688
|
+
{ value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash' },
|
|
689
|
+
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
690
|
+
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
691
|
+
],
|
|
692
|
+
openrouter: [
|
|
693
|
+
{ value: 'openai/gpt-5.4', label: 'GPT-5.4' },
|
|
694
|
+
{ value: 'anthropic/claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
|
|
695
|
+
{ value: 'anthropic/claude-opus-4-6', label: 'Claude Opus 4.6' },
|
|
696
|
+
{ value: 'google/gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro' },
|
|
697
|
+
{ value: 'deepseek/deepseek-v3.2', label: 'DeepSeek V3.2' },
|
|
698
|
+
],
|
|
699
|
+
ollama: [
|
|
700
|
+
{ value: 'qwen3', label: 'Qwen 3' },
|
|
701
|
+
{ value: 'qwen3.5', label: 'Qwen 3.5' },
|
|
702
|
+
{ value: 'deepseek-r1', label: 'DeepSeek R1' },
|
|
703
|
+
{ value: 'llama3.3', label: 'Llama 3.3' },
|
|
704
|
+
],
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
var opts = models[provider] || models.openai;
|
|
708
|
+
modelSelect.innerHTML = opts.map(function(m) {
|
|
709
|
+
return '<option value="' + m.value + '">' + m.label + '</option>';
|
|
710
|
+
}).join('');
|
|
711
|
+
|
|
712
|
+
// Show/hide Ollama URL field
|
|
713
|
+
var ollamaGroup = document.getElementById('nowaikit-ai-ollama-url').closest('.nowaikit-ai-setting-group');
|
|
714
|
+
if (ollamaGroup) {
|
|
715
|
+
ollamaGroup.style.display = provider === 'ollama' ? 'block' : 'none';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Update API key link
|
|
719
|
+
updateApiKeyLink(provider);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function updateApiKeyLink(provider) {
|
|
723
|
+
var linkEl = document.getElementById('nowaikit-ai-key-link');
|
|
724
|
+
if (!linkEl) return;
|
|
725
|
+
var url = AI_KEY_LINKS[provider] || '';
|
|
726
|
+
if (url) {
|
|
727
|
+
linkEl.style.display = 'block';
|
|
728
|
+
linkEl.onclick = function(e) {
|
|
729
|
+
e.preventDefault();
|
|
730
|
+
window.open(url, '_blank');
|
|
731
|
+
};
|
|
732
|
+
} else {
|
|
733
|
+
linkEl.style.display = 'none';
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ─── Theme Toggle ───────────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
var aiCurrentTheme = 'dark';
|
|
740
|
+
|
|
741
|
+
function toggleAiTheme() {
|
|
742
|
+
// Use global theme API if available (syncs all extension UI)
|
|
743
|
+
if (typeof window.nowaikitToggleTheme === 'function') {
|
|
744
|
+
var newTheme = window.nowaikitToggleTheme();
|
|
745
|
+
applyAiTheme(newTheme);
|
|
746
|
+
} else {
|
|
747
|
+
var newTheme = aiCurrentTheme === 'dark' ? 'light' : 'dark';
|
|
748
|
+
applyAiTheme(newTheme);
|
|
749
|
+
chrome.storage.local.set({ nowaikitTheme: newTheme });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function applyAiTheme(theme) {
|
|
754
|
+
aiCurrentTheme = theme;
|
|
755
|
+
if (!aiSidebarEl) return;
|
|
756
|
+
|
|
757
|
+
aiSidebarEl.classList.remove('nowaikit-ai-dark', 'nowaikit-ai-light');
|
|
758
|
+
aiSidebarEl.classList.add('nowaikit-ai-' + theme);
|
|
759
|
+
|
|
760
|
+
// Also toggle body class for CSS overrides
|
|
761
|
+
document.body.classList.remove('nowaikit-light', 'nowaikit-dark');
|
|
762
|
+
document.body.classList.add(theme === 'light' ? 'nowaikit-light' : 'nowaikit-dark');
|
|
763
|
+
|
|
764
|
+
// Update button icon
|
|
765
|
+
var themeBtn = document.getElementById('nowaikit-ai-theme');
|
|
766
|
+
if (themeBtn) {
|
|
767
|
+
themeBtn.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
|
768
|
+
themeBtn.innerHTML = theme === 'dark'
|
|
769
|
+
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>'
|
|
770
|
+
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ─── Toggle Sidebar ─────────────────────────────────────────────
|
|
775
|
+
|
|
776
|
+
function closeAISidebar() {
|
|
777
|
+
if (!aiSidebarOpen) return;
|
|
778
|
+
aiSidebarOpen = false;
|
|
779
|
+
if (aiSidebarEl) aiSidebarEl.classList.remove('open');
|
|
780
|
+
var toggleBtn = document.getElementById('nowaikit-ai-toggle');
|
|
781
|
+
if (toggleBtn) toggleBtn.classList.remove('active');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function toggleAISidebar() {
|
|
785
|
+
aiSidebarOpen = !aiSidebarOpen;
|
|
786
|
+
if (aiSidebarEl) {
|
|
787
|
+
aiSidebarEl.classList.toggle('open', aiSidebarOpen);
|
|
788
|
+
}
|
|
789
|
+
var toggleBtn = document.getElementById('nowaikit-ai-toggle');
|
|
790
|
+
if (toggleBtn) {
|
|
791
|
+
toggleBtn.classList.toggle('active', aiSidebarOpen);
|
|
792
|
+
}
|
|
793
|
+
if (aiSidebarOpen) {
|
|
794
|
+
// Close other panels
|
|
795
|
+
if (typeof window.closeMainPanel === 'function') window.closeMainPanel();
|
|
796
|
+
if (typeof window.closeCodeTemplates === 'function') window.closeCodeTemplates();
|
|
797
|
+
var inputEl = document.getElementById('nowaikit-ai-input');
|
|
798
|
+
if (inputEl) setTimeout(function() { inputEl.focus(); }, 100);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
window.closeAISidebar = closeAISidebar;
|
|
803
|
+
|
|
804
|
+
// ─── Bridge Status Refresh ───────────────────────────────────────
|
|
805
|
+
|
|
806
|
+
function refreshBridgeStatus() {
|
|
807
|
+
var statusEl = document.getElementById('nowaikit-ai-bridge-status');
|
|
808
|
+
if (!statusEl || !statusEl.parentNode) return;
|
|
809
|
+
// Use DOM replacement instead of outerHTML to avoid HTML injection risks
|
|
810
|
+
var wrapper = document.createElement('div');
|
|
811
|
+
wrapper.innerHTML = buildBridgeStatusHTML();
|
|
812
|
+
var newEl = wrapper.firstChild;
|
|
813
|
+
if (newEl) {
|
|
814
|
+
statusEl.parentNode.replaceChild(newEl, statusEl);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Periodically refresh bridge status (every 10 seconds when sidebar is open)
|
|
819
|
+
setInterval(function() {
|
|
820
|
+
if (aiSidebarOpen) refreshBridgeStatus();
|
|
821
|
+
}, 10000);
|
|
822
|
+
|
|
823
|
+
// ─── Health Quick Action ─────────────────────────────────────────
|
|
824
|
+
|
|
825
|
+
async function handleHealthQuickAction() {
|
|
826
|
+
// If bridge is connected, get real health data without needing an LLM call
|
|
827
|
+
if (isBridgeConnected() && typeof bridgeGetInstanceHealth === 'function') {
|
|
828
|
+
addMessageToUI('user', 'Show instance health metrics');
|
|
829
|
+
aiConversation.push({ role: 'user', content: 'Show instance health metrics' });
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
var health = await bridgeGetInstanceHealth();
|
|
833
|
+
var msg = formatHealthData(health);
|
|
834
|
+
addMessageToUI('assistant', msg);
|
|
835
|
+
aiConversation.push({ role: 'assistant', content: msg });
|
|
836
|
+
} catch (e) {
|
|
837
|
+
addMessageToUI('assistant', 'Unable to fetch instance health data: ' + (e.message || 'Unknown error. Please check the bridge connection.'));
|
|
838
|
+
aiConversation.push({ role: 'assistant', content: 'Unable to fetch instance health data.' });
|
|
839
|
+
}
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Fallback: send prompt to the LLM
|
|
844
|
+
sendMessageWithText('Show me the current instance health metrics and any issues.');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function formatHealthData(health) {
|
|
848
|
+
if (!health) return 'No health data available.';
|
|
849
|
+
|
|
850
|
+
var lines = ['**Instance Health Report**', ''];
|
|
851
|
+
|
|
852
|
+
if (health.instanceName) lines.push('**Instance:** ' + health.instanceName);
|
|
853
|
+
if (health.version) lines.push('**Version:** ' + health.version);
|
|
854
|
+
if (health.buildTag) lines.push('**Build:** ' + health.buildTag);
|
|
855
|
+
|
|
856
|
+
// Node counts / status
|
|
857
|
+
if (health.nodes && Array.isArray(health.nodes)) {
|
|
858
|
+
lines.push('', '**Nodes:**');
|
|
859
|
+
health.nodes.forEach(function(node) {
|
|
860
|
+
var status = node.status || 'unknown';
|
|
861
|
+
var statusIcon = status === 'online' ? '(OK)' : '(!)';
|
|
862
|
+
lines.push('- ' + (node.name || 'Node') + ': ' + status + ' ' + statusIcon);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Performance stats
|
|
867
|
+
if (health.stats) {
|
|
868
|
+
lines.push('', '**Performance:**');
|
|
869
|
+
if (health.stats.transactionCount !== undefined) lines.push('- Transactions: ' + health.stats.transactionCount);
|
|
870
|
+
if (health.stats.avgResponseTime !== undefined) lines.push('- Avg Response Time: ' + health.stats.avgResponseTime + 'ms');
|
|
871
|
+
if (health.stats.slowQueries !== undefined) lines.push('- Slow Queries: ' + health.stats.slowQueries);
|
|
872
|
+
if (health.stats.semaphoreWaits !== undefined) lines.push('- Semaphore Waits: ' + health.stats.semaphoreWaits);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Active users
|
|
876
|
+
if (health.activeUsers !== undefined) {
|
|
877
|
+
lines.push('', '**Active Users:** ' + health.activeUsers);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Issues / warnings
|
|
881
|
+
if (health.issues && Array.isArray(health.issues) && health.issues.length > 0) {
|
|
882
|
+
lines.push('', '**Issues:**');
|
|
883
|
+
health.issues.forEach(function(issue) {
|
|
884
|
+
lines.push('- ' + (issue.severity || 'info').toUpperCase() + ': ' + (issue.message || issue));
|
|
885
|
+
});
|
|
886
|
+
} else if (!health.issues || (Array.isArray(health.issues) && health.issues.length === 0)) {
|
|
887
|
+
lines.push('', '**Issues:** None detected');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// If the health object has other top-level keys we haven't handled, show them
|
|
891
|
+
var handledKeys = ['instanceName', 'version', 'buildTag', 'nodes', 'stats', 'activeUsers', 'issues'];
|
|
892
|
+
Object.keys(health).forEach(function(key) {
|
|
893
|
+
if (handledKeys.indexOf(key) === -1 && health[key] !== null && health[key] !== undefined) {
|
|
894
|
+
var val = typeof health[key] === 'object' ? JSON.stringify(health[key]) : String(health[key]);
|
|
895
|
+
lines.push('- **' + key + ':** ' + val);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
return lines.join('\n');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ─── Send Message ───────────────────────────────────────────────
|
|
903
|
+
|
|
904
|
+
function sendMessage() {
|
|
905
|
+
var inputEl = document.getElementById('nowaikit-ai-input');
|
|
906
|
+
var text = (inputEl.value || '').trim();
|
|
907
|
+
if (!text || aiStreaming) return;
|
|
908
|
+
sendMessageWithText(text);
|
|
909
|
+
inputEl.value = '';
|
|
910
|
+
inputEl.style.height = 'auto';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function sendMessageWithText(text) {
|
|
914
|
+
// Check for API key (decrypt from storage)
|
|
915
|
+
chrome.storage.local.get({
|
|
916
|
+
aiProvider: 'openai',
|
|
917
|
+
aiApiKey: '',
|
|
918
|
+
aiModel: 'gpt-5.4',
|
|
919
|
+
aiOllamaUrl: 'http://localhost:11434',
|
|
920
|
+
}, async function(settings) {
|
|
921
|
+
// Decrypt the API key
|
|
922
|
+
var decryptedKey = await decryptApiKey(settings.aiApiKey);
|
|
923
|
+
if (!decryptedKey && settings.aiProvider !== 'ollama') {
|
|
924
|
+
_showToast('Please configure your API key in AI Settings', 'warn');
|
|
925
|
+
document.getElementById('nowaikit-ai-settings-panel').style.display = 'block';
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Add user message to UI
|
|
930
|
+
addMessageToUI('user', text);
|
|
931
|
+
aiConversation.push({ role: 'user', content: text });
|
|
932
|
+
|
|
933
|
+
// Gather context (async — may fetch bridge data)
|
|
934
|
+
var ctx = await gatherPageContext();
|
|
935
|
+
var contextMsg = buildContextMessage(ctx);
|
|
936
|
+
|
|
937
|
+
// Build system prompt — enhance when bridge is connected
|
|
938
|
+
var systemPrompt = SN_SYSTEM_PROMPT;
|
|
939
|
+
if (isBridgeConnected()) {
|
|
940
|
+
var bridgeInfo = getBridgeInfo();
|
|
941
|
+
var instName = (bridgeInfo && bridgeInfo.instanceName) ? bridgeInfo.instanceName : 'unknown';
|
|
942
|
+
var instUrl = (bridgeInfo && bridgeInfo.instanceUrl) ? bridgeInfo.instanceUrl : '';
|
|
943
|
+
systemPrompt += '\n\nYou have access to a live ServiceNow instance (' + instName + ' at ' + instUrl + '). The user\'s current page context includes real-time data from the instance.';
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Build messages array for the LLM
|
|
947
|
+
var messages = [{ role: 'system', content: systemPrompt }];
|
|
948
|
+
|
|
949
|
+
// Add context as first user message if available
|
|
950
|
+
if (contextMsg) {
|
|
951
|
+
messages.push({ role: 'user', content: contextMsg });
|
|
952
|
+
messages.push({ role: 'assistant', content: 'I can see the current page context. How can I help?' });
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Add conversation history (last 20 messages to stay within limits)
|
|
956
|
+
var historySlice = aiConversation.slice(-20);
|
|
957
|
+
historySlice.forEach(function(msg) {
|
|
958
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// Create streaming response UI
|
|
962
|
+
var assistantMsgId = 'ai-msg-' + Date.now();
|
|
963
|
+
addStreamingMessage(assistantMsgId);
|
|
964
|
+
aiStreaming = true;
|
|
965
|
+
updateSendButton(true);
|
|
966
|
+
|
|
967
|
+
// Connect to background for streaming
|
|
968
|
+
aiPort = chrome.runtime.connect({ name: 'nowaikit-ai-stream' });
|
|
969
|
+
|
|
970
|
+
var fullResponse = '';
|
|
971
|
+
|
|
972
|
+
// Handle unexpected port disconnection
|
|
973
|
+
aiPort.onDisconnect.addListener(function() {
|
|
974
|
+
if (aiStreaming) {
|
|
975
|
+
aiStreaming = false;
|
|
976
|
+
updateSendButton(false);
|
|
977
|
+
if (fullResponse) {
|
|
978
|
+
finalizeMessage(assistantMsgId, fullResponse);
|
|
979
|
+
aiConversation.push({ role: 'assistant', content: fullResponse });
|
|
980
|
+
} else {
|
|
981
|
+
updateStreamingMessage(assistantMsgId, 'Connection lost. Please try again.');
|
|
982
|
+
var el = document.getElementById(assistantMsgId);
|
|
983
|
+
if (el) el.classList.add('nowaikit-ai-error');
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
aiPort = null;
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
aiPort.onMessage.addListener(function(msg) {
|
|
990
|
+
if (msg.type === 'token') {
|
|
991
|
+
fullResponse += msg.content;
|
|
992
|
+
updateStreamingMessage(assistantMsgId, fullResponse);
|
|
993
|
+
} else if (msg.type === 'done') {
|
|
994
|
+
aiStreaming = false;
|
|
995
|
+
updateSendButton(false);
|
|
996
|
+
finalizeMessage(assistantMsgId, fullResponse);
|
|
997
|
+
aiConversation.push({ role: 'assistant', content: fullResponse });
|
|
998
|
+
aiPort.disconnect();
|
|
999
|
+
aiPort = null;
|
|
1000
|
+
} else if (msg.type === 'error') {
|
|
1001
|
+
aiStreaming = false;
|
|
1002
|
+
updateSendButton(false);
|
|
1003
|
+
updateStreamingMessage(assistantMsgId, 'Error: ' + msg.content);
|
|
1004
|
+
var el = document.getElementById(assistantMsgId);
|
|
1005
|
+
if (el) el.classList.add('nowaikit-ai-error');
|
|
1006
|
+
aiPort.disconnect();
|
|
1007
|
+
aiPort = null;
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// Send the request (using decrypted key)
|
|
1012
|
+
aiPort.postMessage({
|
|
1013
|
+
action: 'nowaikit-ai-chat',
|
|
1014
|
+
provider: settings.aiProvider,
|
|
1015
|
+
apiKey: decryptedKey,
|
|
1016
|
+
model: settings.aiModel,
|
|
1017
|
+
ollamaUrl: settings.aiOllamaUrl,
|
|
1018
|
+
messages: messages,
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ─── UI Helpers ─────────────────────────────────────────────────
|
|
1024
|
+
|
|
1025
|
+
function addMessageToUI(role, content) {
|
|
1026
|
+
var msgsEl = document.getElementById('nowaikit-ai-messages');
|
|
1027
|
+
// Remove welcome message if present
|
|
1028
|
+
var welcome = msgsEl.querySelector('.nowaikit-ai-welcome');
|
|
1029
|
+
if (welcome) welcome.remove();
|
|
1030
|
+
|
|
1031
|
+
var msgEl = document.createElement('div');
|
|
1032
|
+
msgEl.className = 'nowaikit-ai-message nowaikit-ai-message-' + role;
|
|
1033
|
+
|
|
1034
|
+
var avatarHTML = role === 'user'
|
|
1035
|
+
? '<div class="nowaikit-ai-avatar nowaikit-ai-avatar-user">U</div>'
|
|
1036
|
+
: '<div class="nowaikit-ai-avatar nowaikit-ai-avatar-ai">N</div>';
|
|
1037
|
+
|
|
1038
|
+
msgEl.innerHTML = avatarHTML +
|
|
1039
|
+
'<div class="nowaikit-ai-message-content">' + renderMarkdown(_escapeHtml(content)) + '</div>';
|
|
1040
|
+
|
|
1041
|
+
msgsEl.appendChild(msgEl);
|
|
1042
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function addStreamingMessage(id) {
|
|
1046
|
+
var msgsEl = document.getElementById('nowaikit-ai-messages');
|
|
1047
|
+
var welcome = msgsEl.querySelector('.nowaikit-ai-welcome');
|
|
1048
|
+
if (welcome) welcome.remove();
|
|
1049
|
+
|
|
1050
|
+
var msgEl = document.createElement('div');
|
|
1051
|
+
msgEl.id = id;
|
|
1052
|
+
msgEl.className = 'nowaikit-ai-message nowaikit-ai-message-assistant';
|
|
1053
|
+
msgEl.innerHTML =
|
|
1054
|
+
'<div class="nowaikit-ai-avatar nowaikit-ai-avatar-ai">N</div>' +
|
|
1055
|
+
'<div class="nowaikit-ai-message-content"><span class="nowaikit-ai-typing">Thinking...</span></div>';
|
|
1056
|
+
|
|
1057
|
+
msgsEl.appendChild(msgEl);
|
|
1058
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
var _streamThrottleTimer = null;
|
|
1062
|
+
var _streamPendingContent = null;
|
|
1063
|
+
var _streamPendingId = null;
|
|
1064
|
+
|
|
1065
|
+
function updateStreamingMessage(id, content) {
|
|
1066
|
+
_streamPendingContent = content;
|
|
1067
|
+
_streamPendingId = id;
|
|
1068
|
+
// Throttle DOM updates to max ~15fps during streaming for performance
|
|
1069
|
+
if (_streamThrottleTimer) return;
|
|
1070
|
+
_streamThrottleTimer = setTimeout(function() {
|
|
1071
|
+
_streamThrottleTimer = null;
|
|
1072
|
+
_flushStreamingMessage();
|
|
1073
|
+
}, 66);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function _flushStreamingMessage() {
|
|
1077
|
+
if (!_streamPendingId || _streamPendingContent === null) return;
|
|
1078
|
+
var msgEl = document.getElementById(_streamPendingId);
|
|
1079
|
+
if (!msgEl) return;
|
|
1080
|
+
var contentEl = msgEl.querySelector('.nowaikit-ai-message-content');
|
|
1081
|
+
if (contentEl) {
|
|
1082
|
+
contentEl.innerHTML = renderMarkdown(_escapeHtml(_streamPendingContent)) + '<span class="nowaikit-ai-cursor">|</span>';
|
|
1083
|
+
}
|
|
1084
|
+
var msgsEl = document.getElementById('nowaikit-ai-messages');
|
|
1085
|
+
if (msgsEl) msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function finalizeMessage(id, content) {
|
|
1089
|
+
var msgEl = document.getElementById(id);
|
|
1090
|
+
if (!msgEl) return;
|
|
1091
|
+
var contentEl = msgEl.querySelector('.nowaikit-ai-message-content');
|
|
1092
|
+
if (contentEl) {
|
|
1093
|
+
contentEl.innerHTML = renderMarkdown(_escapeHtml(content));
|
|
1094
|
+
|
|
1095
|
+
// Add "Insert into editor" buttons for code blocks
|
|
1096
|
+
contentEl.querySelectorAll('pre code').forEach(function(codeEl) {
|
|
1097
|
+
var insertBtn = document.createElement('button');
|
|
1098
|
+
insertBtn.className = 'nowaikit-ai-insert-btn';
|
|
1099
|
+
insertBtn.textContent = 'Insert into Editor';
|
|
1100
|
+
insertBtn.addEventListener('click', function() {
|
|
1101
|
+
insertCodeIntoEditor(codeEl.textContent);
|
|
1102
|
+
});
|
|
1103
|
+
var copyBtn = document.createElement('button');
|
|
1104
|
+
copyBtn.className = 'nowaikit-ai-copy-code-btn';
|
|
1105
|
+
copyBtn.textContent = 'Copy';
|
|
1106
|
+
copyBtn.addEventListener('click', function() {
|
|
1107
|
+
navigator.clipboard.writeText(codeEl.textContent).then(function() {
|
|
1108
|
+
copyBtn.textContent = 'Copied!';
|
|
1109
|
+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
|
|
1110
|
+
}).catch(function() {
|
|
1111
|
+
copyBtn.textContent = 'Failed';
|
|
1112
|
+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
var actionsDiv = document.createElement('div');
|
|
1116
|
+
actionsDiv.className = 'nowaikit-ai-code-actions';
|
|
1117
|
+
actionsDiv.appendChild(copyBtn);
|
|
1118
|
+
actionsDiv.appendChild(insertBtn);
|
|
1119
|
+
codeEl.parentElement.insertBefore(actionsDiv, codeEl.parentElement.firstChild);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
var msgsEl = document.getElementById('nowaikit-ai-messages');
|
|
1123
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function updateSendButton(streaming) {
|
|
1127
|
+
var sendBtn = document.getElementById('nowaikit-ai-send');
|
|
1128
|
+
if (sendBtn) {
|
|
1129
|
+
sendBtn.disabled = streaming;
|
|
1130
|
+
sendBtn.style.opacity = streaming ? '0.5' : '1';
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ─── Markdown Rendering ─────────────────────────────────────────
|
|
1135
|
+
// Minimal markdown renderer for chat messages
|
|
1136
|
+
|
|
1137
|
+
function renderMarkdown(text) {
|
|
1138
|
+
// Code blocks: ```lang\n...\n```
|
|
1139
|
+
text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(m, lang, code) {
|
|
1140
|
+
// Sanitize lang to alphanumeric only (prevent class injection)
|
|
1141
|
+
var safeLang = (lang || 'text').replace(/[^a-zA-Z0-9]/g, '');
|
|
1142
|
+
return '<pre><code class="lang-' + safeLang + '">' + code.trim() + '</code></pre>';
|
|
1143
|
+
});
|
|
1144
|
+
// Inline code: `...`
|
|
1145
|
+
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1146
|
+
// Bold: **...**
|
|
1147
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
1148
|
+
// Italic: *...*
|
|
1149
|
+
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
1150
|
+
// Bullet lists: - item or * item
|
|
1151
|
+
text = text.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
|
|
1152
|
+
text = text.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
|
1153
|
+
// Numbered lists: 1. item
|
|
1154
|
+
text = text.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
|
1155
|
+
// Headers: ## ... or ### ...
|
|
1156
|
+
text = text.replace(/^### (.+)$/gm, '<h4>$1</h4>');
|
|
1157
|
+
text = text.replace(/^## (.+)$/gm, '<h3>$1</h3>');
|
|
1158
|
+
// Line breaks
|
|
1159
|
+
text = text.replace(/\n/g, '<br>');
|
|
1160
|
+
// Clean up multiple <br> tags
|
|
1161
|
+
text = text.replace(/(<br>){3,}/g, '<br><br>');
|
|
1162
|
+
|
|
1163
|
+
return text;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ─── Write-Back: Insert Code into Editor ────────────────────────
|
|
1167
|
+
|
|
1168
|
+
function insertCodeIntoEditor(code) {
|
|
1169
|
+
// Try CodeMirror first
|
|
1170
|
+
var cmEl = document.querySelector('.CodeMirror');
|
|
1171
|
+
if (cmEl && cmEl.CodeMirror) {
|
|
1172
|
+
var cm = cmEl.CodeMirror;
|
|
1173
|
+
var cursor = cm.getCursor();
|
|
1174
|
+
cm.replaceRange(code, cursor);
|
|
1175
|
+
_showToast('Code inserted into editor', 'success');
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Try textarea script fields
|
|
1180
|
+
var textareas = document.querySelectorAll('textarea[id*="script"], textarea[name="script"], textarea[name="condition"]');
|
|
1181
|
+
if (textareas.length > 0) {
|
|
1182
|
+
var ta = textareas[0];
|
|
1183
|
+
var start = ta.selectionStart;
|
|
1184
|
+
var end = ta.selectionEnd;
|
|
1185
|
+
ta.value = ta.value.substring(0, start) + code + ta.value.substring(end);
|
|
1186
|
+
ta.selectionStart = ta.selectionEnd = start + code.length;
|
|
1187
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1188
|
+
_showToast('Code inserted into editor', 'success');
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Fallback: copy to clipboard
|
|
1193
|
+
navigator.clipboard.writeText(code).then(function() {
|
|
1194
|
+
_showToast('No editor found. Code copied to clipboard.', 'warn');
|
|
1195
|
+
}).catch(function() {
|
|
1196
|
+
_showToast('No editor found and clipboard access denied.', 'warn');
|
|
1197
|
+
});
|
|
1198
|
+
}
|