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/ai-window.html ADDED
@@ -0,0 +1,598 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NowAIKit Utils — AI Assistant</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ :root {
11
+ --bg-primary: #0B1020;
12
+ --bg-secondary: #111828;
13
+ --bg-input: #0B1020;
14
+ --border: #1e2a3e;
15
+ --text-primary: #e0e5ec;
16
+ --text-secondary: #6b7a90;
17
+ --text-muted: #4a5568;
18
+ --accent: #00D4AA;
19
+ --accent-dark: #0F4C81;
20
+ --msg-user-bg: rgba(0, 212, 170, 0.08);
21
+ --msg-user-border: rgba(0, 212, 170, 0.15);
22
+ --code-bg: #0B1020;
23
+ --code-inline: #00D4AA;
24
+ --strong-color: #fff;
25
+ }
26
+
27
+ body.light {
28
+ --bg-primary: #ffffff;
29
+ --bg-secondary: #f8fafc;
30
+ --bg-input: #f8fafc;
31
+ --border: #e2e8f0;
32
+ --text-primary: #1a202c;
33
+ --text-secondary: #64748b;
34
+ --text-muted: #94a3b8;
35
+ --accent: #0D9488;
36
+ --accent-dark: #0F4C81;
37
+ --msg-user-bg: rgba(0, 212, 170, 0.06);
38
+ --msg-user-border: rgba(0, 212, 170, 0.12);
39
+ --code-bg: #f1f5f9;
40
+ --code-inline: #0D9488;
41
+ --strong-color: #1a202c;
42
+ }
43
+
44
+ body {
45
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
46
+ font-size: 13px;
47
+ color: var(--text-primary);
48
+ background: var(--bg-primary);
49
+ height: 100vh;
50
+ display: flex;
51
+ flex-direction: column;
52
+ overflow: hidden;
53
+ }
54
+
55
+ /* Header */
56
+ .header {
57
+ display: flex; align-items: center; justify-content: space-between;
58
+ padding: 10px 16px; background: var(--bg-secondary);
59
+ border-bottom: 1px solid var(--border); flex-shrink: 0;
60
+ }
61
+ .header-left { display: flex; align-items: center; gap: 8px; }
62
+ .header-title {
63
+ font-weight: 700; font-size: 14px;
64
+ background: linear-gradient(135deg, #00D4AA, #0F4C81);
65
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
66
+ }
67
+ .header-actions { display: flex; gap: 4px; }
68
+ .btn-icon {
69
+ width: 30px; height: 30px; border: none; background: transparent;
70
+ color: var(--text-secondary); cursor: pointer; border-radius: 6px;
71
+ display: flex; align-items: center; justify-content: center;
72
+ transition: background 0.2s, color 0.2s;
73
+ }
74
+ .btn-icon:hover { background: var(--border); color: var(--text-primary); }
75
+
76
+ /* Settings panel */
77
+ .settings-panel {
78
+ padding: 14px 16px; background: var(--bg-secondary);
79
+ border-bottom: 1px solid var(--border); flex-shrink: 0; display: none;
80
+ }
81
+ .setting-group { margin-bottom: 10px; }
82
+ .setting-label {
83
+ display: block; font-size: 11px; font-weight: 600;
84
+ color: var(--text-secondary); text-transform: uppercase;
85
+ letter-spacing: 0.5px; margin-bottom: 4px;
86
+ }
87
+ .setting-select, .setting-input {
88
+ width: 100%; padding: 7px 10px; background: var(--bg-input);
89
+ border: 1px solid var(--border); border-radius: 6px;
90
+ color: var(--text-primary); font-size: 13px; font-family: inherit; outline: none;
91
+ }
92
+ .setting-select:focus, .setting-input:focus { border-color: var(--accent); }
93
+ .save-btn {
94
+ width: 100%; padding: 8px; border: none; border-radius: 6px;
95
+ background: linear-gradient(135deg, #00D4AA, #0F4C81);
96
+ color: #fff; font-size: 13px; font-weight: 600; cursor: pointer;
97
+ }
98
+ .save-btn:hover { opacity: 0.9; }
99
+ .key-link {
100
+ background: none; border: none; color: var(--accent);
101
+ font-size: 11px; cursor: pointer; padding: 4px 0;
102
+ text-decoration: underline; display: block; margin-top: 4px;
103
+ }
104
+
105
+ /* Messages */
106
+ .messages {
107
+ flex: 1; overflow-y: auto; padding: 16px;
108
+ display: flex; flex-direction: column; gap: 12px;
109
+ }
110
+ .messages::-webkit-scrollbar { width: 6px; }
111
+ .messages::-webkit-scrollbar-track { background: transparent; }
112
+ .messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
113
+
114
+ .welcome {
115
+ display: flex; flex-direction: column; align-items: center;
116
+ justify-content: center; text-align: center; padding: 40px 20px; flex: 1;
117
+ }
118
+ .welcome-icon { margin-bottom: 16px; }
119
+ .welcome-title {
120
+ font-size: 18px; font-weight: 700;
121
+ background: linear-gradient(135deg, #00D4AA, #0F4C81);
122
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
123
+ margin-bottom: 8px;
124
+ }
125
+ .welcome-sub { font-size: 13px; color: var(--text-secondary); line-height: 1.5; max-width: 280px; }
126
+
127
+ .message { display: flex; gap: 10px; align-items: flex-start; }
128
+ .avatar {
129
+ width: 28px; height: 28px; border-radius: 50%;
130
+ display: flex; align-items: center; justify-content: center;
131
+ font-size: 12px; font-weight: 700; flex-shrink: 0;
132
+ }
133
+ .avatar-user { background: var(--border); color: var(--text-primary); }
134
+ .avatar-ai { background: linear-gradient(135deg, #00D4AA, #0F4C81); color: #fff; }
135
+
136
+ .msg-content {
137
+ flex: 1; background: var(--bg-secondary); border: 1px solid var(--border);
138
+ border-radius: 10px; padding: 10px 14px; font-size: 13px; line-height: 1.6;
139
+ color: var(--text-primary); overflow-wrap: break-word; word-break: break-word;
140
+ }
141
+ .message-user .msg-content { background: var(--msg-user-bg); border-color: var(--msg-user-border); }
142
+ .msg-content code {
143
+ font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 12px;
144
+ background: var(--code-bg); padding: 1px 5px; border-radius: 3px; color: var(--code-inline);
145
+ }
146
+ .msg-content pre {
147
+ background: var(--code-bg); border: 1px solid var(--border);
148
+ border-radius: 6px; padding: 12px; margin: 8px 0; overflow-x: auto; position: relative;
149
+ }
150
+ .msg-content pre code { background: none; padding: 0; color: var(--text-primary); line-height: 1.5; }
151
+ .msg-content strong { color: var(--strong-color); }
152
+ .msg-content h3, .msg-content h4 { color: var(--strong-color); margin: 8px 0 4px; }
153
+ .msg-content ul, .msg-content ol { padding-left: 20px; margin: 4px 0; }
154
+ .msg-content li { margin-bottom: 2px; }
155
+
156
+ .msg-error .msg-content { border-color: #ff6b6b; color: #ff8787; }
157
+
158
+ .code-actions { display: flex; gap: 6px; margin-bottom: 8px; }
159
+ .copy-btn, .insert-btn {
160
+ padding: 4px 10px; font-size: 11px; font-weight: 500;
161
+ border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
162
+ font-family: inherit; transition: all 0.2s;
163
+ }
164
+ .copy-btn { background: transparent; color: var(--text-secondary); }
165
+ .copy-btn:hover { background: var(--border); color: var(--text-primary); }
166
+
167
+ .typing { color: var(--text-secondary); font-style: italic; }
168
+ .cursor { animation: blink 1s step-end infinite; color: var(--accent); }
169
+ @keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
170
+
171
+ /* Quick actions */
172
+ .quick-actions {
173
+ display: flex; gap: 6px; padding: 10px 16px;
174
+ border-top: 1px solid var(--border); overflow-x: auto; flex-shrink: 0;
175
+ }
176
+ .quick-actions::-webkit-scrollbar { height: 0; }
177
+ .quick-btn {
178
+ display: flex; align-items: center; gap: 4px;
179
+ padding: 6px 10px; background: var(--bg-secondary);
180
+ border: 1px solid var(--border); border-radius: 16px;
181
+ color: var(--text-secondary); font-size: 11px;
182
+ font-family: inherit; cursor: pointer; white-space: nowrap; transition: all 0.2s;
183
+ }
184
+ .quick-btn:hover {
185
+ background: rgba(0, 212, 170, 0.08);
186
+ border-color: rgba(0, 212, 170, 0.2); color: var(--accent);
187
+ }
188
+
189
+ /* Input area */
190
+ .input-area {
191
+ padding: 12px 16px; border-top: 1px solid var(--border);
192
+ background: var(--bg-secondary); flex-shrink: 0;
193
+ }
194
+ .input-wrapper {
195
+ display: flex; align-items: flex-end; gap: 8px;
196
+ background: var(--bg-input); border: 1px solid var(--border);
197
+ border-radius: 10px; padding: 8px 12px; transition: border-color 0.2s;
198
+ }
199
+ .input-wrapper:focus-within { border-color: var(--accent); }
200
+ .chat-input {
201
+ flex: 1; background: transparent; border: none;
202
+ color: var(--text-primary); font-size: 13px; font-family: inherit;
203
+ line-height: 1.4; resize: none; outline: none; max-height: 120px;
204
+ }
205
+ .chat-input::placeholder { color: var(--text-muted); }
206
+ .send-btn {
207
+ width: 32px; height: 32px; border: none;
208
+ background: linear-gradient(135deg, #00D4AA, #0F4C81);
209
+ border-radius: 8px; color: #fff; cursor: pointer;
210
+ display: flex; align-items: center; justify-content: center; flex-shrink: 0;
211
+ }
212
+ .send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
213
+ .input-hint { font-size: 10px; color: var(--text-muted); text-align: center; margin-top: 6px; }
214
+ </style>
215
+ </head>
216
+ <body>
217
+ <!-- Header -->
218
+ <div class="header">
219
+ <div class="header-left">
220
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#00D4AA" 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>
221
+ <span class="header-title">NowAIKit Utils</span>
222
+ </div>
223
+ <div class="header-actions">
224
+ <button class="btn-icon" id="theme-btn" title="Toggle light/dark mode">
225
+ <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>
226
+ </button>
227
+ <button class="btn-icon" id="clear-btn" title="Clear conversation">
228
+ <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>
229
+ </button>
230
+ <button class="btn-icon" id="settings-btn" title="AI Settings">
231
+ <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-2.83 2.83l-.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-4 0v-.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-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 0-4h.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 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.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 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 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
232
+ </button>
233
+ </div>
234
+ </div>
235
+
236
+ <!-- Settings panel (hidden) -->
237
+ <div class="settings-panel" id="settings-panel">
238
+ <div class="setting-group">
239
+ <label class="setting-label">AI Provider</label>
240
+ <select id="provider" class="setting-select">
241
+ <option value="openai">OpenAI (GPT)</option>
242
+ <option value="anthropic">Anthropic (Claude)</option>
243
+ <option value="google">Google (Gemini)</option>
244
+ <option value="openrouter">OpenRouter</option>
245
+ <option value="ollama">Ollama (Local)</option>
246
+ </select>
247
+ </div>
248
+ <div class="setting-group">
249
+ <label class="setting-label">API Key</label>
250
+ <input type="password" id="apikey" class="setting-input" placeholder="sk-... or paste your API key">
251
+ <button id="key-link" class="key-link">Get your API key &rarr;</button>
252
+ </div>
253
+ <div class="setting-group">
254
+ <label class="setting-label">Model</label>
255
+ <select id="model" class="setting-select">
256
+ <option value="gpt-5.4">GPT-5.4</option>
257
+ <option value="gpt-5.4-mini">GPT-5.4 Mini</option>
258
+ </select>
259
+ </div>
260
+ <div class="setting-group" id="ollama-group" style="display:none;">
261
+ <label class="setting-label">Ollama URL</label>
262
+ <input type="text" id="ollama-url" class="setting-input" placeholder="http://localhost:11434" value="http://localhost:11434">
263
+ </div>
264
+ <button class="save-btn" id="save-settings">Save Settings</button>
265
+ </div>
266
+
267
+ <!-- Messages -->
268
+ <div class="messages" id="messages">
269
+ <div class="welcome">
270
+ <div class="welcome-icon">
271
+ <svg width="32" height="32" viewBox="0 0 512 512"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0C0"/><stop offset="100%" stop-color="#0F4C81"/></linearGradient></defs><rect width="512" height="512" rx="128" fill="url(#g)"/><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"/></g></svg>
272
+ </div>
273
+ <div class="welcome-title">NowAIKit Utils</div>
274
+ <div class="welcome-sub">ServiceNow AI Assistant — Ask anything about ServiceNow, generate scripts, review code, or get best practices.</div>
275
+ </div>
276
+ </div>
277
+
278
+ <!-- Quick actions -->
279
+ <div class="quick-actions" id="quick-actions">
280
+ <button class="quick-btn" data-prompt="Explain this script in detail.">&#x1F4A1; Explain</button>
281
+ <button class="quick-btn" data-prompt="Review this code for issues and best practice violations.">&#x1F50D; Review</button>
282
+ <button class="quick-btn" data-prompt="Optimize this script for performance.">&#x1F680; Optimize</button>
283
+ <button class="quick-btn" data-prompt="Fix all issues in this script.">&#x1F527; Fix</button>
284
+ <button class="quick-btn" data-prompt="Help me write a GlideRecord query.">&#x1F50D; Query</button>
285
+ </div>
286
+
287
+ <!-- Input area -->
288
+ <div class="input-area">
289
+ <div class="input-wrapper">
290
+ <textarea id="chat-input" class="chat-input" placeholder="Ask about ServiceNow..." rows="1"></textarea>
291
+ <button class="send-btn" id="send-btn" title="Send (Enter)">
292
+ <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>
293
+ </button>
294
+ </div>
295
+ <div class="input-hint">Enter to send &middot; Shift+Enter for new line</div>
296
+ </div>
297
+
298
+ <script>
299
+ // ─── State ──────────────────────────────────────────────
300
+ var conversation = [];
301
+ var streaming = false;
302
+ var port = null;
303
+ var currentTheme = 'dark';
304
+
305
+ var SN_SYSTEM_PROMPT = 'You are NowAIKit Utils AI Assistant, an expert ServiceNow platform assistant. ' +
306
+ 'You have deep expertise in GlideRecord, GlideAjax, Business Rules, Client Scripts, Script Includes, ' +
307
+ 'Flow Designer, ACLs, REST APIs, Service Portal, and all ServiceNow modules. ' +
308
+ 'Always provide code examples following ServiceNow best practices.';
309
+
310
+ var MODELS = {
311
+ openai: [
312
+ { value: 'gpt-5.4', label: 'GPT-5.4' },
313
+ { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' },
314
+ { value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano' },
315
+ ],
316
+ anthropic: [
317
+ { value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
318
+ { value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
319
+ { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
320
+ ],
321
+ google: [
322
+ { value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro' },
323
+ { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash' },
324
+ { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
325
+ ],
326
+ openrouter: [
327
+ { value: 'openai/gpt-5.4', label: 'GPT-5.4' },
328
+ { value: 'anthropic/claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
329
+ { value: 'anthropic/claude-opus-4-6', label: 'Claude Opus 4.6' },
330
+ { value: 'google/gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro' },
331
+ ],
332
+ ollama: [
333
+ { value: 'qwen3', label: 'Qwen 3' },
334
+ { value: 'qwen3.5', label: 'Qwen 3.5' },
335
+ { value: 'deepseek-r1', label: 'DeepSeek R1' },
336
+ { value: 'llama3.3', label: 'Llama 3.3' },
337
+ ],
338
+ };
339
+
340
+ var KEY_LINKS = {
341
+ openai: 'https://platform.openai.com/api-keys',
342
+ anthropic: 'https://console.anthropic.com/settings/keys',
343
+ google: 'https://aistudio.google.com/apikey',
344
+ openrouter: 'https://openrouter.ai/keys',
345
+ ollama: '',
346
+ };
347
+
348
+ // ─── Theme ──────────────────────────────────────────────
349
+ function toggleTheme() {
350
+ currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
351
+ applyTheme(currentTheme);
352
+ chrome.storage.local.set({ aiTheme: currentTheme });
353
+ }
354
+
355
+ function applyTheme(theme) {
356
+ currentTheme = theme;
357
+ document.body.classList.remove('light');
358
+ if (theme === 'light') document.body.classList.add('light');
359
+ var btn = document.getElementById('theme-btn');
360
+ btn.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
361
+ btn.innerHTML = theme === 'dark'
362
+ ? '<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>'
363
+ : '<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>';
364
+ }
365
+
366
+ // ─── Settings ───────────────────────────────────────────
367
+ function updateModels(provider) {
368
+ var sel = document.getElementById('model');
369
+ var opts = MODELS[provider] || MODELS.openai;
370
+ sel.innerHTML = opts.map(function(m) { return '<option value="' + m.value + '">' + m.label + '</option>'; }).join('');
371
+ document.getElementById('ollama-group').style.display = provider === 'ollama' ? 'block' : 'none';
372
+ updateKeyLink(provider);
373
+ }
374
+
375
+ function updateKeyLink(provider) {
376
+ var el = document.getElementById('key-link');
377
+ var url = KEY_LINKS[provider] || '';
378
+ el.style.display = url ? 'block' : 'none';
379
+ el.onclick = function(e) { e.preventDefault(); if (url) window.open(url, '_blank'); };
380
+ }
381
+
382
+ // ─── Markdown ───────────────────────────────────────────
383
+ function escapeHtml(str) {
384
+ var div = document.createElement('div');
385
+ div.textContent = str;
386
+ return div.innerHTML;
387
+ }
388
+
389
+ function renderMarkdown(text) {
390
+ text = text.replace(/```(\w*)\n([\s\S]*?)```/g, function(m, lang, code) {
391
+ return '<pre><code class="lang-' + (lang || 'text') + '">' + code.trim() + '</code></pre>';
392
+ });
393
+ text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
394
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
395
+ text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
396
+ text = text.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
397
+ text = text.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
398
+ text = text.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
399
+ text = text.replace(/^### (.+)$/gm, '<h4>$1</h4>');
400
+ text = text.replace(/^## (.+)$/gm, '<h3>$1</h3>');
401
+ text = text.replace(/\n/g, '<br>');
402
+ text = text.replace(/(<br>){3,}/g, '<br><br>');
403
+ return text;
404
+ }
405
+
406
+ // ─── UI Helpers ─────────────────────────────────────────
407
+ function addMessage(role, content) {
408
+ var el = document.getElementById('messages');
409
+ var w = el.querySelector('.welcome');
410
+ if (w) w.remove();
411
+
412
+ var msg = document.createElement('div');
413
+ msg.className = 'message message-' + role;
414
+ var avatarCls = role === 'user' ? 'avatar avatar-user' : 'avatar avatar-ai';
415
+ var avatarText = role === 'user' ? 'U' : 'N';
416
+ msg.innerHTML = '<div class="' + avatarCls + '">' + avatarText + '</div>' +
417
+ '<div class="msg-content">' + renderMarkdown(escapeHtml(content)) + '</div>';
418
+ el.appendChild(msg);
419
+ el.scrollTop = el.scrollHeight;
420
+ }
421
+
422
+ function addStreamMsg(id) {
423
+ var el = document.getElementById('messages');
424
+ var w = el.querySelector('.welcome');
425
+ if (w) w.remove();
426
+ var msg = document.createElement('div');
427
+ msg.id = id;
428
+ msg.className = 'message message-assistant';
429
+ msg.innerHTML = '<div class="avatar avatar-ai">N</div>' +
430
+ '<div class="msg-content"><span class="typing">Thinking...</span></div>';
431
+ el.appendChild(msg);
432
+ el.scrollTop = el.scrollHeight;
433
+ }
434
+
435
+ function updateStreamMsg(id, content) {
436
+ var msg = document.getElementById(id);
437
+ if (!msg) return;
438
+ var c = msg.querySelector('.msg-content');
439
+ if (c) c.innerHTML = renderMarkdown(escapeHtml(content)) + '<span class="cursor">|</span>';
440
+ document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
441
+ }
442
+
443
+ function finalizeMsg(id, content) {
444
+ var msg = document.getElementById(id);
445
+ if (!msg) return;
446
+ var c = msg.querySelector('.msg-content');
447
+ if (c) {
448
+ c.innerHTML = renderMarkdown(escapeHtml(content));
449
+ c.querySelectorAll('pre code').forEach(function(codeEl) {
450
+ var copyBtn = document.createElement('button');
451
+ copyBtn.className = 'copy-btn';
452
+ copyBtn.textContent = 'Copy';
453
+ copyBtn.addEventListener('click', function() {
454
+ navigator.clipboard.writeText(codeEl.textContent);
455
+ copyBtn.textContent = 'Copied!';
456
+ setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
457
+ });
458
+ var actions = document.createElement('div');
459
+ actions.className = 'code-actions';
460
+ actions.appendChild(copyBtn);
461
+ codeEl.parentElement.insertBefore(actions, codeEl.parentElement.firstChild);
462
+ });
463
+ }
464
+ document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
465
+ }
466
+
467
+ // ─── Send ───────────────────────────────────────────────
468
+ function sendMsg(text) {
469
+ if (!text || streaming) return;
470
+
471
+ chrome.storage.local.get({
472
+ aiProvider: 'openai', aiApiKey: '', aiModel: 'gpt-5.4', aiOllamaUrl: 'http://localhost:11434',
473
+ }, function(settings) {
474
+ if (!settings.aiApiKey && settings.aiProvider !== 'ollama') {
475
+ alert('Please configure your API key in Settings.');
476
+ document.getElementById('settings-panel').style.display = 'block';
477
+ return;
478
+ }
479
+
480
+ addMessage('user', text);
481
+ conversation.push({ role: 'user', content: text });
482
+
483
+ var messages = [{ role: 'system', content: SN_SYSTEM_PROMPT }];
484
+ conversation.slice(-20).forEach(function(m) { messages.push({ role: m.role, content: m.content }); });
485
+
486
+ var msgId = 'msg-' + Date.now();
487
+ addStreamMsg(msgId);
488
+ streaming = true;
489
+ document.getElementById('send-btn').disabled = true;
490
+
491
+ port = chrome.runtime.connect({ name: 'nowaikit-ai-stream' });
492
+ var fullResp = '';
493
+
494
+ port.onMessage.addListener(function(msg) {
495
+ if (msg.type === 'token') {
496
+ fullResp += msg.content;
497
+ updateStreamMsg(msgId, fullResp);
498
+ } else if (msg.type === 'done') {
499
+ streaming = false;
500
+ document.getElementById('send-btn').disabled = false;
501
+ finalizeMsg(msgId, fullResp);
502
+ conversation.push({ role: 'assistant', content: fullResp });
503
+ port.disconnect();
504
+ port = null;
505
+ } else if (msg.type === 'error') {
506
+ streaming = false;
507
+ document.getElementById('send-btn').disabled = false;
508
+ updateStreamMsg(msgId, 'Error: ' + msg.content);
509
+ var el = document.getElementById(msgId);
510
+ if (el) el.classList.add('msg-error');
511
+ port.disconnect();
512
+ port = null;
513
+ }
514
+ });
515
+
516
+ port.postMessage({
517
+ action: 'nowaikit-ai-chat',
518
+ provider: settings.aiProvider,
519
+ apiKey: settings.aiApiKey,
520
+ model: settings.aiModel,
521
+ ollamaUrl: settings.aiOllamaUrl,
522
+ messages: messages,
523
+ });
524
+ });
525
+ }
526
+
527
+ // ─── Event Binding ──────────────────────────────────────
528
+ document.getElementById('theme-btn').addEventListener('click', toggleTheme);
529
+
530
+ document.getElementById('clear-btn').addEventListener('click', function() {
531
+ conversation = [];
532
+ document.getElementById('messages').innerHTML =
533
+ '<div class="welcome"><div class="welcome-icon"><svg width="32" height="32" viewBox="0 0 512 512"><defs><linearGradient id="g2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0C0"/><stop offset="100%" stop-color="#0F4C81"/></linearGradient></defs><rect width="512" height="512" rx="128" fill="url(#g2)"/><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"/></g></svg></div><div class="welcome-title">NowAIKit Utils</div><div class="welcome-sub">Conversation cleared. Ask anything about ServiceNow.</div></div>';
534
+ });
535
+
536
+ document.getElementById('settings-btn').addEventListener('click', function() {
537
+ var p = document.getElementById('settings-panel');
538
+ p.style.display = p.style.display === 'none' ? 'block' : 'none';
539
+ });
540
+
541
+ document.getElementById('save-settings').addEventListener('click', function() {
542
+ chrome.storage.local.set({
543
+ aiProvider: document.getElementById('provider').value,
544
+ aiApiKey: document.getElementById('apikey').value,
545
+ aiModel: document.getElementById('model').value,
546
+ aiOllamaUrl: document.getElementById('ollama-url').value,
547
+ }, function() {
548
+ document.getElementById('settings-panel').style.display = 'none';
549
+ });
550
+ });
551
+
552
+ document.getElementById('provider').addEventListener('change', function() {
553
+ updateModels(this.value);
554
+ });
555
+
556
+ document.getElementById('send-btn').addEventListener('click', function() {
557
+ var input = document.getElementById('chat-input');
558
+ var text = (input.value || '').trim();
559
+ if (text) { sendMsg(text); input.value = ''; input.style.height = 'auto'; }
560
+ });
561
+
562
+ document.getElementById('chat-input').addEventListener('keydown', function(e) {
563
+ if (e.key === 'Enter' && !e.shiftKey) {
564
+ e.preventDefault();
565
+ var text = (this.value || '').trim();
566
+ if (text) { sendMsg(text); this.value = ''; this.style.height = 'auto'; }
567
+ }
568
+ });
569
+
570
+ document.getElementById('chat-input').addEventListener('input', function() {
571
+ this.style.height = 'auto';
572
+ this.style.height = Math.min(this.scrollHeight, 120) + 'px';
573
+ });
574
+
575
+ document.querySelectorAll('.quick-btn').forEach(function(btn) {
576
+ btn.addEventListener('click', function() {
577
+ var prompt = this.dataset.prompt;
578
+ if (prompt) sendMsg(prompt);
579
+ });
580
+ });
581
+
582
+ // ─── Init ───────────────────────────────────────────────
583
+ chrome.storage.local.get({
584
+ aiProvider: 'openai', aiApiKey: '', aiModel: 'gpt-5.4',
585
+ aiOllamaUrl: 'http://localhost:11434', aiTheme: 'dark',
586
+ }, function(s) {
587
+ document.getElementById('provider').value = s.aiProvider;
588
+ document.getElementById('apikey').value = s.aiApiKey;
589
+ document.getElementById('model').value = s.aiModel;
590
+ document.getElementById('ollama-url').value = s.aiOllamaUrl;
591
+ updateModels(s.aiProvider);
592
+ applyTheme(s.aiTheme);
593
+ });
594
+
595
+ document.getElementById('chat-input').focus();
596
+ </script>
597
+ </body>
598
+ </html>