// ==UserScript== // @name UniWHIT Assistant // @namespace http://tampermonkey.net/ // @version 8.1 // @description AI-powered ServiceNow incident auto-classifier with Floating Window UI // @author none // @match https://*.service-now.com/incident.do* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect 127.0.0.1 // ==/UserScript== (function() { 'use strict'; const DEBUG = 1; const log = (...args) => DEBUG && console.log('[UniWHIT]', ...args); // Ikony Lucide (SVG) const ICONS = { grip: '', close: '', check: '', zap: '', map: '', database: '', stats: '', trending: '', download: '', upload: '', spinner: '', }; const API_URL = 'http://127.0.0.1:11434/api/generate'; const MODEL = 'dolphin3:8b'; const STORAGE_KEY = 'servicenow_training_data'; const AUTO_EXPORT_THRESHOLD = 50; const LOCATION_MAP = { 'BRZ | Ul. Biznesowa 1': 'WHIT BRZ', 'BYD | Hutnicza 100': 'WHIT BYD (Bydgoszcz)', 'ELZ | In der Hochstedter Ecke 1': 'WHIT EF (Erfurt)', 'GDA | Ameryka 30': 'WHIT GDA (Gdańsk / Olsztynek)', 'LOD | ul. Inwestycyjna 1': 'WHIT LOD (Łódź)', 'LLZ | Einsteinallee 26': 'WHIT LR (Lahr)', 'LUU | Uferring 8': 'WHIT LUU (Ludwigsfelde)', 'MGL | Regioparkring 25': 'WHIT MG (Monchengladbach)', 'POZ | Fabryczna 5': 'WHIT POZ', 'ROT | Klappolder 130': 'WHIT ROT (Rotterdam)', 'SZC | ul. Innowacyjna 8': 'WHIT SZC (Szczecin)', 'VER | Via Alcide de Gasperi 13-15': 'WHIT VER (Verona)' }; const ASSIGNMENT_GROUP_TO_LOCATION = { 'WHIT BRZ': 'BRZ | Ul. Biznesowa 1', 'WHIT BYD (Bydgoszcz)': 'BYD | Hutnicza 100', 'WHIT EF (Erfurt)': 'ELZ | In der Hochstedter Ecke 1', 'WHIT GDA (Gdańsk / Olsztynek)': 'GDA | Ameryka 30', 'WHIT LOD (Łódź)': 'LOD | ul. Inwestycyjna 1', 'WHIT LR (Lahr)': 'LLZ | Einsteinallee 26', 'WHIT LUU (Ludwigsfelde)': 'LUU | Uferring 8', 'WHIT MG (Monchengladbach)': 'MGL | Regioparkring 25', 'WHIT POZ': 'POZ | Fabryczna 5', 'WHIT ROT (Rotterdam)': 'ROT | Klappolder 130', 'WHIT SZC (Szczecin)': 'SZC | ul. Innowacyjna 8', 'WHIT VER (Verona)': 'VER | Via Alcide de Gasperi 13-15', 'WHIT UniWHIT - Global': 'Working remotely' }; const VALID_LOCAL_GROUPS = [ 'WHIT BRZ', 'WHIT BYD (Bydgoszcz)', 'WHIT EF (Erfurt)', 'WHIT GDA (Gdańsk / Olsztynek)', 'WHIT LOD (Łódź)', 'WHIT LR (Lahr)', 'WHIT LUU (Ludwigsfelde)', 'WHIT MG (Monchengladbach)', 'WHIT POZ', 'WHIT ROT (Rotterdam)', 'WHIT SZC (Szczecin)', 'WHIT VER (Verona)' ]; const SERVICE_OFFERINGS = { 'Warehouse IT - Print': ['Device', 'Supplies', 'Network (including datalines)', 'Server', 'Settings / Software'], 'Warehouse IT - Accounts': ['ZalOS', 'Okta', 'Google / AD', 'Other'], 'Warehouse IT - Ops Workstations': ['Device', 'Device Management', 'Network (including datalines)', 'Peripherals / Chargers'], 'Warehouse IT - Handhelds': ['Device', 'Device Management'], 'Warehouse IT - Comms System': ['Phone system', 'Alerting & Monitoring systems', 'Digital Signage', 'Meeting Room'], 'Warehouse IT - Server & Database': ['Server', 'CloudService', 'Settings / Software / Database'], 'Warehouse IT - Network Connection': ['LAN', 'WAN', 'VPN', 'WiFi'], 'Warehouse IT - Applications & Services': ['WHITtools', 'Other (any "other" service: not owned or supported by WHIT)'], 'Warehouse IT - Asset Mgmt.': ['Notebook/PC/Tablet', 'Smartphone', 'Accessories'], 'Warehouse IT - Construct / reconstruct / deconstruct': ['Setup', 'Change / move', 'Dismantle / Remove'] }; const ASSIGNMENT_GROUP_MAP = { 'Warehouse IT - Print': 'keep_local', 'Warehouse IT - Accounts': 'WHIT UniWHIT - Global', 'Warehouse IT - Ops Workstations': 'WHIT UniWHIT - Global', 'Warehouse IT - Handhelds': 'keep_local', 'Warehouse IT - Comms System': 'WHIT UniWHIT - Global', 'Warehouse IT - Server & Database': 'keep_local', 'Warehouse IT - Network Connection': 'keep_local', 'Warehouse IT - Applications & Services': 'WHIT UniWHIT - Global', 'Warehouse IT - Asset Mgmt.': 'keep_local', 'Warehouse IT - Construct / reconstruct / deconstruct': 'keep_local' }; const SYSTEM_PROMPT = ` You will receive a ServiceNow incident description. Classify it into 2 fields with confidence scores (0-100). ### Input: "{{FORM_DATA}}" ### Available Services and their Service Offerings: **Warehouse IT - Print** (print-service end-to-end): - Device - Supplies - Network (including datalines) - Server - Settings / Software **Warehouse IT - Accounts** (any permission or account topic): - ZalOS - Okta - Google / AD - Other **Warehouse IT - Ops Workstations** (any WMS-workplace related topic): - Device - Device Management - Network (including datalines) - Peripherals / Chargers **Warehouse IT - Handhelds** (any WMS-workplace related topic): - Device - Device Management **Warehouse IT - Comms System** (any communication based service, incl. alerting systems): - Phone system - Alerting & Monitoring systems - Digital Signage - Meeting Room **Warehouse IT - Server & Database** (any server connection based service): - Server - CloudService - Settings / Software / Database **Warehouse IT - Network Connection** (any data, network and internet connection based service): - LAN - WAN - VPN - WiFi **Warehouse IT - Applications & Services** (software support / consultation / user support / instructions / knowledge transfer): - WHITtools - Other (any "other" service: not owned or supported by WHIT) **Warehouse IT - Asset Mgmt.** (handover / return / repair of personal equipment): - Notebook/PC/Tablet - Smartphone - Accessories **Warehouse IT - Construct / reconstruct / deconstruct** (whatever task requiring a screwdriver / spiral tubes): - Setup - Change / move - Dismantle / Remove ### Fields to classify: **1. service** - Choose the main Service category from the list above **2. serviceOffering** - Choose the appropriate Service Offering that matches the selected Service (use EXACT names from the list above, without any additional text) ### Output Format (JSON only): { "service": {"value": "service_name", "confidence": 95}, "serviceOffering": {"value": "service_offering_name", "confidence": 88} } `; const DEFAULT_VALUES = { location: '', service: '', serviceOffering: '', assignmentGroup: '', remoteResolutionPossible: 'No', assignedTo: '' }; const QUICK_ASSIGN_GROUPS = [ { label: 'BYD', value: 'WHIT BYD (Bydgoszcz)' }, { label: 'EF', value: 'WHIT EF (Erfurt)' }, { label: 'GDA', value: 'WHIT GDA (Gdańsk / Olsztynek)' }, { label: 'LOD', value: 'WHIT LOD (Łódź)' }, { label: 'LR', value: 'WHIT LR (Lahr)' }, { label: 'LUU', value: 'WHIT LUU (Ludwigsfelde)' }, { label: 'MG', value: 'WHIT MG (Monchengladbach)' }, { label: 'ROT', value: 'WHIT ROT (Rotterdam)' }, { label: 'SZC', value: 'WHIT SZC (Szczecin)' }, { label: 'VER', value: 'WHIT VER (Verona)' } ]; GM_addStyle(` #ai-sidebar-overlay { position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.1); z-index: 40; } #ai-sidebar-window { position: fixed; z-index: 50; width: 380px; height: 520px; background: #ffffff; border: 2px solid #d7d7d7; box-shadow: 0 8px 32px rgba(0,0,0,0.12); border-radius: 0.75rem; /* 12px */ display: flex; flex-direction: column; overflow: hidden; font-family: "SourceSansPro", "Helvetica Neue", "Helvetica", Arial, sans-serif; } #ai-sidebar-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid #d7d7d7; background-image: linear-gradient(to bottom, #f8f9fa, #f4f4f4); cursor: move; user-select: none; } #ai-sidebar-header .header-left { display: flex; align-items: center; gap: 8px; } #ai-sidebar-header .header-left svg { color: #697077; } #ai-sidebar-header .header-title { font-size: 12px; color: #293e40; } #ai-sidebar-header .close-btn { height: 24px; width: 24px; padding: 0; border-radius: 6px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; } #ai-sidebar-header .close-btn:hover { background-color: #e8e8e8; } #ai-sidebar-content { flex-grow: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; /* space-y-3 */ } /* --- Sekcja Sugestii --- */ .ai-suggestions-box { background-color: #fff; border-radius: 6px; border: 1px solid #ddd; padding: 12px; display: flex; flex-direction: column; gap: 10px; box-shadow: 0 2px 6px rgba(0,0,0,0.05); } .ai-suggestion-card { border: none; background: transparent; padding: 0; margin: 0; } .ai-suggestion-card .watermark { display: none; } .ai-suggestion-card .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } .ai-suggestion-card .card-value { font-size: 13px; font-weight: 600; color: #1f2d3d; } .ai-suggestion-card .card-check { color: #16a34a; } .ai-suggestion-card .confidence-bar { height: 5px; border-radius: 4px; background-color: #e0e0e0; overflow: hidden; } .ai-suggestion-card .confidence-bar-fill { height: 100%; transition: width 0.3s ease; background-color: #0f62fe; } .ai-suggestion-buttons { display: flex; gap: 8px; } /* Przycisk (zamiast importu z ui/button) */ .ai-btn { display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; border: 1px solid transparent; height: 32px; /* h-8 */ padding: 0 12px; flex-grow: 1; } .ai-btn svg { margin-right: 6px; } .ai-btn-primary { background-color: #0f62fe; color: #ffffff; } .ai-btn-primary:hover { background-color: #0353e9; } .ai-btn-outline { background-color: #ffffff; color: #333; border-color: #d7d7d7; } .ai-btn-outline:hover { background-color: #f4f4f4; } .ai-btn-outline.small { height: 28px; /* h-7 */ font-size: 11px; justify-content: flex-start; } .ai-alternative-box { padding-top: 4px; } .ai-alternative-box .alt-label { font-size: 10px; color: #697077; margin-bottom: 4px; } .ai-alternative-box .alt-card { padding: 10px; background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 12px; } .ai-alternative-box .alt-row { display: flex; justify-content: space-between; } .ai-alternative-box .alt-text { color: #697077; } /* --- Sekcje Zwijane (Collapsible) --- */ .ai-collapsible-trigger { width: 100%; display: flex; align-items: center; justify-content: space-between; padding: 10px; background-color: #f8f9fa; border-radius: 6px; border: none; cursor: pointer; transition: background-color 0.2s; text-align: left; } .ai-collapsible-trigger:hover { background-color: #eeeff0; } .ai-collapsible-trigger .trigger-left { display: flex; align-items: center; gap: 6px; } .ai-collapsible-trigger .trigger-left svg { width: 16px; height: 16px; } .ai-collapsible-trigger .trigger-left .icon-zap { color: #8a3ffc; } .ai-collapsible-trigger .trigger-left .icon-db { color: #0f62fe; } .ai-collapsible-trigger .trigger-title { font-size: 13px; font-weight: 600; color: #333; } .ai-collapsible-badge { font-size: 10px; height: 20px; padding: 0 6px; border-radius: 6px; background: #fff; border: 1px solid #d7d7d7; color: #555; } .ai-collapsible-content { padding-top: 10px; display: block; } .ai-collapsible-content.collapsed { display: none; } /* Sekcja Ręczna */ #manual-assign-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } /* Sekcja Treningu */ #training-content { display: flex; flex-direction: column; gap: 10px; } .training-stats-box { padding: 10px; background-color: #f8f9fa; border-radius: 6px; display: flex; flex-direction: column; gap: 8px; } .training-stats-row { display: flex; align-items: center; justify-content: space-between; } .training-stats-row .stat-left { display: flex; align-items: center; gap: 6px; } .training-stats-row .stat-left svg { color: #697077; } .training-stats-row .stat-label { font-size: 11px; color: #697077; } .training-stats-row .stat-value { font-size: 12px; color: #293e40; font-weight: 500; } .training-actions { display: flex; flex-direction: column; gap: 6px; } /* Spinner */ @keyframes rotateSpinner { to { transform: rotate(360deg); } } .ai-spinner { animation: rotateSpinner 1s linear infinite; width: 18px; height: 18px; } /* Error message */ .ai-error-message { color: #d32f2f; background: #feefef; padding: 8px; border-radius: 4px; margin: 5px 0; } `); const TrainingData = { load() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : []; }, save(entry) { const data = this.load(); data.push(entry); localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); this.updateStatsDisplay(); if (data.length % AUTO_EXPORT_THRESHOLD === 0) { this.export(); } }, export() { const data = this.load(); if (data.length === 0) return; const jsonl = data.map(e => JSON.stringify(e)).join('\n'); const blob = new Blob([jsonl], { type: 'application/jsonl' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const timestamp = new Date().toISOString().split('T')[0]; a.href = url; a.download = `servicenow_training_${timestamp}_${data.length}tickets.jsonl`; a.click(); URL.revokeObjectURL(url); }, import() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.jsonl'; input.onchange = (e) => { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = (ev) => { try { const lines = ev.target.result.split('\n').filter(l => l.trim()); const imported = lines.map(l => JSON.parse(l)); const existing = this.load(); localStorage.setItem(STORAGE_KEY, JSON.stringify([...existing, ...imported])); this.updateStatsDisplay(); alert(`Imported ${imported.length} tickets`); } catch(err) { alert('Import failed: ' + err.message); } }; reader.readAsText(file); }; input.click(); }, clear() { if (confirm('Clear all training data?')) { localStorage.removeItem(STORAGE_KEY); this.updateStatsDisplay(); } }, stats() { const data = this.load(); const localCount = data.filter(d => d.user_choice === 'local').length; const globalCount = data.filter(d => d.user_choice === 'global').length; const avgConf = data.reduce((sum, d) => { const avg = (d.confidence?.service || 0 + d.confidence?.serviceOffering || 0) / 2; return sum + avg; }, 0) / data.length || 0; alert(`Training Data Stats:\n\nTotal: ${data.length}\nLocal: ${localCount}\nGlobal: ${globalCount}\nAvg Confidence: ${avgConf.toFixed(1)}%`); }, updateStatsDisplay() { const statsEl = document.getElementById('training-stats-tickets'); if (statsEl) { statsEl.textContent = `${this.load().length}`; } } }; function createFloatingWindow() { const overlay = document.createElement('div'); overlay.id = 'ai-sidebar-overlay'; overlay.style.display = 'none'; overlay.onclick = closeSidebar; document.body.appendChild(overlay); const windowEl = document.createElement('div'); windowEl.id = 'ai-sidebar-window'; windowEl.style.display = 'none'; windowEl.style.left = `${window.innerWidth - 420}px`; windowEl.style.top = '80px'; windowEl.innerHTML = `
`; document.body.appendChild(windowEl); document.getElementById('ai-sidebar-close').onclick = closeSidebar; const manualTrigger = document.getElementById('manual-trigger'); const manualContent = document.getElementById('manual-content'); manualTrigger.onclick = () => { manualTrigger.classList.toggle('collapsed'); manualContent.classList.toggle('collapsed'); }; const trainingTrigger = document.getElementById('training-trigger'); const trainingContent = document.getElementById('training-content'); trainingTrigger.onclick = () => { trainingTrigger.classList.toggle('collapsed'); trainingContent.classList.toggle('collapsed'); }; let isDragging = false; let dragOffset = { x: 0, y: 0 }; const header = document.getElementById('ai-sidebar-header'); header.onmousedown = (e) => { const rect = windowEl.getBoundingClientRect(); dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; isDragging = true; document.body.style.userSelect = 'none'; }; document.onmousemove = (e) => { if (isDragging) { let newX = e.clientX - dragOffset.x; let newY = e.clientY - dragOffset.y; newX = Math.max(0, Math.min(newX, window.innerWidth - windowEl.offsetWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - windowEl.offsetHeight)); windowEl.style.left = `${newX}px`; windowEl.style.top = `${newY}px`; } }; document.onmouseup = () => { isDragging = false; document.body.style.userSelect = 'auto'; }; } function openSidebar() { document.getElementById('ai-sidebar-overlay').style.display = 'block'; document.getElementById('ai-sidebar-window').style.display = 'flex'; } function closeSidebar() { document.getElementById('ai-sidebar-overlay').style.display = 'none'; document.getElementById('ai-sidebar-window').style.display = 'none'; const originalBtn = document.getElementById('ai-assistant-btn'); if (originalBtn) { originalBtn.innerHTML = '🤖'; originalBtn.classList.remove('working', 'error', 'success'); originalBtn.disabled = false; } } function createAssistantButton() { const formActionsContainer = document.querySelector('.navbar_ui_actions'); if (!formActionsContainer) { log('Form actions container not found'); return null; } log('Creating assistant button'); const asystentBtn = document.createElement('button'); asystentBtn.className = 'form_action_button header action_context btn btn-default'; asystentBtn.id = 'ai-assistant-btn'; asystentBtn.type = 'button'; asystentBtn.style.whiteSpace = 'nowrap'; asystentBtn.innerHTML = '🤖'; formActionsContainer.appendChild(asystentBtn); return asystentBtn; } function populateQuickAssignPanel() { const container = document.getElementById('manual-assign-grid'); if (!container) return; let html = ''; QUICK_ASSIGN_GROUPS.forEach(group => { html += ``; }); container.innerHTML = html; container.querySelectorAll('.ai-btn').forEach(btn => { btn.addEventListener('click', (e) => { // **POPRAWKA:** Usunięto rzutowanie 'as HTMLElement' const group = e.currentTarget.getAttribute('data-group'); changeAssignmentGroup(group); closeSidebar(); }); }); } function populateTrainingPanel() { TrainingData.updateStatsDisplay(); document.getElementById('btn-save-current').onclick = () => { saveManualTrainingData(); }; document.getElementById('btn-export').onclick = () => TrainingData.export(); document.getElementById('btn-import').onclick = () => TrainingData.import(); document.getElementById('training-stats-tickets').parentElement.parentElement.onclick = () => TrainingData.stats(); } function saveManualTrainingData() { log('Saving manual training data'); const formData = collectFormData(); const ticketId = gel('sys_original.incident.number')?.value || 'UNKNOWN'; const entry = { ticket_id: ticketId, timestamp: new Date().toISOString(), input: { short_description: formData.shortDescription || '', description: formData.description || '', location: formData.location || null }, output: { service: formData.service || '', serviceOffering: formData.serviceOffering || '' }, confidence: { service: 100, serviceOffering: 100 }, user_choice: formData.assignmentGroup === 'WHIT UniWHIT - Global' ? 'global' : 'local', source: 'manual_figma_ui', has_location_initially: !!formData.location }; TrainingData.save(entry); alert('Zapisano bieżący ticket do bazy treningowej.'); } function init() { if (!window.location.href.includes('/incident.do')) { log('Not on incident page'); return; } log('Initializing Floating AI Assistant 8.1'); let lastDetectedValues = null; let lastConfidence = null; let initialFormData = null; createFloatingWindow(); const originalBtn = createAssistantButton(); if (!originalBtn) return; populateQuickAssignPanel(); populateTrainingPanel(); function waitForElement(selector) { return new Promise(resolve => { const interval = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(interval); resolve(el); } }, 300); }); } async function setReferenceField(fieldName, displayValue) { log(`Setting reference field: ${fieldName} = ${displayValue}`); const displayField = await waitForElement(`input[name="sys_display.incident.${fieldName}"]`); const hiddenField = document.querySelector(`input[name="incident.${fieldName}"]`); displayField.focus(); await new Promise(resolve => setTimeout(resolve, 100)); displayField.click(); await new Promise(resolve => setTimeout(resolve, 100)); displayField.value = displayValue; if (hiddenField) hiddenField.value = ''; displayField.dispatchEvent(new Event('input', { bubbles: true })); displayField.dispatchEvent(new Event('change', { bubbles: true })); displayField.dispatchEvent(new Event('keydown', { bubbles: true })); displayField.dispatchEvent(new Event('keyup', { bubbles: true })); displayField.dispatchEvent(new Event('blur', { bubbles: true })); await new Promise(resolve => setTimeout(resolve, 500)); } async function setSelectField(selector, value) { log(`Setting select field: ${selector} = ${value}`); const field = await waitForElement(selector); field.focus(); field.click(); if (field.tagName === 'SELECT') { const opt = [...field.options].find(o => o.text === value); if (opt) opt.selected = true; } else { field.value = value; } field.dispatchEvent(new Event('change', { bubbles: true })); field.dispatchEvent(new Event('blur', { bubbles: true })); await new Promise(resolve => setTimeout(resolve, 300)); } function changeAssignmentGroup(groupValue, assignedToValue = null) { log(`Changing assignment group: ${groupValue}`); waitForElement('input[name="sys_display.incident.assignment_group"]').then(field => { field.focus(); field.click(); field.value = groupValue; field.dispatchEvent(new Event('change', { bubbles: true })); field.dispatchEvent(new Event('blur', { bubbles: true })); if (assignedToValue) { setTimeout(() => { waitForElement('input[name="sys_display.incident.assigned_to"]').then(assignedField => { assignedField.focus(); assignedField.click(); assignedField.value = assignedToValue; assignedField.dispatchEvent(new Event('change', { bubbles: true })); assignedField.dispatchEvent(new Event('blur', { bubbles: true })); }); }, 300); } }); } originalBtn.addEventListener('click', () => { log('Assistant button clicked'); setButtonLoading(originalBtn); const formData = collectFormData(); initialFormData = formData; log('Initial form data collected:', formData); sendToAI(formData, originalBtn); }); function setButtonSuccess(btn) { log('Button success state'); btn.classList.remove('error','working'); btn.classList.add('success'); btn.innerHTML = '🤖'; btn.disabled = false; } function setButtonLoading(btn) { log('Button loading state'); btn.classList.remove('error','success'); btn.classList.add('working'); btn.innerHTML = ICONS.spinner; btn.disabled = true; } function setButtonError(btn, msg = 'Błąd') { log('Button error state:', msg); btn.classList.remove('working','success'); btn.classList.add('error'); btn.innerHTML = 'Błąd!'; const suggestionsContainer = document.getElementById('ai-suggestions-container'); if (suggestionsContainer) { suggestionsContainer.innerHTML = ``; } setTimeout(() => { btn.innerHTML = '🤖'; btn.classList.remove('error'); btn.disabled = false; }, 2000); } function collectFormData() { const map = { shortDescription: 'incident.short_description', description: 'incident.description', location: 'sys_display.incident.location', service: 'sys_display.incident.business_service', serviceOffering: 'sys_display.incident.service_offering', assignmentGroup: 'sys_display.incident.assignment_group', remoteResolutionPossible: 'incident.x_kpmg3_pit_inc_remote_resolution_possible', assignedTo: 'sys_display.incident.assigned_to' }; const out = {}; for (const [key, id] of Object.entries(map)) { const el = gel(id); if (el && el.value) { out[key] = el.value; } } return out; } function sendToAI(formData, btn) { log('Sending to AI'); const finalPrompt = SYSTEM_PROMPT.replace('{{FORM_DATA}}', JSON.stringify(formData, null, 2)); const payload = { model: MODEL, prompt: finalPrompt, stream: false, format: "json" }; log('Payload:', payload); GM_xmlhttpRequest({ method: 'POST', url: API_URL, data: JSON.stringify(payload), headers: {'Content-Type': 'application/json'}, onload: (resp) => { try { const parsed = JSON.parse(resp.responseText); const data = JSON.parse(parsed.response); log('AI Data:', data); lastDetectedValues = data; lastConfidence = { service: data.service.confidence / 100, // Normalizuj do 0-1 serviceOffering: data.serviceOffering.confidence / 100 // Normalizuj do 0-1 }; const altData = { alternativeService: data.alternativeService || "Warehouse IT - Print", alternativeOffering: data.alternativeOffering || "Device", alternativeConfidence: data.alternativeConfidence ? data.alternativeConfidence / 100 : 0.45 }; displayResults(formData, data, altData); setButtonSuccess(btn); openSidebar(); } catch(e) { log('AI parse error:', e); console.error('AI parse error:', e); setButtonError(btn, 'Failed to parse AI response'); openSidebar(); } }, onerror: (err) => { log('Ollama error:', err); console.error('Ollama error', err); setButtonError(btn, err.responseText || 'Ollama is not running'); openSidebar(); } }); } function displayResults(existingValues, aiValues, altValues) { log('Displaying results in new UI'); const container = document.getElementById('ai-suggestions-container'); if (!container) return; const confidence = lastConfidence?.service || 0; const confidenceColor = confidence > 0.8 ? '#16a34a' : confidence > 0.6 ? '#eab308' : '#dc2626'; container.innerHTML = `