yasserrmd commited on
Commit
eca6249
Β·
verified Β·
1 Parent(s): aec9f68

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +1372 -425
index.html CHANGED
@@ -1,5 +1,5 @@
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" />
@@ -20,363 +20,1287 @@
20
  card: '#1c2128',
21
  border: '#30363d',
22
  muted: '#8b949e',
 
 
 
 
 
 
 
23
  }
24
  }
25
  }
26
  }
27
  </script>
28
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
29
  <style>
30
- * { font-family: 'Inter', system-ui, sans-serif; }
31
- html { background: #0d1117; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- /* Scrollbar */
34
- ::-webkit-scrollbar { width: 5px; height: 5px; }
35
  ::-webkit-scrollbar-track { background: transparent; }
36
- ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 10px; }
37
- ::-webkit-scrollbar-thumb:hover { background: #484f58; }
38
-
39
- /* Markdown */
40
- .md-body { line-height: 1.7; }
41
- .md-body h2 { font-size: 1.05rem; font-weight: 700; margin: 1rem 0 0.5rem; color: #e6edf3; border-bottom: 1px solid #30363d; padding-bottom: 0.35rem; }
42
- .md-body h3 { font-size: 0.95rem; font-weight: 600; margin: 0.75rem 0 0.35rem; color: #cdd9e5; }
43
- .md-body p { margin: 0.4rem 0; color: #adbac7; }
44
- .md-body ul, .md-body ol { margin: 0.4rem 0 0.4rem 1.5rem; }
45
- .md-body li { margin: 0.3rem 0; color: #adbac7; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  .md-body strong { color: #e6edf3; font-weight: 600; }
47
- .md-body blockquote {
48
- border-left: 3px solid #f0883e; padding: 0.5rem 0.85rem;
49
- margin: 0.6rem 0; color: #f0883e; background: rgba(240,136,62,0.07);
50
- border-radius: 0 6px 6px 0; font-size: 0.85rem;
51
- }
52
- .md-body code {
53
- background: #161b22; padding: 2px 6px; border-radius: 5px;
54
- font-size: 0.82em; color: #79c0ff; border: 1px solid #30363d;
55
- font-family: 'SF Mono', 'Fira Code', monospace;
56
- }
57
- .md-body pre { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 0.85rem; margin: 0.5rem 0; overflow-x: auto; }
58
- .md-body pre code { background: none; border: none; padding: 0; }
59
- .md-body table { width: 100%; border-collapse: collapse; margin: 0.6rem 0; font-size: 0.82rem; }
60
- .md-body th { background: #1c2128; padding: 7px 12px; text-align: left; border: 1px solid #30363d; color: #cdd9e5; font-weight: 600; }
61
- .md-body td { padding: 6px 12px; border: 1px solid #30363d; color: #adbac7; }
62
- .md-body hr { border-color: #30363d; margin: 0.75rem 0; }
63
- .md-body ol { list-style: decimal; }
64
- .md-body ul { list-style: disc; }
65
-
66
- /* Streaming cursor */
67
- .streaming-cursor::after { content: 'β–Œ'; animation: blink-cur 0.75s infinite; color: #388bfd; }
68
- @keyframes blink-cur { 0%,100%{opacity:1} 50%{opacity:0} }
69
-
70
- /* Typing dots */
71
- .typing-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #484f58; animation: bounce-dot 1.3s infinite; }
72
- .typing-dot:nth-child(2) { animation-delay: 0.15s; }
73
- .typing-dot:nth-child(3) { animation-delay: 0.3s; }
74
- @keyframes bounce-dot { 0%,80%,100%{transform:translateY(0);opacity:.4} 40%{transform:translateY(-5px);opacity:1} }
75
-
76
- /* Form fields */
77
- .form-bubble form { all: unset; display: block; }
78
- .form-bubble input[type=text],
79
- .form-bubble input[type=date],
80
- .form-bubble input[type=number],
81
- .form-bubble input[type=email],
82
- .form-bubble textarea,
83
- .form-bubble select {
84
- display: block; width: 100%;
85
- padding: 0.5rem 0.75rem;
86
- background: #0d1117; color: #cdd9e5;
87
- border: 1px solid #30363d; border-radius: 8px;
88
- font-size: 0.85rem; margin-top: 4px; box-sizing: border-box;
89
- transition: border-color 0.15s, box-shadow 0.15s;
90
- font-family: 'Inter', system-ui, sans-serif;
91
  }
92
- .form-bubble select[multiple] { height: 8rem; }
93
- .form-bubble input:focus,
94
- .form-bubble select:focus,
95
- .form-bubble textarea:focus {
96
- outline: none; border-color: #388bfd;
97
- box-shadow: 0 0 0 3px rgba(56,139,253,0.15);
98
- }
99
- .form-bubble label {
100
- font-size: 0.78rem; font-weight: 500;
101
- color: #8b949e; display: block; margin-top: 0.9rem; letter-spacing: 0.01em;
102
- }
103
- .form-bubble .flex.items-center { display: flex; align-items: center; gap: 8px; margin-top: 0.8rem; }
104
- .form-bubble .flex.items-center label { margin-top: 0; color: #cdd9e5; font-size: 0.85rem; }
105
- .form-bubble input[type=checkbox] { width: 16px; height: 16px; accent-color: #388bfd; flex-shrink: 0; cursor: pointer; }
106
- .form-bubble button[type=submit] {
107
- margin-top: 1.25rem; width: 100%; padding: 0.65rem;
108
- background: #1f6feb; color: white; border-radius: 8px;
109
- font-weight: 600; font-size: 0.875rem; cursor: pointer;
110
- border: none; transition: background 0.15s, transform 0.1s;
111
- font-family: 'Inter', system-ui, sans-serif; letter-spacing: 0.01em;
112
- }
113
- .form-bubble button[type=submit]:hover { background: #388bfd; }
114
- .form-bubble button[type=submit]:active { transform: scale(0.99); }
115
- .form-bubble button[type=submit]:disabled { background: #21262d; color: #484f58; cursor: not-allowed; transform: none; }
116
- .form-bubble .text-red-500 { color: #f85149; font-size: 0.78rem; margin-top: 3px; display: block; }
117
-
118
- /* Step wizard */
119
- .formora-step { display: none; }
120
- .formora-step.active { display: block; }
121
- .step-nav { display: flex; gap: 8px; margin-top: 1.1rem; }
122
- .step-nav button {
123
- flex: 1; padding: 0.55rem; border-radius: 8px; font-weight: 600;
124
- font-size: 0.82rem; cursor: pointer; border: none;
125
- font-family: 'Inter', system-ui, sans-serif; transition: background 0.15s;
126
- }
127
- .btn-back { background: #21262d; color: #8b949e; border: 1px solid #30363d !important; }
128
- .btn-back:hover { background: #30363d; }
129
- .btn-next { background: #1f6feb; color: white; }
130
- .btn-next:hover { background: #388bfd; }
131
- .step-progress {
132
- display: flex; gap: 5px; justify-content: center; margin-top: 0.9rem;
133
- }
134
- .step-pip {
135
- height: 4px; flex: 1; border-radius: 2px; background: #30363d;
136
- transition: background 0.2s;
137
- }
138
- .step-pip.active { background: #388bfd; }
139
-
140
- /* Model progress bar */
141
  #model-progress-bar { transition: width 0.4s ease; }
142
 
143
- /* Suggestion chips */
144
  .chip {
145
  display: inline-flex; align-items: center; gap: 6px;
146
- background: #1c2128; border: 1px solid #30363d;
147
- color: #8b949e; font-size: 0.8rem; padding: 0.45rem 0.85rem;
148
- border-radius: 20px; cursor: pointer; transition: all 0.15s;
149
  white-space: nowrap;
150
  }
151
- .chip:hover { background: #21262d; border-color: #388bfd; color: #cdd9e5; }
152
 
153
- /* Input send button icon */
154
- #send-btn svg { display: inline; }
 
 
 
155
  </style>
156
  </head>
 
157
 
158
- <body class="bg-surface text-gray-100 h-screen flex flex-col overflow-hidden" style="color-scheme:dark">
159
-
160
- <!-- ═══════════════════════════════════════════ HEADER ═══ -->
161
- <header class="bg-panel border-b border-border px-5 h-14 flex items-center gap-3 flex-shrink-0 z-10">
162
- <!-- Logo -->
163
  <div class="flex items-center gap-2.5">
164
- <div class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0"
165
- style="background: linear-gradient(135deg,#1f6feb,#388bfd)">
166
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
167
  <path d="M9 17H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3"/>
168
  <path d="M13 21l2-2 4 4 2-2-4-4 2-2-6-1z"/>
169
  </svg>
170
  </div>
171
  <div>
172
- <div class="font-semibold text-white text-sm leading-tight">DeskFlow</div>
173
- <div class="text-xs" style="color:#8b949e">AI-powered IT Helpdesk</div>
174
  </div>
175
  </div>
176
-
177
- <!-- Model status -->
178
  <div id="model-status-wrap" class="ml-auto flex items-center gap-3">
179
- <div class="flex flex-col items-end gap-1">
180
- <div id="model-status" class="text-xs font-medium" style="color:#d29922">⏳ Loading AI…</div>
181
- <div id="model-progress-wrap" class="w-36 h-1 rounded-full hidden" style="background:#21262d">
182
- <div id="model-progress-bar" class="h-1 rounded-full" style="width:0%;background:#388bfd"></div>
183
- </div>
184
  </div>
 
185
  </div>
186
  </header>
187
 
188
- <!-- ═══════════════════════════════════════════ CHAT AREA ═══ -->
189
- <div id="messages" class="flex-1 overflow-y-auto px-4 py-6" style="max-height:calc(100vh - 7rem)">
190
 
191
  <!-- Empty state -->
192
- <div id="empty-state" class="flex flex-col items-center justify-center h-full gap-6 select-none" style="min-height:300px">
193
- <div class="flex flex-col items-center gap-3">
194
- <div class="w-14 h-14 rounded-2xl flex items-center justify-center"
195
- style="background:linear-gradient(135deg,#1f6feb22,#388bfd22);border:1px solid #388bfd44">
196
- <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#388bfd" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
197
- <path d="M9 17H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3"/>
198
- <path d="M13 21l2-2 4 4 2-2-4-4 2-2-6-1z"/>
199
- </svg>
200
- </div>
201
- <div>
202
- <h2 class="text-base font-semibold text-center" style="color:#e6edf3">How can I help you today?</h2>
203
- <p class="text-sm text-center mt-1" style="color:#8b949e">Describe your IT issue or pick a common topic below.</p>
204
- </div>
205
  </div>
206
- <div class="flex flex-wrap justify-center gap-2 max-w-lg">
207
- <button class="chip" onclick="useChip('My VPN is not connecting')">
208
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
209
- VPN issue
210
- </button>
211
- <button class="chip" onclick="useChip('I am locked out of my account')">
212
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
213
- Account locked
214
- </button>
215
- <button class="chip" onclick="useChip('My computer is not working properly')">
216
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
217
- Hardware problem
218
- </button>
219
- <button class="chip" onclick="useChip('My internet connection is slow or not working')">
220
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
221
- Network issue
222
- </button>
223
- <button class="chip" onclick="useChip('I need to request access to a system')">
224
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
225
- Access request
226
- </button>
227
- <button class="chip" onclick="useChip('I want to set up a new employee')">
228
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
229
- New hire onboarding
230
- </button>
231
  </div>
232
  </div>
233
 
234
- </div>
235
 
236
- <!-- ═══════════════════════════════════════════ INPUT BAR ═══ -->
237
- <div class="flex-shrink-0 px-4 pb-4 pt-2" style="background:#0d1117">
238
- <div class="flex items-center gap-2 rounded-2xl px-4 py-2.5 border"
239
- style="background:#161b22;border-color:#30363d;box-shadow:0 0 0 0 transparent;transition:box-shadow 0.15s"
240
- id="input-wrap">
241
- <input
242
- id="msg-input"
243
- type="text"
244
  placeholder="Describe your IT issue…"
245
- autocomplete="off"
246
- class="flex-1 bg-transparent text-sm outline-none"
247
- style="color:#e6edf3;caret-color:#388bfd"
248
- />
249
- <button id="send-btn"
250
- class="flex-shrink-0 w-8 h-8 rounded-xl flex items-center justify-center transition-all disabled:opacity-40"
251
- style="background:#1f6feb"
252
- >
253
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
254
  <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
255
  </svg>
256
  </button>
257
  </div>
258
- <p class="text-center text-xs mt-2" style="color:#484f58">DeskFlow uses on-device AI β€” your data never leaves your browser.</p>
259
- </div>
260
 
261
- <!-- ═══════════════════════════════════════════ WebGPU MODULE ═══ -->
262
- <script type="module">
263
- import { pipeline, TextStreamer, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.0/dist/transformers.min.js';
264
 
265
- env.allowLocalModels = false;
266
- if (env.backends?.onnx?.wasm) {
267
- env.backends.onnx.wasm.numThreads = 1;
268
- env.backends.onnx.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  }
270
 
271
- const MODEL_ID = 'HuggingFaceTB/SmolLM2-360M-Instruct';
272
- const DTYPE = 'q4';
273
 
274
- window._llm = { status: 'idle', generator: null, error: null };
 
 
 
 
 
 
275
 
276
- const statusEl = document.getElementById('model-status');
277
- const progressWrap = document.getElementById('model-progress-wrap');
278
- const progressBar = document.getElementById('model-progress-bar');
279
- const statusWrap = document.getElementById('model-status-wrap');
 
 
 
 
 
 
 
 
 
 
280
 
281
- function setStatusUI(text, color, progress) {
282
- statusEl.textContent = text;
283
- statusEl.style.color = color;
284
- if (progress != null) {
285
- progressWrap.classList.remove('hidden');
286
- progressBar.style.width = Math.round(progress) + '%';
287
- } else {
288
- progressWrap.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  }
290
  }
291
 
292
- if (!navigator.gpu) {
293
- window._llm.status = 'unavailable';
294
- setStatusUI('⚠️ Templates only (no WebGPU)', '#d29922', null);
295
- setTimeout(() => statusWrap.classList.add('hidden'), 4000);
296
- } else {
297
- setStatusUI('⏳ Loading AI…', '#d29922', 0);
298
- progressWrap.classList.remove('hidden');
299
 
300
- (async () => {
301
- window._llm.status = 'loading';
302
- try {
303
- window._llm.generator = await pipeline('text-generation', MODEL_ID, {
304
- dtype: DTYPE,
305
- device: 'webgpu',
306
- progress_callback: (p) => {
307
- if (p.status !== 'progress') return;
308
- const pct = p.progress ?? 0;
309
- setStatusUI(`⏳ Loading AI… ${Math.round(pct)}%`, '#d29922', pct);
310
- },
311
- });
312
- window._llm.status = 'ready';
313
- setStatusUI('βœ… AI model ready', '#3fb950', null);
314
- setTimeout(() => statusWrap.classList.add('hidden'), 3000);
315
- } catch (err) {
316
- console.error('[DeskFlow] Model load failed:', err);
317
- window._llm.status = 'error';
318
- window._llm.error = err.message;
319
- setStatusUI('⚠️ Templates only (AI unavailable)', '#d29922', null);
320
- setTimeout(() => statusWrap.classList.add('hidden'), 5000);
321
- }
322
- })();
323
  }
324
 
325
- window._llmGenerate = async function(systemPrompt, userPrompt, templateResponse, onToken, onDone, onFallback) {
326
- if (!window._llm.generator || window._llm.status !== 'ready') {
327
- onFallback(templateResponse);
328
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  }
330
- const messages = [
331
- { role: 'system', content: systemPrompt },
332
- { role: 'user', content: userPrompt },
333
- ];
334
- let accumulated = '';
335
- const streamer = new TextStreamer(window._llm.generator.tokenizer, {
336
- skip_prompt: true,
337
- skip_special_tokens: true,
338
- callback_function: (chunk) => { accumulated += chunk; onToken(accumulated); },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  try {
341
- await window._llm.generator(messages, { max_new_tokens: 512, streamer, do_sample: false });
342
- onDone(accumulated || templateResponse);
343
- } catch (err) {
344
- console.error('[DeskFlow] Inference error:', err);
345
- onFallback(templateResponse);
346
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  };
 
 
 
 
 
348
  </script>
349
 
350
- <!-- ═══════════════════════════════════════════ MAIN UI ═══ -->
351
  <script>
352
- const messagesEl = document.getElementById('messages');
353
- const msgInput = document.getElementById('msg-input');
354
- const sendBtn = document.getElementById('send-btn');
355
- const inputWrap = document.getElementById('input-wrap');
356
- const emptyState = document.getElementById('empty-state');
357
-
358
- // Input focus ring
359
- msgInput.addEventListener('focus', () => inputWrap.style.boxShadow = '0 0 0 3px rgba(56,139,253,0.25)');
360
- msgInput.addEventListener('blur', () => inputWrap.style.boxShadow = '0 0 0 0 transparent');
361
- msgInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) sendMessage(); });
362
- sendBtn.addEventListener('click', sendMessage);
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
- function useChip(text) {
365
- msgInput.value = text;
366
- msgInput.focus();
367
- sendMessage();
 
 
 
 
 
368
  }
369
 
370
- function hideEmptyState() {
371
- if (emptyState) emptyState.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  }
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  function scrollBottom() {
375
- messagesEl.scrollTop = messagesEl.scrollHeight;
 
 
 
 
 
 
 
 
 
 
376
  }
377
 
378
- // Bot row = avatar + bubble
379
- function addBotRow(innerHTML, extraBubbleClass = '') {
 
 
 
380
  const row = document.createElement('div');
381
  row.className = 'flex items-start gap-3 max-w-3xl';
382
 
@@ -386,15 +1310,12 @@ function addBotRow(innerHTML, extraBubbleClass = '') {
386
  avatar.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 17H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3"/><path d="M13 21l2-2 4 4 2-2-4-4 2-2-6-1z"/></svg>`;
387
 
388
  const bubble = document.createElement('div');
389
- bubble.className = 'rounded-2xl rounded-tl-sm px-4 py-3 text-sm leading-relaxed flex-1 ' + extraBubbleClass;
390
- bubble.style.cssText = 'background:#1c2128;border:1px solid #30363d;color:#adbac7;max-width:44rem';
391
  bubble.innerHTML = innerHTML;
392
 
393
  row.appendChild(avatar);
394
  row.appendChild(bubble);
395
-
396
- const wrap = document.createElement('div');
397
- wrap.className = 'flex justify-start';
398
  wrap.appendChild(row);
399
  messagesEl.appendChild(wrap);
400
  scrollBottom();
@@ -402,7 +1323,6 @@ function addBotRow(innerHTML, extraBubbleClass = '') {
402
  }
403
 
404
  function addUserBubble(text) {
405
- hideEmptyState();
406
  const wrap = document.createElement('div');
407
  wrap.className = 'flex justify-end';
408
  const bubble = document.createElement('div');
@@ -415,38 +1335,80 @@ function addUserBubble(text) {
415
  }
416
 
417
  function addTextBubble(text) {
418
- return addBotRow(`<span style="color:#adbac7">${escapeHtml(text)}</span>`);
 
 
 
 
419
  }
420
 
421
  function addTyping() {
422
  return addBotRow('<div class="flex items-center gap-1.5 py-1"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>');
423
  }
424
 
425
- function addMarkdownBubble(md) {
426
- addBotRow(`<div class="md-body">${marked.parse(md)}</div>`);
427
- }
 
 
428
 
429
- function createStreamingBubble() {
430
- const bubble = addBotRow('<div class="md-body streaming-cursor"></div>');
431
- const inner = bubble.querySelector('.md-body');
432
- return {
433
- update(md) { inner.className = 'md-body streaming-cursor'; inner.innerHTML = marked.parse(md); scrollBottom(); },
434
- done(md) { inner.className = 'md-body'; inner.innerHTML = marked.parse(md || ''); scrollBottom(); },
435
- remove() { bubble.closest('.flex.justify-start').remove(); },
436
- };
437
- }
438
 
439
- function escapeHtml(s) {
440
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  }
442
 
443
- function setLoading(on) {
444
- sendBtn.disabled = on;
445
- msgInput.disabled = on;
446
- sendBtn.style.opacity = on ? '0.4' : '1';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  }
448
 
449
- // ── Send message ──────────────────────────────────────────────────── //
450
  async function sendMessage() {
451
  const text = msgInput.value.trim();
452
  if (!text) return;
@@ -454,38 +1416,26 @@ async function sendMessage() {
454
  hideEmptyState();
455
  addUserBubble(text);
456
  msgInput.value = '';
 
457
  setLoading(true);
458
 
459
  const typingBubble = addTyping();
460
- let apiData;
461
-
462
- try {
463
- const res = await fetch('/api/message', {
464
- method: 'POST', headers: { 'Content-Type': 'application/json' },
465
- body: JSON.stringify({ message: text }),
466
- });
467
- apiData = await res.json();
468
- } catch (_err) {
469
- typingBubble.closest('.flex.justify-start').remove();
470
- addTextBubble('Sorry, something went wrong. Please try again.');
471
- setLoading(false);
472
- return;
473
- }
474
 
475
- typingBubble.closest('.flex.justify-start').remove();
 
476
 
477
- // 1. Show template response immediately β€” never block on LLM
478
- const chatBubble = addTextBubble(apiData.template_response);
479
 
480
- // 2. Show form immediately (don't wait for LLM)
481
- if (apiData.form_html) {
482
- injectForm(apiData.form_html, apiData.intent);
483
  } else {
484
  msgInput.focus();
485
  }
486
  setLoading(false);
487
 
488
- // 3. Enhance chat bubble with LLM in the background (non-blocking)
489
  const llmGenerate = window._llmGenerate;
490
  if (typeof llmGenerate !== 'function' || window._llm?.status !== 'ready') return;
491
 
@@ -493,117 +1443,114 @@ async function sendMessage() {
493
  const innerEl = chatBubble.querySelector('.md-body');
494
 
495
  llmGenerate(
496
- apiData.system_prompt,
497
- apiData.user_prompt,
498
- apiData.template_response,
499
- (acc) => {
500
- innerEl.className = 'md-body streaming-cursor';
501
- innerEl.innerHTML = marked.parse(acc);
502
- scrollBottom();
503
- },
504
- (final) => {
505
- innerEl.className = 'md-body';
506
- innerEl.innerHTML = marked.parse(final || apiData.template_response);
507
- scrollBottom();
508
- },
509
- (_t) => {
510
- chatBubble.textContent = apiData.template_response;
511
- },
512
  );
513
  }
514
 
515
- // ── Form injection ────────────────────────��───────────────────────── //
516
- function injectForm(formHtml, intent) {
517
- const outer = document.createElement('div');
518
- outer.className = 'flex items-start gap-3 max-w-3xl';
 
519
 
520
- const avatar = document.createElement('div');
521
- avatar.className = 'w-7 h-7 rounded-lg flex-shrink-0 flex items-center justify-center mt-0.5 invisible';
522
- outer.appendChild(avatar);
 
 
 
 
523
 
524
- const bubble = document.createElement('div');
525
- bubble.className = 'form-bubble rounded-2xl rounded-tl-sm px-5 py-4 text-sm flex-1';
526
- bubble.style.cssText = 'background:#1c2128;border:1px solid #30363d;max-width:44rem';
527
- bubble.innerHTML = formHtml;
528
- outer.appendChild(bubble);
529
 
530
- const wrap = document.createElement('div');
531
- wrap.className = 'flex justify-start';
532
- wrap.appendChild(outer);
533
- messagesEl.appendChild(wrap);
534
- scrollBottom();
535
 
536
- // Get the form ID from the data-formora-id wrapper (formora-core pattern)
537
- const formId = bubble.querySelector('[data-formora-id]')?.getAttribute('data-formora-id') || '';
 
 
 
538
 
539
- // Listen on window for formora:submit CustomEvent β€” matches formora-core renderer output
540
- async function onFormoraSubmit(e) {
541
- const { raw } = e.detail;
542
- // Parse the __formora__{...} message to get form_id and data
543
- let payload;
544
- try { payload = JSON.parse(raw.slice('__formora__'.length)); } catch { return; }
545
- if (payload.form_id !== formId) return; // not this form
 
 
 
546
 
547
- // One submission per form
548
- window.removeEventListener('formora:submit', onFormoraSubmit);
 
 
 
 
 
549
 
550
- const formData = payload.data;
551
- const typingBubble = addTyping();
552
- let apiData;
553
  try {
554
- const res = await fetch('/api/submit', {
555
- method: 'POST', headers: { 'Content-Type': 'application/json' },
556
- body: JSON.stringify({ form_id: formId, data: formData }),
 
 
 
 
557
  });
558
- apiData = await res.json();
559
- } catch (_err) {
560
- typingBubble.closest('.flex.justify-start').remove();
561
- addTextBubble('Failed to process your request. Please try again.');
562
- return;
563
- }
564
-
565
- // Collapse form bubble to a compact ticket pill
566
- const reqNum = apiData.request_number || '';
567
- bubble.className = 'rounded-xl px-4 py-2 text-xs font-medium flex items-center gap-2';
568
- bubble.style.cssText = 'background:#1c2128;border:1px solid #3fb95022;color:#3fb950;max-width:44rem';
569
- bubble.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg> Request <strong>${reqNum}</strong> submitted`;
570
-
571
- typingBubble.closest('.flex.justify-start').remove();
572
-
573
- async function resolveTicket(response, source) {
574
- try {
575
- await fetch('/api/resolve', {
576
- method: 'POST', headers: { 'Content-Type': 'application/json' },
577
- body: JSON.stringify({ ticket_id: apiData.ticket_id, request_number: apiData.request_number, form_id: formId, data: formData, response, source }),
578
- });
579
- } catch (err) { console.warn('[DeskFlow] /api/resolve failed:', err); }
580
  }
 
581
 
582
- const llmGenerate = window._llmGenerate;
583
- if (typeof llmGenerate !== 'function' || window._llm?.status !== 'ready') {
584
- addMarkdownBubble(apiData.template_response);
585
- resolveTicket(apiData.template_response, 'template');
586
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  }
588
-
589
- const streaming = createStreamingBubble();
590
- llmGenerate(
591
- apiData.system_prompt,
592
- apiData.user_prompt,
593
- apiData.template_response,
594
- (acc) => streaming.update(acc),
595
- (final) => { streaming.done(final); resolveTicket(final, 'webgpu'); },
596
- (template) => { streaming.remove(); addMarkdownBubble(template); resolveTicket(template, 'template'); },
597
- );
598
- scrollBottom();
599
- }
600
-
601
- window.addEventListener('formora:submit', onFormoraSubmit);
602
  }
603
-
604
- // collectFormData and setupStepWizard removed:
605
- // formora-core now handles data collection and multi-step navigation
606
- // via its own inline JavaScript, dispatching window formora:submit.
607
  </script>
608
 
609
  </body>
 
1
  <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 
20
  card: '#1c2128',
21
  border: '#30363d',
22
  muted: '#8b949e',
23
+ text: '#adbac7',
24
+ bright: '#e6edf3',
25
+ accent: '#388bfd',
26
+ 'accent-dark': '#1f6feb',
27
+ success: '#3fb950',
28
+ danger: '#f85149',
29
+ warning: '#d29922',
30
  }
31
  }
32
  }
33
  }
34
  </script>
35
+
36
  <style>
37
+ /* ── formora.css ── */
38
+ .frm-wrapper {
39
+ font-family: 'Inter', system-ui, sans-serif;
40
+ font-size: 0.875rem;
41
+ color: #adbac7;
42
+ width: 100%;
43
+ }
44
+ .frm-title {
45
+ font-size: 0.95rem;
46
+ font-weight: 600;
47
+ color: #e6edf3;
48
+ margin-bottom: 4px;
49
+ }
50
+ .frm-desc {
51
+ font-size: 0.8rem;
52
+ color: #8b949e;
53
+ margin-bottom: 12px;
54
+ }
55
+ .frm-progress-label {
56
+ font-size: 0.75rem;
57
+ color: #8b949e;
58
+ margin-bottom: 4px;
59
+ }
60
+ .frm-progress-bar {
61
+ height: 4px;
62
+ background: #30363d;
63
+ border-radius: 2px;
64
+ margin-bottom: 16px;
65
+ overflow: hidden;
66
+ }
67
+ .frm-progress-fill {
68
+ height: 100%;
69
+ background: linear-gradient(90deg, #1f6feb, #388bfd);
70
+ border-radius: 2px;
71
+ transition: width 0.3s ease;
72
+ }
73
+ .frm-step-title {
74
+ font-size: 0.8rem;
75
+ font-weight: 600;
76
+ color: #58a6ff;
77
+ text-transform: uppercase;
78
+ letter-spacing: 0.05em;
79
+ margin-bottom: 10px;
80
+ }
81
+ .frm-field {
82
+ margin-bottom: 12px;
83
+ }
84
+ .frm-label {
85
+ display: block;
86
+ font-size: 0.8rem;
87
+ font-weight: 500;
88
+ color: #8b949e;
89
+ margin-bottom: 4px;
90
+ }
91
+ .frm-req {
92
+ color: #f85149;
93
+ margin-left: 2px;
94
+ }
95
+ .frm-input {
96
+ width: 100%;
97
+ background: #0d1117;
98
+ border: 1px solid #30363d;
99
+ border-radius: 6px;
100
+ color: #e6edf3;
101
+ font-size: 0.875rem;
102
+ font-family: inherit;
103
+ padding: 7px 10px;
104
+ box-sizing: border-box;
105
+ transition: border-color 0.15s, box-shadow 0.15s;
106
+ outline: none;
107
+ -webkit-appearance: none;
108
+ appearance: none;
109
+ }
110
+ .frm-input:focus {
111
+ border-color: #1f6feb;
112
+ box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.15);
113
+ }
114
+ .frm-input.frm-invalid {
115
+ border-color: #f85149;
116
+ box-shadow: 0 0 0 3px rgba(248, 81, 73, 0.15);
117
+ }
118
+ .frm-textarea {
119
+ resize: vertical;
120
+ min-height: 80px;
121
+ }
122
+ .frm-select {
123
+ cursor: pointer;
124
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
125
+ background-repeat: no-repeat;
126
+ background-position: right 10px center;
127
+ padding-right: 30px;
128
+ }
129
+ .frm-radio-group {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 6px;
133
+ margin-top: 4px;
134
+ }
135
+ .frm-radio-label {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 8px;
139
+ cursor: pointer;
140
+ font-size: 0.875rem;
141
+ color: #adbac7;
142
+ }
143
+ .frm-radio-label input[type="radio"] {
144
+ accent-color: #388bfd;
145
+ width: 14px;
146
+ height: 14px;
147
+ cursor: pointer;
148
+ flex-shrink: 0;
149
+ }
150
+ .frm-checkbox-label {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 8px;
154
+ cursor: pointer;
155
+ font-size: 0.875rem;
156
+ color: #adbac7;
157
+ }
158
+ .frm-checkbox-label input[type="checkbox"] {
159
+ accent-color: #388bfd;
160
+ width: 14px;
161
+ height: 14px;
162
+ cursor: pointer;
163
+ flex-shrink: 0;
164
+ }
165
+ .frm-error-msg {
166
+ font-size: 0.75rem;
167
+ color: #f85149;
168
+ margin-top: 3px;
169
+ }
170
+ .frm-actions,
171
+ .frm-nav {
172
+ display: flex;
173
+ gap: 8px;
174
+ margin-top: 16px;
175
+ justify-content: flex-end;
176
+ }
177
+ .frm-btn-submit,
178
+ .frm-btn-next {
179
+ background: linear-gradient(135deg, #1f6feb, #388bfd);
180
+ color: #fff;
181
+ border: none;
182
+ border-radius: 6px;
183
+ padding: 7px 16px;
184
+ font-size: 0.875rem;
185
+ font-weight: 500;
186
+ font-family: inherit;
187
+ cursor: pointer;
188
+ transition: opacity 0.15s;
189
+ }
190
+ .frm-btn-submit:hover,
191
+ .frm-btn-next:hover {
192
+ opacity: 0.88;
193
+ }
194
+ .frm-btn-back {
195
+ background: transparent;
196
+ color: #8b949e;
197
+ border: 1px solid #30363d;
198
+ border-radius: 6px;
199
+ padding: 7px 14px;
200
+ font-size: 0.875rem;
201
+ font-family: inherit;
202
+ cursor: pointer;
203
+ transition: border-color 0.15s, color 0.15s;
204
+ }
205
+ .frm-btn-back:hover {
206
+ border-color: #58a6ff;
207
+ color: #58a6ff;
208
+ }
209
+ .frm-success {
210
+ font-size: 0.875rem;
211
+ color: #3fb950;
212
+ background: rgba(63, 185, 80, 0.08);
213
+ border: 1px solid rgba(63, 185, 80, 0.2);
214
+ border-radius: 6px;
215
+ padding: 10px 14px;
216
+ margin-top: 8px;
217
+ }
218
+ .frm-select option {
219
+ background: #1c2128;
220
+ color: #e6edf3;
221
+ }
222
+
223
+ /* ── App styles ── */
224
+ * { box-sizing: border-box; }
225
+ body { margin: 0; background: #0d1117; color: #adbac7; font-family: 'Inter', system-ui, sans-serif; }
226
+ html, body, #root { height: 100%; }
227
 
228
+ ::-webkit-scrollbar { width: 6px; }
 
229
  ::-webkit-scrollbar-track { background: transparent; }
230
+ ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
231
+
232
+ .typing-dot {
233
+ width: 6px; height: 6px; background: #58a6ff;
234
+ border-radius: 50%; animation: typing-bounce 1.2s infinite;
235
+ }
236
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
237
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
238
+ @keyframes typing-bounce {
239
+ 0%,60%,100% { transform: translateY(0); opacity: 0.4; }
240
+ 30% { transform: translateY(-4px); opacity: 1; }
241
+ }
242
+
243
+ .streaming-cursor::after {
244
+ content: 'β–‹'; display: inline-block;
245
+ animation: blink 0.8s step-end infinite; color: #388bfd;
246
+ }
247
+ @keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
248
+
249
+ .md-body h2 { font-size: 0.9rem; font-weight: 600; color: #e6edf3; margin: 0 0 8px; }
250
+ .md-body h3 { font-size: 0.85rem; font-weight: 600; color: #e6edf3; margin: 8px 0 4px; }
251
+ .md-body p { margin: 4px 0; }
252
+ .md-body ol,.md-body ul { margin: 6px 0; padding-left: 18px; }
253
+ .md-body li { margin: 3px 0; }
254
  .md-body strong { color: #e6edf3; font-weight: 600; }
255
+ .md-body code { background: #0d1117; border: 1px solid #30363d; border-radius: 3px; padding: 1px 5px; font-size: 0.8rem; color: #79c0ff; font-family: 'SF Mono', Consolas, monospace; }
256
+ .md-body blockquote { border-left: 3px solid #d29922; margin: 8px 0; padding: 4px 10px; background: rgba(210,153,34,0.07); border-radius: 0 4px 4px 0; color: #d29922; }
257
+ .md-body blockquote p { margin: 0; }
258
+
259
+ .ticket-pill {
260
+ display: inline-flex; align-items: center; gap: 6px;
261
+ background: rgba(56,139,253,0.1); border: 1px solid rgba(56,139,253,0.25);
262
+ border-radius: 20px; padding: 4px 12px; font-size: 0.75rem;
263
+ color: #58a6ff; cursor: default; user-select: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  }
265
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  #model-progress-bar { transition: width 0.4s ease; }
267
 
 
268
  .chip {
269
  display: inline-flex; align-items: center; gap: 6px;
270
+ background: #1c2128; border: 1px solid #30363d; border-radius: 20px;
271
+ padding: 6px 14px; font-size: 0.8rem; color: #8b949e;
272
+ cursor: pointer; transition: border-color .15s, color .15s;
273
  white-space: nowrap;
274
  }
275
+ .chip:hover { border-color: #388bfd; color: #58a6ff; }
276
 
277
+ .inc-pill {
278
+ font-size: 0.7rem; color: #3fb950;
279
+ background: rgba(63,185,80,0.08); border: 1px solid rgba(63,185,80,0.2);
280
+ border-radius: 12px; padding: 2px 8px; display: inline-block; margin-bottom: 6px;
281
+ }
282
  </style>
283
  </head>
284
+ <body class="flex flex-col h-full">
285
 
286
+ <!-- ── Header ────────────────────────────────────────────────────────────── -->
287
+ <header class="flex items-center px-4 py-3 border-b shrink-0" style="background:#161b22;border-color:#21262d">
 
 
 
288
  <div class="flex items-center gap-2.5">
289
+ <div class="w-8 h-8 rounded-xl flex items-center justify-center" style="background:linear-gradient(135deg,#1f6feb,#388bfd)">
 
290
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
291
  <path d="M9 17H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3"/>
292
  <path d="M13 21l2-2 4 4 2-2-4-4 2-2-6-1z"/>
293
  </svg>
294
  </div>
295
  <div>
296
+ <div class="font-semibold text-sm" style="color:#e6edf3">DeskFlow</div>
297
+ <div class="text-xs" style="color:#8b949e">IT Support Assistant</div>
298
  </div>
299
  </div>
 
 
300
  <div id="model-status-wrap" class="ml-auto flex items-center gap-3">
301
+ <div id="model-progress-wrap" class="w-36 h-1 rounded-full hidden" style="background:#21262d">
302
+ <div id="model-progress-bar" class="h-1 rounded-full" style="width:0%;background:#388bfd"></div>
 
 
 
303
  </div>
304
+ <div id="model-status" class="text-xs font-medium" style="color:#d29922">⏳ Loading AI…</div>
305
  </div>
306
  </header>
307
 
308
+ <!-- ── Chat area ─────────────────────────────────────────────────────────── -->
309
+ <main id="messages" class="flex-1 overflow-y-auto px-4 py-6 flex flex-col gap-4">
310
 
311
  <!-- Empty state -->
312
+ <div id="empty-state" class="flex flex-col items-center justify-center flex-1 gap-6 py-12">
313
+ <div class="w-16 h-16 rounded-2xl flex items-center justify-center" style="background:linear-gradient(135deg,#1f6feb,#388bfd)">
314
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
315
+ <path d="M9 17H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3"/>
316
+ <path d="M13 21l2-2 4 4 2-2-4-4 2-2-6-1z"/>
317
+ </svg>
318
+ </div>
319
+ <div class="text-center">
320
+ <h2 class="text-lg font-semibold mb-1" style="color:#e6edf3">Hi, I'm DeskFlow</h2>
321
+ <p class="text-sm" style="color:#8b949e">Your AI-powered IT support assistant.<br/>Tell me what's going on β€” I'll help you fix it.</p>
 
 
 
322
  </div>
323
+ <div class="flex flex-wrap gap-2 justify-center max-w-lg">
324
+ <button class="chip" data-msg="My VPN isn't connecting">πŸ”’ VPN not connecting</button>
325
+ <button class="chip" data-msg="My account is locked">πŸ”‘ Account locked</button>
326
+ <button class="chip" data-msg="Laptop screen is broken">πŸ’» Hardware issue</button>
327
+ <button class="chip" data-msg="Outlook is not working">πŸ“§ Email issue</button>
328
+ <button class="chip" data-msg="I need access to a shared drive">πŸ—‚οΈ Access request</button>
329
+ <button class="chip" data-msg="I'm a new joiner, need setup">πŸ‘‹ New joiner setup</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  </div>
331
  </div>
332
 
333
+ </main>
334
 
335
+ <!-- ── Input bar ─────────────────────────────────────────────────────────── -->
336
+ <footer class="shrink-0 px-4 py-3 border-t" style="background:#161b22;border-color:#21262d">
337
+ <div class="flex items-end gap-2 max-w-3xl mx-auto">
338
+ <textarea id="msg-input" rows="1"
 
 
 
 
339
  placeholder="Describe your IT issue…"
340
+ class="flex-1 resize-none rounded-xl px-4 py-3 text-sm outline-none transition"
341
+ style="background:#1c2128;border:1px solid #30363d;color:#e6edf3;max-height:140px;line-height:1.5;font-family:inherit"
342
+ oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,140)+'px'"
343
+ ></textarea>
344
+ <button id="send-btn" class="rounded-xl px-4 py-3 text-sm font-medium shrink-0 transition"
345
+ style="background:linear-gradient(135deg,#1f6feb,#388bfd);color:white;border:none;cursor:pointer">
346
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
 
 
347
  <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
348
  </svg>
349
  </button>
350
  </div>
351
+ <p class="text-center text-xs mt-2" style="color:#30363d">DeskFlow Β· Powered by WebGPU Β· All processing is local</p>
352
+ </footer>
353
 
354
+ <!-- ── Marked.js ─────────────────────────────────────────────────────────── -->
355
+ <script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
 
356
 
357
+ <!-- ── formora.js (inlined) ──────────────────────────────────────────────── -->
358
+ <script>
359
+ /**
360
+ * formora.js β€” JavaScript port of the formora form builder.
361
+ */
362
+
363
+ const FORMORA_PREFIX = '__formora__';
364
+
365
+ // ─── Validation Rules ────────────────────────────────────────────────────── //
366
+
367
+ class Rule {
368
+ static required(msg = 'This field is required') {
369
+ return { type: 'required', message: msg };
370
+ }
371
+ static minLength(n, msg) {
372
+ return { type: 'minLength', value: n, message: msg || `Minimum ${n} characters required` };
373
+ }
374
+ static maxLength(n, msg) {
375
+ return { type: 'maxLength', value: n, message: msg || `Maximum ${n} characters allowed` };
376
+ }
377
+ static min(n, msg) {
378
+ return { type: 'min', value: n, message: msg || `Must be at least ${n}` };
379
+ }
380
+ static max(n, msg) {
381
+ return { type: 'max', value: n, message: msg || `Must be at most ${n}` };
382
+ }
383
+ static email(msg = 'Enter a valid email address') {
384
+ return { type: 'email', message: msg };
385
+ }
386
+ static regex(pattern, msg = 'Invalid format') {
387
+ return { type: 'regex', value: pattern, message: msg };
388
+ }
389
  }
390
 
391
+ // ─── Condition for conditional field visibility ─────────────────────────── //
 
392
 
393
+ class Condition {
394
+ constructor(fieldId, operator, value) {
395
+ this.fieldId = fieldId;
396
+ this.operator = operator;
397
+ this.value = value;
398
+ }
399
+ }
400
 
401
+ // ─── Form Builder ────────────────────────────────────────────────────────── //
402
+
403
+ class Form {
404
+ constructor(formId) {
405
+ this._id = formId;
406
+ this._title = null;
407
+ this._description = null;
408
+ this._submitLabel = 'Submit';
409
+ this._successMsg = 'Thank you! Your request has been received.';
410
+ this._fields = [];
411
+ this._steps = [];
412
+ this._multiStep = false;
413
+ this._currentStep = null;
414
+ }
415
 
416
+ title(t) { this._title = t; return this; }
417
+ description(d) { this._description = d; return this; }
418
+ submitLabel(l) { this._submitLabel = l; return this; }
419
+ successMessage(m) { this._successMsg = m; return this; }
420
+
421
+ step(title = null) {
422
+ this._multiStep = true;
423
+ this._steps.push({ title });
424
+ this._currentStep = this._steps.length - 1;
425
+ return this;
426
+ }
427
+
428
+ _add(field) {
429
+ field.stepIndex = this._multiStep ? this._currentStep : null;
430
+ this._fields.push(field);
431
+ return this;
432
+ }
433
+
434
+ text(id, label, { required = false, placeholder = '', rules = [], condition = null } = {}) {
435
+ return this._add({ type: 'text', id, label, required, placeholder, rules, condition });
436
+ }
437
+ email(id, label, { required = false, placeholder = '', rules = [], condition = null } = {}) {
438
+ return this._add({ type: 'email', id, label, required, placeholder, rules, condition });
439
+ }
440
+ number(id, label, { required = false, min = null, max = null, rules = [], condition = null } = {}) {
441
+ return this._add({ type: 'number', id, label, required, min, max, rules, condition });
442
+ }
443
+ textarea(id, label, { required = false, rows = 4, placeholder = '', rules = [], condition = null } = {}) {
444
+ return this._add({ type: 'textarea', id, label, required, rows, placeholder, rules, condition });
445
+ }
446
+ select(id, label, options, { required = false, rules = [], condition = null } = {}) {
447
+ const opts = options.map(o => Array.isArray(o) ? { label: o[0], value: o[1] } : { label: o, value: o });
448
+ return this._add({ type: 'select', id, label, required, options: opts, rules, condition });
449
+ }
450
+ radio(id, label, options, { required = false, condition = null } = {}) {
451
+ const opts = options.map(o => Array.isArray(o) ? { label: o[0], value: o[1] } : { label: o, value: o });
452
+ return this._add({ type: 'radio', id, label, required, options: opts, condition });
453
+ }
454
+ checkbox(id, label, { condition = null } = {}) {
455
+ return this._add({ type: 'checkbox', id, label, condition });
456
+ }
457
+ hidden(id, value) {
458
+ return this._add({ type: 'hidden', id, value });
459
+ }
460
+
461
+ build() {
462
+ return renderForm(this);
463
  }
464
  }
465
 
466
+ // ─── HTML Renderer ───────────────────────────────────────────────────────── //
 
 
 
 
 
 
467
 
468
+ function _fEsc(s) {
469
+ return String(s)
470
+ .replace(/&/g, '&amp;')
471
+ .replace(/</g, '&lt;')
472
+ .replace(/>/g, '&gt;')
473
+ .replace(/"/g, '&quot;');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  }
475
 
476
+ function renderRules(rules) {
477
+ return JSON.stringify(rules || []);
478
+ }
479
+
480
+ function renderCondition(cond) {
481
+ if (!cond) return '';
482
+ return `data-condition='${JSON.stringify(cond)}'`;
483
+ }
484
+
485
+ function renderField(f) {
486
+ const condAttr = renderCondition(f.condition);
487
+ const rulesAttr = `data-rules='${renderRules(f.rules)}'`;
488
+ const req = f.required ? 'required' : '';
489
+ const reqMark = f.required ? '<span class="frm-req">*</span>' : '';
490
+ const wrapStyle = f.condition ? ' style="display:none"' : '';
491
+
492
+ const wrap = (inner) =>
493
+ `<div class="frm-field" data-field-id="${_fEsc(f.id)}" ${condAttr}${wrapStyle}>${inner}</div>`;
494
+
495
+ const label = `<label class="frm-label" for="${_fEsc(f.id)}">${_fEsc(f.label)}${reqMark}</label>`;
496
+
497
+ switch (f.type) {
498
+ case 'hidden':
499
+ return `<input type="hidden" id="${_fEsc(f.id)}" name="${_fEsc(f.id)}" value="${_fEsc(f.value)}" />`;
500
+ case 'text':
501
+ case 'email':
502
+ return wrap(`${label}
503
+ <input class="frm-input" type="${f.type}" id="${_fEsc(f.id)}" name="${_fEsc(f.id)}"
504
+ placeholder="${_fEsc(f.placeholder || '')}" ${req} ${rulesAttr} />`);
505
+ case 'number':
506
+ return wrap(`${label}
507
+ <input class="frm-input" type="number" id="${_fEsc(f.id)}" name="${_fEsc(f.id)}"
508
+ ${f.min != null ? `min="${f.min}"` : ''} ${f.max != null ? `max="${f.max}"` : ''}
509
+ ${req} ${rulesAttr} />`);
510
+ case 'textarea':
511
+ return wrap(`${label}
512
+ <textarea class="frm-input frm-textarea" id="${_fEsc(f.id)}" name="${_fEsc(f.id)}"
513
+ rows="${f.rows || 4}" placeholder="${_fEsc(f.placeholder || '')}" ${req} ${rulesAttr}></textarea>`);
514
+ case 'select':
515
+ return wrap(`${label}
516
+ <select class="frm-input frm-select" id="${_fEsc(f.id)}" name="${_fEsc(f.id)}" ${req} ${rulesAttr}>
517
+ <option value="">Select…</option>
518
+ ${f.options.map(o => `<option value="${_fEsc(o.value)}">${_fEsc(o.label)}</option>`).join('')}
519
+ </select>`);
520
+ case 'radio':
521
+ return wrap(`${label}
522
+ <div class="frm-radio-group">
523
+ ${f.options.map(o => `
524
+ <label class="frm-radio-label">
525
+ <input type="radio" name="${_fEsc(f.id)}" value="${_fEsc(o.value)}" ${req} />
526
+ <span>${_fEsc(o.label)}</span>
527
+ </label>`).join('')}
528
+ </div>`);
529
+ case 'checkbox':
530
+ return wrap(`<label class="frm-checkbox-label">
531
+ <input type="checkbox" id="${_fEsc(f.id)}" name="${_fEsc(f.id)}" />
532
+ <span>${_fEsc(f.label)}</span>
533
+ </label>`);
534
+ default:
535
+ return '';
536
+ }
537
+ }
538
+
539
+ function renderForm(form) {
540
+ const id = form._id;
541
+ const fields = form._fields;
542
+
543
+ if (form._multiStep && form._steps.length > 0) {
544
+ return renderMultiStep(form);
545
  }
546
+
547
+ const fieldHtml = fields.map(renderField).join('\n');
548
+
549
+ return `
550
+ <div class="frm-wrapper" data-formora-id="${_fEsc(id)}">
551
+ ${form._title ? `<div class="frm-title">${_fEsc(form._title)}</div>` : ''}
552
+ ${form._description ? `<div class="frm-desc">${_fEsc(form._description)}</div>` : ''}
553
+ <form class="frm-form" data-formora-id="${_fEsc(id)}" novalidate>
554
+ ${fieldHtml}
555
+ <div class="frm-actions">
556
+ <button type="submit" class="frm-btn-submit">${_fEsc(form._submitLabel)}</button>
557
+ </div>
558
+ <div class="frm-success" style="display:none">${_fEsc(form._successMsg)}</div>
559
+ <div class="frm-errors" style="display:none"></div>
560
+ </form>
561
+ </div>`;
562
+ }
563
+
564
+ function renderMultiStep(form) {
565
+ const id = form._id;
566
+ const steps = form._steps;
567
+ const fields = form._fields;
568
+ const total = steps.length;
569
+
570
+ const stepGroups = steps.map((_, i) => ({
571
+ title: steps[i].title,
572
+ fields: fields.filter(f => f.stepIndex === i),
573
+ }));
574
+
575
+ const stepsHtml = stepGroups.map((step, i) => `
576
+ <div class="frm-step" data-step="${i}" ${i > 0 ? 'style="display:none"' : ''}>
577
+ ${step.title ? `<div class="frm-step-title">${_fEsc(step.title)}</div>` : ''}
578
+ ${step.fields.map(renderField).join('\n')}
579
+ </div>`).join('\n');
580
+
581
+ return `
582
+ <div class="frm-wrapper" data-formora-id="${_fEsc(id)}">
583
+ ${form._title ? `<div class="frm-title">${_fEsc(form._title)}</div>` : ''}
584
+ ${form._description ? `<div class="frm-desc">${_fEsc(form._description)}</div>` : ''}
585
+ <div class="frm-progress-label">Step 1 of ${total}</div>
586
+ <div class="frm-progress-bar"><div class="frm-progress-fill" id="${_fEsc(id)}-progress" style="width:${Math.round(100/total)}%"></div></div>
587
+ <form class="frm-form" data-formora-id="${_fEsc(id)}" novalidate>
588
+ ${stepsHtml}
589
+ <div class="frm-nav">
590
+ <button type="button" class="frm-btn-back" style="display:none">Back</button>
591
+ <button type="button" class="frm-btn-next">Next</button>
592
+ <button type="submit" class="frm-btn-submit" style="display:none">${_fEsc(form._submitLabel)}</button>
593
+ </div>
594
+ <div class="frm-success" style="display:none">${_fEsc(form._successMsg)}</div>
595
+ <div class="frm-errors" style="display:none"></div>
596
+ </form>
597
+ </div>`;
598
+ }
599
+
600
+ // ─── Runtime: validation + submission ────────────────────────────────────── //
601
+
602
+ function attachForms(container) {
603
+ container.querySelectorAll('form[data-formora-id]').forEach(form => {
604
+ _attachConditions(form);
605
+ _attachMultiStep(form);
606
+ form.addEventListener('submit', e => {
607
+ e.preventDefault();
608
+ if (!_validate(form)) return;
609
+ const result = _collect(form);
610
+ _showSuccess(form);
611
+ window.dispatchEvent(new CustomEvent('formora:submit', {
612
+ detail: { raw: result.toMessage(), parsed: result },
613
+ }));
614
+ });
615
  });
616
+ }
617
+
618
+ function _attachConditions(form) {
619
+ const condFields = form.querySelectorAll('[data-condition]');
620
+ condFields.forEach(el => {
621
+ const cond = JSON.parse(el.dataset.condition);
622
+ const watch = form.querySelector(`[name="${CSS.escape(cond.fieldId)}"]`);
623
+ if (!watch) return;
624
+ const update = () => {
625
+ const val = watch.value;
626
+ const show = _evalCondition(cond, val);
627
+ el.style.display = show ? '' : 'none';
628
+ if (!show) {
629
+ el.querySelectorAll('input,select,textarea').forEach(i => i.value = '');
630
+ }
631
+ };
632
+ watch.addEventListener('change', update);
633
+ watch.addEventListener('input', update);
634
+ update();
635
+ });
636
+ }
637
+
638
+ function _evalCondition(cond, val) {
639
+ switch (cond.operator) {
640
+ case 'eq': return val === cond.value;
641
+ case 'neq': return val !== cond.value;
642
+ case 'contains': return val.includes(cond.value);
643
+ case 'gt': return parseFloat(val) > parseFloat(cond.value);
644
+ case 'lt': return parseFloat(val) < parseFloat(cond.value);
645
+ default: return true;
646
+ }
647
+ }
648
+
649
+ function _attachMultiStep(form) {
650
+ const stepEls = form.querySelectorAll('.frm-step');
651
+ if (!stepEls.length) return;
652
+
653
+ const total = stepEls.length;
654
+ let current = 0;
655
+ const formId = form.getAttribute('data-formora-id') || form.closest('[data-formora-id]')?.getAttribute('data-formora-id') || '';
656
+ const progress = document.getElementById(`${formId}-progress`);
657
+ const label = form.closest('.frm-wrapper')?.querySelector('.frm-progress-label');
658
+ const btnBack = form.querySelector('.frm-btn-back');
659
+ const btnNext = form.querySelector('.frm-btn-next');
660
+ const btnSub = form.querySelector('.frm-btn-submit');
661
+
662
+ const goTo = (i) => {
663
+ stepEls[current].style.display = 'none';
664
+ current = i;
665
+ stepEls[current].style.display = '';
666
+ if (label) label.textContent = `Step ${current + 1} of ${total}`;
667
+ if (progress) progress.style.width = `${Math.round((current + 1) * 100 / total)}%`;
668
+ if (btnBack) btnBack.style.display = current > 0 ? '' : 'none';
669
+ if (btnNext) btnNext.style.display = current < total - 1 ? '' : 'none';
670
+ if (btnSub) btnSub.style.display = current === total - 1 ? '' : 'none';
671
+ };
672
+
673
+ if (btnNext) btnNext.addEventListener('click', () => {
674
+ if (!_validate(form, stepEls[current])) return;
675
+ if (current < total - 1) goTo(current + 1);
676
+ });
677
+ if (btnBack) btnBack.addEventListener('click', () => {
678
+ if (current > 0) goTo(current - 1);
679
+ });
680
+
681
+ goTo(0);
682
+ }
683
+
684
+ function _validate(form, scope = form) {
685
+ let valid = true;
686
+ scope.querySelectorAll('input,select,textarea').forEach(el => {
687
+ _clearError(el);
688
+ const rules = JSON.parse(el.dataset.rules || '[]');
689
+ const val = el.value.trim();
690
+
691
+ for (const rule of rules) {
692
+ let err = null;
693
+ if (rule.type === 'required' && !val) err = rule.message;
694
+ if (rule.type === 'minLength' && val && val.length < rule.value) err = rule.message;
695
+ if (rule.type === 'maxLength' && val && val.length > rule.value) err = rule.message;
696
+ if (rule.type === 'min' && val && parseFloat(val) < rule.value) err = rule.message;
697
+ if (rule.type === 'max' && val && parseFloat(val) > rule.value) err = rule.message;
698
+ if (rule.type === 'email' && val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) err = rule.message;
699
+ if (rule.type === 'regex' && val && !new RegExp(rule.value).test(val)) err = rule.message;
700
+ if (err) { _showError(el, err); valid = false; break; }
701
+ }
702
+
703
+ if (el.required && !val) {
704
+ _showError(el, 'This field is required');
705
+ valid = false;
706
+ }
707
+ });
708
+ return valid;
709
+ }
710
+
711
+ function _showError(el, msg) {
712
+ el.classList.add('frm-invalid');
713
+ const err = document.createElement('div');
714
+ err.className = 'frm-error-msg';
715
+ err.textContent = msg;
716
+ el.parentNode.appendChild(err);
717
+ }
718
+
719
+ function _clearError(el) {
720
+ el.classList.remove('frm-invalid');
721
+ el.parentNode?.querySelectorAll('.frm-error-msg').forEach(e => e.remove());
722
+ }
723
+
724
+ function _collect(form) {
725
+ const formId = form.getAttribute('data-formora-id') || form.closest('[data-formora-id]')?.getAttribute('data-formora-id') || '';
726
+ const data = {};
727
+ const typed = {};
728
+
729
+ form.querySelectorAll('input,select,textarea').forEach(el => {
730
+ if (!el.name) return;
731
+ if (el.type === 'radio' && !el.checked) return;
732
+ if (el.type === 'checkbox') {
733
+ data[el.name] = el.checked ? 'true' : 'false';
734
+ typed[el.name] = el.checked;
735
+ return;
736
+ }
737
+ data[el.name] = el.value;
738
+ const num = Number(el.value);
739
+ typed[el.name] = el.type === 'number' && !isNaN(num) ? num : el.value;
740
+ });
741
+
742
+ return new FormResult(formId, data, typed);
743
+ }
744
+
745
+ function _showSuccess(form) {
746
+ form.querySelectorAll('.frm-field,.frm-nav,.frm-step').forEach(el => el.style.display = 'none');
747
+ form.querySelector('.frm-actions')?.style && (form.querySelector('.frm-actions').style.display = 'none');
748
+ const suc = form.querySelector('.frm-success');
749
+ if (suc) suc.style.display = '';
750
+ }
751
+
752
+ // ─── FormResult ──────────────────────────────────────────────────────────── //
753
+
754
+ class FormResult {
755
+ constructor(formId, data, typedData) {
756
+ this.formId = formId;
757
+ this.data = data;
758
+ this.typedData = typedData;
759
+ }
760
+
761
+ toMessage() {
762
+ return FORMORA_PREFIX + JSON.stringify({
763
+ form_id: this.formId,
764
+ data: this.data,
765
+ typed_data: this.typedData,
766
+ });
767
+ }
768
+
769
+ asText() {
770
+ return Object.entries(this.typedData)
771
+ .filter(([, v]) => v !== '' && v !== false)
772
+ .map(([k, v]) => `${k.replace(/_/g, ' ')}: ${v}`)
773
+ .join(', ');
774
+ }
775
+ }
776
+
777
+ function isFormoraMessage(msg) {
778
+ return typeof msg === 'string' && msg.startsWith(FORMORA_PREFIX);
779
+ }
780
+
781
+ function parseFormoraMessage(msg) {
782
+ if (!isFormoraMessage(msg)) return null;
783
  try {
784
+ const payload = JSON.parse(msg.slice(FORMORA_PREFIX.length));
785
+ return new FormResult(payload.form_id, payload.data, payload.typed_data);
786
+ } catch {
787
+ return null;
 
788
  }
789
+ }
790
+ </script>
791
+
792
+ <!-- ── forms.js (inlined) ────────────────────────────────────────────────── -->
793
+ <script>
794
+ function buildVpnForm() {
795
+ return new Form('vpn_issue')
796
+ .title('VPN Issue')
797
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required(), Rule.minLength(2)] })
798
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
799
+ .select('os', 'Operating System', [['Windows 10/11', 'windows'], ['macOS', 'macos'], ['Linux', 'linux']], { required: true })
800
+ .text('vpn_client', 'VPN Client', { placeholder: 'e.g. Cisco AnyConnect, GlobalProtect' })
801
+ .textarea('issue', 'Describe the Issue', { required: true, rows: 3, rules: [Rule.required()] })
802
+ .select('urgency', 'Urgency', [['Low', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Critical', 'critical']], { required: true })
803
+ .submitLabel('Submit')
804
+ .successMessage('Your VPN issue has been logged. Check your inbox for next steps.')
805
+ .build();
806
+ }
807
+
808
+ function buildAccountForm() {
809
+ return new Form('account_issue')
810
+ .title('Account / Login Issue')
811
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
812
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
813
+ .select('issue_type', 'Issue Type', [
814
+ ['Account locked', 'locked'],
815
+ ['Forgot password', 'forgot_password'],
816
+ ['MFA not working', 'mfa'],
817
+ ['Access denied', 'access_denied'],
818
+ ['Other', 'other'],
819
+ ], { required: true })
820
+ .textarea('details', 'Additional Details', { rows: 3 })
821
+ .select('urgency', 'Urgency', [['Low', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Critical', 'critical']], { required: true })
822
+ .submitLabel('Submit')
823
+ .successMessage('Account issue logged. Our team will be in touch shortly.')
824
+ .build();
825
+ }
826
+
827
+ function buildHardwareForm() {
828
+ return new Form('hardware_issue')
829
+ .title('Hardware Issue')
830
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
831
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
832
+ .select('device_type', 'Device Type', [
833
+ ['Laptop', 'laptop'], ['Desktop', 'desktop'], ['Monitor', 'monitor'],
834
+ ['Keyboard / Mouse', 'peripheral'], ['Printer', 'printer'],
835
+ ['Headset / Webcam', 'audio_video'], ['Other', 'other'],
836
+ ], { required: true })
837
+ .text('device_model', 'Device Model / Asset Tag', { placeholder: 'e.g. Dell XPS 15, Asset #12345' })
838
+ .textarea('issue', 'Describe the Problem', { required: true, rows: 3, rules: [Rule.required()] })
839
+ .select('urgency', 'Urgency', [['Low', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Critical', 'critical']], { required: true })
840
+ .submitLabel('Submit')
841
+ .successMessage('Hardware issue logged. IT will assess and follow up.')
842
+ .build();
843
+ }
844
+
845
+ function buildSoftwareForm() {
846
+ return new Form('software_issue')
847
+ .title('Software Issue')
848
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
849
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
850
+ .text('application', 'Application Name', { required: true, placeholder: 'e.g. Microsoft Teams, Slack', rules: [Rule.required()] })
851
+ .select('issue_type', 'Issue Type', [
852
+ ['App crashing', 'crash'], ['Cannot install', 'install'],
853
+ ['Licence / activation', 'licence'], ['Performance issues', 'perf'],
854
+ ['Error message', 'error'], ['Other', 'other'],
855
+ ], { required: true })
856
+ .text('error_code', 'Error Code (if any)', { placeholder: 'Optional' })
857
+ .textarea('details', 'Describe the Issue', { required: true, rows: 3, rules: [Rule.required()] })
858
+ .select('urgency', 'Urgency', [['Low', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Critical', 'critical']], { required: true })
859
+ .submitLabel('Submit')
860
+ .successMessage("Software issue logged. We'll be in touch.")
861
+ .build();
862
+ }
863
+
864
+ function buildNetworkForm() {
865
+ return new Form('network_issue')
866
+ .title('Network / Connectivity Issue')
867
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
868
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
869
+ .select('connection_type', 'Connection Type', [
870
+ ['Wi-Fi', 'wifi'], ['Ethernet', 'ethernet'], ['VPN-only', 'vpn'], ['Both Wi-Fi and Ethernet', 'both'],
871
+ ], { required: true })
872
+ .select('issue_type', 'Issue Type', [
873
+ ['No internet', 'no_internet'], ['Slow connection', 'slow'],
874
+ ['Cannot reach internal systems', 'internal'], ['DNS issues', 'dns'],
875
+ ['Packet loss / drops', 'drops'], ['Other', 'other'],
876
+ ], { required: true })
877
+ .textarea('details', 'Additional Details', { rows: 3 })
878
+ .select('urgency', 'Urgency', [['Low', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Critical', 'critical']], { required: true })
879
+ .submitLabel('Submit')
880
+ .successMessage('Network issue logged. Our team will investigate.')
881
+ .build();
882
+ }
883
+
884
+ function buildEmailForm() {
885
+ return new Form('email_issue')
886
+ .title('Email / M365 Issue')
887
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
888
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
889
+ .select('issue_type', 'Issue Type', [
890
+ ['Outlook not working', 'outlook'], ['Cannot send/receive email', 'send_recv'],
891
+ ['Calendar issues', 'calendar'], ['Teams issue', 'teams'],
892
+ ['Shared mailbox', 'shared_mailbox'], ['Other', 'other'],
893
+ ], { required: true })
894
+ .textarea('details', 'Describe the Issue', { required: true, rows: 3, rules: [Rule.required()] })
895
+ .select('urgency', 'Urgency', [['Low', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Critical', 'critical']], { required: true })
896
+ .submitLabel('Submit')
897
+ .successMessage("Email issue logged. We'll get this sorted.")
898
+ .build();
899
+ }
900
+
901
+ function buildAccessForm() {
902
+ return new Form('access_request')
903
+ .title('Access Request')
904
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
905
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
906
+ .email('manager_email', "Manager's Email", { required: true, rules: [Rule.required(), Rule.email()] })
907
+ .text('system_resource', 'System / Resource Requested', { required: true, placeholder: 'e.g. Salesforce, HR Drive, Azure portal', rules: [Rule.required()] })
908
+ .textarea('business_justification', 'Business Justification', { required: true, rows: 3, rules: [Rule.required()] })
909
+ .select('access_level', 'Access Level', [['Read Only', 'read'], ['Read/Write', 'readwrite'], ['Admin', 'admin']], { required: true })
910
+ .submitLabel('Submit Request')
911
+ .successMessage('Access request submitted. Your manager will receive an approval email.')
912
+ .build();
913
+ }
914
+
915
+ function buildProcurementForm() {
916
+ return new Form('procurement_request')
917
+ .title('IT Procurement Request')
918
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
919
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
920
+ .email('manager_email', 'Approving Manager Email', { required: true, rules: [Rule.required(), Rule.email()] })
921
+ .text('item_description', 'Item / Equipment Required', { required: true, placeholder: 'e.g. MacBook Pro 14", USB-C hub', rules: [Rule.required()] })
922
+ .number('quantity', 'Quantity', { required: true, min: 1, rules: [Rule.required(), Rule.min(1)] })
923
+ .textarea('business_justification', 'Business Justification', { required: true, rows: 3, rules: [Rule.required()] })
924
+ .submitLabel('Submit Request')
925
+ .successMessage('Purchase request submitted. Approval email sent to your manager.')
926
+ .build();
927
+ }
928
+
929
+ function buildOnboardingForm() {
930
+ return new Form('onboarding_setup')
931
+ .title('New Employee Onboarding')
932
+ .step('Personal Details')
933
+ .text('employee_name', 'Full Name', { required: true, rules: [Rule.required()] })
934
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
935
+ .text('department', 'Department', { required: true, rules: [Rule.required()] })
936
+ .text('job_title', 'Job Title', { required: true, rules: [Rule.required()] })
937
+ .step('Setup Preferences')
938
+ .select('device_preference', 'Device Preference', [
939
+ ['MacBook Pro', 'macbook_pro'], ['MacBook Air', 'macbook_air'],
940
+ ['Windows Laptop', 'windows_laptop'], ['Desktop', 'desktop'],
941
+ ], { required: true })
942
+ .select('start_date_readiness', 'Start Date', [
943
+ ['Within 1 week', '1week'], ['1–2 weeks', '2weeks'],
944
+ ['2–4 weeks', '4weeks'], ['More than a month', 'month_plus'],
945
+ ])
946
+ .textarea('additional_requirements', 'Any Additional Requirements?', { rows: 3 })
947
+ .submitLabel('Submit Onboarding Request')
948
+ .successMessage('Onboarding request received! IT will prepare your setup before day one.')
949
+ .build();
950
+ }
951
+
952
+ function buildGenericForm() {
953
+ return new Form('generic_incident')
954
+ .title('IT Support Request')
955
+ .text('employee_name', 'Your Name', { required: true, rules: [Rule.required()] })
956
+ .email('email', 'Work Email', { required: true, rules: [Rule.required(), Rule.email()] })
957
+ .textarea('issue', 'Describe Your Issue', { required: true, rows: 4, rules: [Rule.required(), Rule.minLength(10)] })
958
+ .select('urgency', 'Urgency', [['Low', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Critical', 'critical']], { required: true })
959
+ .submitLabel('Submit')
960
+ .successMessage('Issue logged. Our team will be in touch shortly.')
961
+ .build();
962
+ }
963
+
964
+ const FORM_MAP = {
965
+ vpn: buildVpnForm,
966
+ account: buildAccountForm,
967
+ hardware: buildHardwareForm,
968
+ software: buildSoftwareForm,
969
+ network: buildNetworkForm,
970
+ email: buildEmailForm,
971
+ access: buildAccessForm,
972
+ procurement: buildProcurementForm,
973
+ onboarding: buildOnboardingForm,
974
+ generic_incident: buildGenericForm,
975
  };
976
+
977
+ function dispatchForm(intent) {
978
+ const builder = FORM_MAP[intent];
979
+ return builder ? builder() : null;
980
+ }
981
  </script>
982
 
983
+ <!-- ── intent.js (inlined) ───────────────────────────────────────────────── -->
984
  <script>
985
+ const GREETING_RE = /^\s*(hi+|hey+|hello+|howdy|yo+|sup|good (morning|afternoon|evening)|thanks?|thank you|cheers|bye|goodbye|ok|okay|cool|great|sure|got it|noted)\s*[!?.]*\s*$/i;
986
+
987
+ const INTENT_KEYWORDS = {
988
+ vpn: ['vpn', 'remote', 'tunnel', 'connect remotely', 'cisco anyconnect', 'wireguard', 'openvpn', 'remote access'],
989
+ account: ['password', 'locked', 'login', 'reset', 'access denied', 'sign in', 'forgot', 'credentials', 'mfa', '2fa', 'account'],
990
+ hardware: ['laptop', 'keyboard', 'mouse', 'monitor', 'printer', 'screen', 'device', 'battery', 'charger', 'headset', 'webcam'],
991
+ software: ['install', 'app', 'software', 'crash', 'update', 'license', 'application', 'error code', 'not opening', 'uninstall'],
992
+ network: ['internet', 'wifi', 'slow', 'network', 'connection', 'no internet', 'packet loss', 'latency', 'dns', 'ethernet'],
993
+ email: ['email', 'outlook', 'calendar', 'teams', 'mailbox', 'meeting', 'distribution list', 'signature', 'spam'],
994
+ access: ['permission', 'folder', 'drive', 'share', 'access', 'shared drive', 'read only', 'write access', 'file server'],
995
+ procurement: ['order', 'request', 'new laptop', 'equipment', 'purchase', 'budget', 'quote', 'hardware request'],
996
+ onboarding: ['new joiner', 'joined', 'first day', 'setup', 'onboard', 'new employee', 'getting started'],
997
+ };
998
+
999
+ function detectIntent(message) {
1000
+ const stripped = message.trim();
1001
+ if (GREETING_RE.test(stripped)) return 'greeting';
1002
+
1003
+ const lower = stripped.toLowerCase();
1004
+ const scores = {};
1005
+
1006
+ for (const [intent, keywords] of Object.entries(INTENT_KEYWORDS)) {
1007
+ scores[intent] = keywords.filter(kw => lower.includes(kw)).length;
1008
+ }
1009
 
1010
+ const best = Object.keys(scores).reduce((a, b) => scores[a] >= scores[b] ? a : b);
1011
+ const bestScore = scores[best];
1012
+
1013
+ if (bestScore === 0) {
1014
+ if (stripped.split(/\s+/).length <= 4) return 'greeting';
1015
+ return 'generic_incident';
1016
+ }
1017
+
1018
+ return best;
1019
  }
1020
 
1021
+ const INTRO = {
1022
+ greeting: "Hey! I'm DeskFlow, your IT support assistant. What can I help you with today?",
1023
+ vpn: "VPN troubles β€” not fun! Let me grab a few details so I can get you sorted.",
1024
+ account: "Account issues are stressful, but we'll get you sorted quickly.",
1025
+ hardware: "Hardware gremlins! Let me pull up a quick form to get the specifics.",
1026
+ software: "Software being awkward? Let me ask you a few things so I can give you the right fix.",
1027
+ network: "Connection problems are the worst. Let me get some details to help diagnose this.",
1028
+ email: "Email issues β€” always at the worst time! Let me get some details.",
1029
+ access: "Access requests sorted quickly β€” fill in the details below.",
1030
+ procurement: "Happy to help with your purchase request.",
1031
+ onboarding: "Welcome! Let's get everything set up properly.",
1032
+ generic_incident: "I'm here to help! Let me take the details so I can find the best solution.",
1033
+ };
1034
+
1035
+ function getIntroMessage(intent) {
1036
+ return INTRO[intent] || INTRO.generic_incident;
1037
+ }
1038
+
1039
+ const RUNBOOKS = {
1040
+ vpn_issue: `VPN Runbook:
1041
+ - Likely causes: credential expiry, MFA drift, firewall blocking UDP 1194/TCP 443, client misconfiguration.
1042
+ - Steps: verify internet; restart VPN client from tray; check credentials; re-sync MFA app; reboot; flush DNS (ipconfig /flushdns on Windows); disable conflicting firewall/AV; reinstall VPN client.
1043
+ - Escalate if: unresolved after reinstall, multiple users affected, or Critical urgency.`,
1044
+
1045
+ account_issue: `Account Runbook:
1046
+ - Likely causes: too many failed logins (auto-lockout), expired password, MFA device time drift.
1047
+ - Steps: wait 15 min for auto-unlock; try self-service password reset; re-sync MFA app; contact IT to manually unlock; request Temporary Access Pass (TAP) for MFA bypass.
1048
+ - Escalate: High/Critical urgency or if account is admin-level.`,
1049
+
1050
+ hardware_issue: `Hardware Runbook:
1051
+ - Likely causes: driver fault, physical damage, hardware component failure.
1052
+ - Steps: power cycle device; test peripheral on another port/machine; check Device Manager for errors; update driver; run built-in diagnostics (Dell SupportAssist, Apple Diagnostics).
1053
+ - Escalate: physical damage or persistent failure β€” request hardware assessment/replacement.`,
1054
+
1055
+ software_issue: `Software Runbook:
1056
+ - Likely causes: corrupt install, missing dependencies, licence expiry, insufficient permissions.
1057
+ - Steps: close fully (check tray); run as Administrator; check for updates; uninstall, delete AppData, reinstall; verify disk space (>10 GB free); check licence validity.
1058
+ - Escalate: business-critical application, High/Critical urgency.`,
1059
+
1060
+ network_issue: `Network Runbook:
1061
+ - Likely causes: DHCP lease failure, DNS misconfiguration, physical link issue.
1062
+ - Steps: check other devices; restart network adapter; ipconfig /release + /renew + /flushdns; try Ethernet if on Wi-Fi; nslookup to test DNS; reboot router/switch (home).
1063
+ - Escalate: office-wide outage, infrastructure suspected.`,
1064
+
1065
+ email_issue: `Email/M365 Runbook:
1066
+ - Likely causes: Outlook profile corruption, OST file corruption, M365 service outage.
1067
+ - Steps: check M365 Service Health for outages; test Outlook Web Access (OWA); restart Outlook (close from tray); run in Safe Mode (outlook.exe /safe); create new Outlook profile; clear Teams cache + reinstall.
1068
+ - Escalate: if OWA also failing, likely tenant-wide issue β€” contact Microsoft.`,
1069
+
1070
+ access_request: `Access Request Runbook:
1071
+ - Process: manager approval required within 48 h; IT provisions within 1 business day after approval.
1072
+ - Provisioning: role-based access following least-privilege; audit log entry created.
1073
+ - Escalation: urgent access β€” provide business justification; emergency access via IT helpdesk.`,
1074
+
1075
+ procurement_request: `Procurement Runbook:
1076
+ - Process: manager approval β†’ PO raised β†’ procurement team orders β†’ delivery 5–10 business days (in-stock).
1077
+ - Status updates emailed at: Approved β†’ PO Raised β†’ Dispatched β†’ Delivered.
1078
+ - Escalation: urgent procurement requires budget-holder sign-off and IT director approval.`,
1079
+
1080
+ onboarding_setup: `Onboarding Runbook:
1081
+ - Timeline: M365 account created 1–2 days before start; device imaged and configured before day one.
1082
+ - Day one: IT walkthrough β€” login, MFA setup, VPN, key applications.
1083
+ - Access provisioned based on manager approvals (role-based).
1084
+ - IT checks in on day 3 for outstanding issues.`,
1085
+
1086
+ generic_incident: `General IT Runbook:
1087
+ - Start: restart affected app; if persisting, reboot device; note error messages/codes.
1088
+ - Check IT status page for known incidents; check if other users affected.
1089
+ - Collect: device model, OS, application version, steps to reproduce, error screenshots.
1090
+ - Escalate: High/Critical urgency or if multiple users impacted.`,
1091
+ };
1092
+
1093
+ function getRunbookContext(formId) {
1094
+ return RUNBOOKS[formId] || RUNBOOKS.generic_incident;
1095
+ }
1096
+
1097
+ function buildChatSystemPrompt() {
1098
+ return (
1099
+ 'You are DeskFlow, a friendly IT support assistant. ' +
1100
+ 'Reply conversationally in 1–3 short sentences. ' +
1101
+ 'Do NOT provide technical steps yet β€” just respond naturally to what the user said.'
1102
+ );
1103
  }
1104
 
1105
+ function buildResolutionSystemPrompt(formId, requestNumber) {
1106
+ const context = getRunbookContext(formId);
1107
+ const ticketLine = requestNumber
1108
+ ? `The user's request has been logged as **${requestNumber}**. Open your reply by mentioning this number warmly. `
1109
+ : '';
1110
+
1111
+ return (
1112
+ `You are DeskFlow, a friendly IT support assistant. ${ticketLine}Be concise, warm, and practical.\n\n` +
1113
+ `Reply with: 1) Likely cause 2) Numbered resolution steps 3) Escalation note if urgency is High or Critical.\n\n` +
1114
+ `Runbook context:\n${context}`
1115
+ );
1116
+ }
1117
+
1118
+ function formResultToPrompt(formResult) {
1119
+ const title = (formResult.formId || 'IT Support Request').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
1120
+ const lines = [`IT Support Request: ${title}`];
1121
+ for (const [k, v] of Object.entries(formResult.typedData || {})) {
1122
+ if (v !== '' && v !== false && v != null) {
1123
+ lines.push(`- ${k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}: ${v}`);
1124
+ }
1125
+ }
1126
+ return lines.join('\n');
1127
+ }
1128
+
1129
+ const TEMPLATES = {
1130
+ vpn_issue: `## VPN Issue β€” Suggested Resolution
1131
+
1132
+ **Likely cause:** VPN client configuration issue, credential expiry, or network port blocking.
1133
+
1134
+ **Steps to resolve:**
1135
+ 1. Verify your internet connection is working.
1136
+ 2. Restart the VPN client completely (close from system tray).
1137
+ 3. Ensure your username and password are correct and not expired.
1138
+ 4. Re-sync your MFA authenticator app β€” check device clock is accurate.
1139
+ 5. Reboot your device and try again.
1140
+ 6. On Windows: run \`ipconfig /flushdns\` in an elevated command prompt.
1141
+ 7. Check that your firewall is not blocking the VPN port (UDP 1194 / TCP 443).
1142
+ 8. Reinstall the VPN client from the IT software portal.`,
1143
+
1144
+ account_issue: `## Account Issue β€” Suggested Resolution
1145
+
1146
+ **Likely cause:** Account lockout, expired password, or MFA misconfiguration.
1147
+
1148
+ **Steps to resolve:**
1149
+ 1. Wait 15 minutes β€” most accounts auto-unlock after a lockout period.
1150
+ 2. Verify Caps Lock is off and you are using the correct username format.
1151
+ 3. Attempt a self-service password reset at the IT portal.
1152
+ 4. Re-sync your MFA authenticator app.
1153
+ 5. Contact IT to manually unlock if auto-unlock has not worked.
1154
+ 6. If MFA is the issue, IT can provide a Temporary Access Pass (TAP).`,
1155
+
1156
+ hardware_issue: `## Hardware Issue β€” Suggested Resolution
1157
+
1158
+ **Likely cause:** Driver issue, physical damage, or hardware component failure.
1159
+
1160
+ **Steps to resolve:**
1161
+ 1. Power cycle the device completely (hold power for 10 seconds, then restart).
1162
+ 2. For peripherals: try a different USB port and test on another machine.
1163
+ 3. Check Device Manager for driver errors (yellow exclamation marks).
1164
+ 4. Update the device driver from the manufacturer's website.
1165
+ 5. Run built-in diagnostics (Dell SupportAssist, HP Support Assistant, or Apple Diagnostics).
1166
+ 6. If the issue persists, contact IT for a hardware assessment.`,
1167
+
1168
+ software_issue: `## Software Issue β€” Suggested Resolution
1169
+
1170
+ **Likely cause:** Corrupt installation, missing dependencies, licence expiry, or permissions issue.
1171
+
1172
+ **Steps to resolve:**
1173
+ 1. Close the application completely (check the system tray) and reopen.
1174
+ 2. Run the application as Administrator.
1175
+ 3. Check for available updates and install them.
1176
+ 4. Reinstall: uninstall, delete leftover app data, then reinstall.
1177
+ 5. Verify you have enough disk space (minimum 10 GB free).
1178
+ 6. Check that your software licence is still valid.`,
1179
+
1180
+ network_issue: `## Network Issue β€” Suggested Resolution
1181
+
1182
+ **Likely cause:** DHCP lease failure, DNS misconfiguration, or physical connectivity issue.
1183
+
1184
+ **Steps to resolve:**
1185
+ 1. Check if other devices on the same network are affected.
1186
+ 2. Restart your network adapter.
1187
+ 3. On Windows: run \`ipconfig /release\`, then \`/renew\`, then \`/flushdns\`.
1188
+ 4. Try connecting via Ethernet if you are on Wi-Fi.
1189
+ 5. Test DNS: run \`nslookup google.com\` in Command Prompt.
1190
+ 6. Reboot your router/switch if you are at home.`,
1191
+
1192
+ email_issue: `## Email / Communications Issue β€” Suggested Resolution
1193
+
1194
+ **Likely cause:** Outlook profile corruption, OST file corruption, or M365 service issue.
1195
+
1196
+ **Steps to resolve:**
1197
+ 1. Check the Microsoft 365 service health page for known outages.
1198
+ 2. Test Outlook Web Access (OWA) β€” if OWA works, the issue is with the desktop client.
1199
+ 3. Restart Outlook from the system tray.
1200
+ 4. Run Outlook in Safe Mode: Win+R β†’ \`outlook.exe /safe\`.
1201
+ 5. Create a new Outlook profile: Control Panel β†’ Mail β†’ Show Profiles β†’ Add.
1202
+ 6. For Teams issues: clear the Teams cache and reinstall.`,
1203
+
1204
+ access_request: `## Access Request β€” Next Steps
1205
+
1206
+ **Your access request has been received.**
1207
+
1208
+ 1. An approval email will be sent to your manager.
1209
+ 2. Your manager has **48 hours** to approve or decline.
1210
+ 3. Once approved, IT will provision access within **1 business day**.
1211
+ 4. You will receive a confirmation email when access has been granted.`,
1212
+
1213
+ procurement_request: `## Procurement Request β€” Next Steps
1214
+
1215
+ **Your purchase request has been received.**
1216
+
1217
+ 1. An approval request will be sent to your approving manager.
1218
+ 2. Upon approval, IT Procurement will raise a Purchase Order (PO).
1219
+ 3. Standard delivery: **5–10 business days** for in-stock items.
1220
+ 4. Status updates emailed at each stage: Approved β†’ PO Raised β†’ Dispatched β†’ Delivered.`,
1221
+
1222
+ onboarding_setup: `## Onboarding Setup β€” What to Expect
1223
+
1224
+ **Your onboarding request has been received.**
1225
+
1226
+ 1. **Account creation**: Your M365 account and email will be created 1–2 days before start date.
1227
+ 2. **Device provisioning**: Your laptop/desktop will be imaged and configured before day one.
1228
+ 3. **Day-one**: IT will walk you through login, MFA setup, and VPN connection.
1229
+ 4. **Access**: All requested system access provisioned based on your manager's approvals.
1230
+ 5. **Follow-up**: IT will check in on day 3 to address any outstanding issues.`,
1231
+
1232
+ generic_incident: `## IT Incident β€” Suggested Resolution
1233
+
1234
+ **Likely cause:** Based on your description, this may be a software, network, or configuration issue.
1235
+
1236
+ **Steps to resolve:**
1237
+ 1. Restart the affected application or service.
1238
+ 2. Reboot your device if the issue persists.
1239
+ 3. Check if other users are affected.
1240
+ 4. Note any error messages or codes.
1241
+ 5. Check the IT status page for known incidents.`,
1242
+ };
1243
+
1244
+ const URGENCY_NOTE = {
1245
+ high: '\n\n> **Escalation**: Marked **High** urgency. If unresolved within 1 hour, contact the IT helpdesk directly.',
1246
+ critical: '\n\n> **Escalation**: Marked **CRITICAL**. Contact the IT helpdesk **immediately** or call the emergency support line.',
1247
+ };
1248
+
1249
+ function generateTemplateResponse(formResult, requestNumber = '') {
1250
+ const formId = formResult.formId || 'generic_incident';
1251
+ const urgency = String((formResult.typedData || {}).urgency || '').toLowerCase();
1252
+ let response = TEMPLATES[formId] || TEMPLATES.generic_incident;
1253
+
1254
+ if (URGENCY_NOTE[urgency]) response += URGENCY_NOTE[urgency];
1255
+ if (requestNumber) {
1256
+ response = `I've logged your request as **${requestNumber}**. Here's what I recommend:\n\n` + response;
1257
+ }
1258
+ return response;
1259
+ }
1260
+ </script>
1261
+
1262
+ <!-- ── Main app logic ─────────────────────────────────────────────────────── -->
1263
+ <script>
1264
+ // ── DOM refs ──────────────────────────────────────────────────────────────── //
1265
+ const messagesEl = document.getElementById('messages');
1266
+ const emptyState = document.getElementById('empty-state');
1267
+ const msgInput = document.getElementById('msg-input');
1268
+ const sendBtn = document.getElementById('send-btn');
1269
+ const statusEl = document.getElementById('model-status');
1270
+ const progressWrap = document.getElementById('model-progress-wrap');
1271
+ const progressBar = document.getElementById('model-progress-bar');
1272
+ const statusWrap = document.getElementById('model-status-wrap');
1273
+
1274
+ // ── Ticket counter ────────────────────────────────────────────────────────── //
1275
+ let _ticketSeq = Math.floor(Math.random() * 9000) + 1000;
1276
+ function nextTicket() { return `INC-${++_ticketSeq}`; }
1277
+
1278
+ // ── LLM reference (set by WebGPU module) ─────────────────────────────────── //
1279
+ window._llm = { status: 'idle', generator: null };
1280
+ window._llmGenerate = null;
1281
+
1282
+ // ── Helpers ───────────────────────────────────────────────────────────────── //
1283
+ function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
1284
+
1285
  function scrollBottom() {
1286
+ requestAnimationFrame(() => messagesEl.scrollTop = messagesEl.scrollHeight);
1287
+ }
1288
+
1289
+ function hideEmptyState() {
1290
+ emptyState.style.display = 'none';
1291
+ }
1292
+
1293
+ function setLoading(on) {
1294
+ sendBtn.disabled = on;
1295
+ msgInput.disabled = on;
1296
+ sendBtn.style.opacity = on ? '0.5' : '1';
1297
  }
1298
 
1299
+ // ── Bot row factory ───────────────────────────────────────────────────────── //
1300
+ function addBotRow(innerHTML) {
1301
+ const wrap = document.createElement('div');
1302
+ wrap.className = 'flex justify-start';
1303
+
1304
  const row = document.createElement('div');
1305
  row.className = 'flex items-start gap-3 max-w-3xl';
1306
 
 
1310
  avatar.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 17H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3"/><path d="M13 21l2-2 4 4 2-2-4-4 2-2-6-1z"/></svg>`;
1311
 
1312
  const bubble = document.createElement('div');
1313
+ bubble.className = 'rounded-2xl rounded-tl-sm px-4 py-3 text-sm leading-relaxed';
1314
+ bubble.style.cssText = 'background:#1c2128;border:1px solid #30363d;color:#adbac7;max-width:44rem;flex:1';
1315
  bubble.innerHTML = innerHTML;
1316
 
1317
  row.appendChild(avatar);
1318
  row.appendChild(bubble);
 
 
 
1319
  wrap.appendChild(row);
1320
  messagesEl.appendChild(wrap);
1321
  scrollBottom();
 
1323
  }
1324
 
1325
  function addUserBubble(text) {
 
1326
  const wrap = document.createElement('div');
1327
  wrap.className = 'flex justify-end';
1328
  const bubble = document.createElement('div');
 
1335
  }
1336
 
1337
  function addTextBubble(text) {
1338
+ return addBotRow(`<span style="color:#adbac7">${esc(text)}</span>`);
1339
+ }
1340
+
1341
+ function addMarkdownBubble(md) {
1342
+ return addBotRow(`<div class="md-body">${marked.parse(md)}</div>`);
1343
  }
1344
 
1345
  function addTyping() {
1346
  return addBotRow('<div class="flex items-center gap-1.5 py-1"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>');
1347
  }
1348
 
1349
+ // ── Form injection ────────────────────────────────────────────────────────── //
1350
+ function injectForm(formHtml, intent) {
1351
+ const bubble = addBotRow('');
1352
+ bubble.style.padding = '12px';
1353
+ bubble.innerHTML = formHtml;
1354
 
1355
+ const formEl = bubble.querySelector('form[data-formora-id]');
1356
+ const formId = formEl?.getAttribute('data-formora-id');
1357
+
1358
+ attachForms(bubble);
 
 
 
 
 
1359
 
1360
+ function onFormoraSubmit(e) {
1361
+ const { parsed } = e.detail;
1362
+ if (parsed.formId !== formId) return;
1363
+
1364
+ window.removeEventListener('formora:submit', onFormoraSubmit);
1365
+
1366
+ const requestNumber = nextTicket();
1367
+
1368
+ const pill = document.createElement('div');
1369
+ pill.className = 'inc-pill';
1370
+ pill.textContent = `βœ“ ${requestNumber}`;
1371
+ bubble.insertBefore(pill, bubble.firstChild);
1372
+
1373
+ handleFormSubmit(parsed, requestNumber, intent);
1374
+ }
1375
+
1376
+ window.addEventListener('formora:submit', onFormoraSubmit);
1377
+ scrollBottom();
1378
  }
1379
 
1380
+ // ── Form submit β†’ LLM resolution ─────────────────────────────────────────── //
1381
+ function handleFormSubmit(formResult, requestNumber, intent) {
1382
+ const systemPrompt = buildResolutionSystemPrompt(formResult.formId, requestNumber);
1383
+ const userPrompt = formResultToPrompt(formResult);
1384
+ const templateResponse = generateTemplateResponse(formResult, requestNumber);
1385
+
1386
+ const typing = addTyping();
1387
+
1388
+ function showTemplate() {
1389
+ typing.closest('.flex.justify-start')?.remove();
1390
+ addMarkdownBubble(templateResponse);
1391
+ }
1392
+
1393
+ const llmGenerate = window._llmGenerate;
1394
+ if (typeof llmGenerate !== 'function' || window._llm?.status !== 'ready') {
1395
+ showTemplate();
1396
+ return;
1397
+ }
1398
+
1399
+ typing.closest('.flex.justify-start')?.remove();
1400
+ const chatBubble = addBotRow('<div class="md-body streaming-cursor"></div>');
1401
+ const innerEl = chatBubble.querySelector('.md-body');
1402
+
1403
+ llmGenerate(
1404
+ systemPrompt, userPrompt, templateResponse,
1405
+ (acc) => { innerEl.className = 'md-body streaming-cursor'; innerEl.innerHTML = marked.parse(acc); scrollBottom(); },
1406
+ (final) => { innerEl.className = 'md-body'; innerEl.innerHTML = marked.parse(final || templateResponse); scrollBottom(); },
1407
+ (_t) => { chatBubble.remove(); addMarkdownBubble(templateResponse); },
1408
+ );
1409
  }
1410
 
1411
+ // ── Send message ──────────────────────────────────────────────────────────── //
1412
  async function sendMessage() {
1413
  const text = msgInput.value.trim();
1414
  if (!text) return;
 
1416
  hideEmptyState();
1417
  addUserBubble(text);
1418
  msgInput.value = '';
1419
+ msgInput.style.height = 'auto';
1420
  setLoading(true);
1421
 
1422
  const typingBubble = addTyping();
1423
+ const intent = detectIntent(text);
1424
+ const templateChat = getIntroMessage(intent);
1425
+ const formHtml = dispatchForm(intent);
 
 
 
 
 
 
 
 
 
 
 
1426
 
1427
+ await new Promise(r => setTimeout(r, 180));
1428
+ typingBubble.closest('.flex.justify-start')?.remove();
1429
 
1430
+ const chatBubble = addTextBubble(templateChat);
 
1431
 
1432
+ if (formHtml) {
1433
+ injectForm(formHtml, intent);
 
1434
  } else {
1435
  msgInput.focus();
1436
  }
1437
  setLoading(false);
1438
 
 
1439
  const llmGenerate = window._llmGenerate;
1440
  if (typeof llmGenerate !== 'function' || window._llm?.status !== 'ready') return;
1441
 
 
1443
  const innerEl = chatBubble.querySelector('.md-body');
1444
 
1445
  llmGenerate(
1446
+ buildChatSystemPrompt(), text, templateChat,
1447
+ (acc) => { innerEl.className = 'md-body streaming-cursor'; innerEl.innerHTML = marked.parse(acc); scrollBottom(); },
1448
+ (final) => { innerEl.className = 'md-body'; innerEl.innerHTML = marked.parse(final || templateChat); scrollBottom(); },
1449
+ (_t) => { chatBubble.textContent = templateChat; },
 
 
 
 
 
 
 
 
 
 
 
 
1450
  );
1451
  }
1452
 
1453
+ // ── Event listeners ───────────────────────────────────────────────────────── //
1454
+ msgInput.addEventListener('keydown', e => {
1455
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
1456
+ });
1457
+ sendBtn.addEventListener('click', sendMessage);
1458
 
1459
+ document.querySelectorAll('.chip').forEach(chip => {
1460
+ chip.addEventListener('click', () => {
1461
+ msgInput.value = chip.dataset.msg;
1462
+ sendMessage();
1463
+ });
1464
+ });
1465
+ </script>
1466
 
1467
+ <!-- ── WebGPU / LLM module ───────────────────────────────────────────────── -->
1468
+ <script type="module">
1469
+ import { pipeline, TextStreamer, env }
1470
+ from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.0/dist/transformers.min.js';
 
1471
 
1472
+ env.allowLocalModels = false;
1473
+ if (env.backends?.onnx?.wasm) {
1474
+ env.backends.onnx.wasm.numThreads = 1;
1475
+ env.backends.onnx.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/';
1476
+ }
1477
 
1478
+ const MODEL_ID = 'HuggingFaceTB/SmolLM2-360M-Instruct';
1479
+ const statusEl = document.getElementById('model-status');
1480
+ const progressWrap= document.getElementById('model-progress-wrap');
1481
+ const progressBar = document.getElementById('model-progress-bar');
1482
+ const statusWrap = document.getElementById('model-status-wrap');
1483
 
1484
+ function setStatusUI(text, color, pct) {
1485
+ statusEl.textContent = text;
1486
+ statusEl.style.color = color;
1487
+ if (pct != null) {
1488
+ progressWrap.classList.remove('hidden');
1489
+ progressBar.style.width = Math.round(pct) + '%';
1490
+ } else {
1491
+ progressWrap.classList.add('hidden');
1492
+ }
1493
+ }
1494
 
1495
+ if (!navigator.gpu) {
1496
+ window._llm.status = 'unavailable';
1497
+ setStatusUI('⚠️ Templates only (no WebGPU)', '#d29922', null);
1498
+ setTimeout(() => statusWrap.classList.add('hidden'), 4000);
1499
+ } else {
1500
+ setStatusUI('⏳ Loading AI…', '#d29922', 0);
1501
+ progressWrap.classList.remove('hidden');
1502
 
1503
+ (async () => {
1504
+ window._llm.status = 'loading';
 
1505
  try {
1506
+ window._llm.generator = await pipeline('text-generation', MODEL_ID, {
1507
+ dtype: 'q4',
1508
+ device: 'webgpu',
1509
+ progress_callback: (p) => {
1510
+ if (p.status !== 'progress') return;
1511
+ setStatusUI(`⏳ Loading AI… ${Math.round(p.progress ?? 0)}%`, '#d29922', p.progress ?? 0);
1512
+ },
1513
  });
1514
+ window._llm.status = 'ready';
1515
+ setStatusUI('βœ… AI model ready', '#3fb950', null);
1516
+ setTimeout(() => statusWrap.classList.add('hidden'), 3000);
1517
+ } catch (err) {
1518
+ console.error('[DeskFlow] Model load failed:', err);
1519
+ window._llm.status = 'error';
1520
+ setStatusUI('⚠️ Templates only (AI unavailable)', '#d29922', null);
1521
+ setTimeout(() => statusWrap.classList.add('hidden'), 5000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522
  }
1523
+ })();
1524
 
1525
+ window._llmGenerate = async function(systemPrompt, userPrompt, templateFallback, onToken, onDone, onFallback) {
1526
+ if (!window._llm.generator) { onFallback(templateFallback); return; }
1527
+ try {
1528
+ const messages = [
1529
+ { role: 'system', content: systemPrompt },
1530
+ { role: 'user', content: userPrompt },
1531
+ ];
1532
+ let accumulated = '';
1533
+ const streamer = new TextStreamer(window._llm.generator.tokenizer, {
1534
+ skip_prompt: true,
1535
+ callback_function: (token) => {
1536
+ accumulated += token;
1537
+ onToken(accumulated);
1538
+ },
1539
+ });
1540
+ const result = await window._llm.generator(messages, {
1541
+ max_new_tokens: 512,
1542
+ temperature: 0.7,
1543
+ do_sample: true,
1544
+ streamer,
1545
+ });
1546
+ const finalText = result?.[0]?.generated_text?.at?.(-1)?.content || accumulated;
1547
+ onDone(finalText || accumulated);
1548
+ } catch (err) {
1549
+ console.error('[DeskFlow] Generation failed:', err);
1550
+ onFallback(templateFallback);
1551
  }
1552
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
1553
  }
 
 
 
 
1554
  </script>
1555
 
1556
  </body>