From bef5c20c83beb19e7e8fe165d28d6a0ede547f21 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 20 Feb 2026 07:10:06 +0100 Subject: [PATCH] feat: Implement AI-powered Case Analysis Service and QuickCreate Modal - Added CaseAnalysisService for analyzing case text using Ollama LLM. - Integrated AI analysis into the QuickCreate modal for automatic case creation. - Created HTML structure for QuickCreate modal with dynamic fields for title, description, customer, priority, technician, and tags. - Implemented customer search functionality with debounce for efficient querying. - Added priority field to sag_sager table with migration for consistency in case management. - Introduced caching mechanism in CaseAnalysisService to optimize repeated analyses. - Enhanced error handling and user feedback in the QuickCreate modal. --- app/core/config.py | 4 +- app/models/schemas.py | 73 ++ app/modules/sag/backend/router.py | 32 +- app/modules/sag/templates/create.html | 46 +- app/modules/sag/templates/detail.html | 34 +- app/modules/telefoni/backend/router.py | 4 +- app/services/case_analysis_service.py | 498 +++++++++++++ app/shared/frontend/base.html | 38 + app/shared/frontend/quick_create_modal.html | 670 ++++++++++++++++++ .../frontend/mockups/tech_v1_overview.html | 578 +++++++++++++-- app/ticket/frontend/views.py | 37 +- docker-compose.yml | 2 + migrations/137_add_priority_to_sag.sql | 19 + 13 files changed, 1923 insertions(+), 112 deletions(-) create mode 100644 app/services/case_analysis_service.py create mode 100644 app/shared/frontend/quick_create_modal.html create mode 100644 migrations/137_add_priority_to_sag.sql diff --git a/app/core/config.py b/app/core/config.py index 3c9c6bd..919d9d2 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -78,8 +78,8 @@ class Settings(BaseSettings): WIKI_READ_ONLY: bool = True # Ollama LLM - OLLAMA_ENDPOINT: str = "http://localhost:11434" - OLLAMA_MODEL: str = "llama3.2:3b" + OLLAMA_ENDPOINT: str = "http://172.16.31.195:11434" + OLLAMA_MODEL: str = "llama3.2" # Email System Configuration # IMAP Settings diff --git a/app/models/schemas.py b/app/models/schemas.py index dbe96dc..64d81fb 100644 --- a/app/models/schemas.py +++ b/app/models/schemas.py @@ -2,6 +2,7 @@ Pydantic Models and Schemas """ +from enum import Enum from pydantic import BaseModel, ConfigDict from typing import Optional, List from datetime import datetime, date @@ -303,3 +304,75 @@ class AnyDeskSessionUpdate(BaseModel): status: str ended_at: Optional[str] = None duration_minutes: Optional[int] = None + + +# ============================================================================ +# SAG MODULE (Cases) - QuickCreate and Priority Support +# ============================================================================ + +class SagPriority(str, Enum): + """Case priority levels matching database enum""" + LOW = "low" + NORMAL = "normal" + HIGH = "high" + URGENT = "urgent" + + +class QuickCreateAnalysis(BaseModel): + """AI analysis result for QuickCreate feature""" + suggested_title: str + suggested_description: str + suggested_priority: SagPriority = SagPriority.NORMAL + suggested_customer_id: Optional[int] = None + suggested_customer_name: Optional[str] = None + suggested_technician_id: Optional[int] = None + suggested_technician_name: Optional[str] = None + suggested_group_id: Optional[int] = None + suggested_group_name: Optional[str] = None + suggested_tags: List[str] = [] + hardware_references: List[dict] = [] # [{id, brand, model, serial_number}] + confidence: float = 0.0 + ai_reasoning: Optional[str] = None # Debug info for low confidence + + model_config = ConfigDict(from_attributes=True) + + +class SagBase(BaseModel): + """Base schema for SAG (cases)""" + titel: str + beskrivelse: Optional[str] = None + priority: SagPriority = SagPriority.NORMAL + customer_id: Optional[int] = None + ansvarlig_bruger_id: Optional[int] = None + assigned_group_id: Optional[int] = None + deadline: Optional[datetime] = None + + +class SagCreate(SagBase): + """Schema for creating a case""" + template_key: Optional[str] = None + tags: Optional[List[str]] = None + + +class SagUpdate(BaseModel): + """Schema for updating a case""" + titel: Optional[str] = None + beskrivelse: Optional[str] = None + priority: Optional[SagPriority] = None + status: Optional[str] = None + ansvarlig_bruger_id: Optional[int] = None + assigned_group_id: Optional[int] = None + deadline: Optional[datetime] = None + + +class Sag(SagBase): + """Full case schema""" + id: int + status: str + template_key: Optional[str] = None + created_by_user_id: int + created_at: datetime + updated_at: datetime + deleted_at: Optional[datetime] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/app/modules/sag/backend/router.py b/app/modules/sag/backend/router.py index a9bf9ba..5559324 100644 --- a/app/modules/sag/backend/router.py +++ b/app/modules/sag/backend/router.py @@ -10,9 +10,10 @@ from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Request from fastapi.responses import FileResponse from pydantic import BaseModel, Field from app.core.database import execute_query, execute_query_single -from app.models.schemas import TodoStep, TodoStepCreate, TodoStepUpdate +from app.models.schemas import TodoStep, TodoStepCreate, TodoStepUpdate, QuickCreateAnalysis from app.core.config import settings from app.services.email_service import EmailService +from app.services.case_analysis_service import CaseAnalysisService try: import extract_msg @@ -99,6 +100,35 @@ def _validate_group_id(group_id: Optional[int], field_name: str = "assigned_grou if not exists: raise HTTPException(status_code=400, detail=f"Invalid {field_name}") +# ============================================================================ +# QUICKCREATE AI ANALYSIS +# ============================================================================ + +class QuickCreateRequest(BaseModel): + text: str = Field(..., min_length=1, max_length=5000) + user_id: int + + +@router.post("/sag/analyze-quick-create", response_model=QuickCreateAnalysis) +async def analyze_quick_create(request: QuickCreateRequest): + """ + Analyze case description text using AI for QuickCreate feature. + Returns structured suggestions for customer, technician, priority, tags, etc. + """ + try: + logger.info(f"🔍 QuickCreate analysis requested by user {request.user_id}, text length: {len(request.text)}") + + # Initialize service and analyze + service = CaseAnalysisService() + analysis = await service.analyze_case_text(request.text, request.user_id) + + logger.info(f"✅ QuickCreate analysis complete: confidence={analysis.confidence}, priority={analysis.suggested_priority}") + return analysis + + except Exception as e: + logger.error(f"❌ QuickCreate analysis failed: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}") + # ============================================================================ # SAGER - CRUD Operations # ============================================================================ diff --git a/app/modules/sag/templates/create.html b/app/modules/sag/templates/create.html index 94fe9bf..616457b 100644 --- a/app/modules/sag/templates/create.html +++ b/app/modules/sag/templates/create.html @@ -309,8 +309,23 @@ let selectedContactsCompanies = {}; let customerSearchTimeout; let contactSearchTimeout; + let successAlertTimeout; let telefoniPrefill = { contactId: null, title: null, callId: null, customerId: null, description: null }; + // Helper function to show success alert + function showSuccessAlert(message, duration = 3000) { + if (successAlertTimeout) { + clearTimeout(successAlertTimeout); + } + const successDiv = document.getElementById('success'); + successDiv.classList.remove('d-none'); + document.getElementById('success-text').textContent = message; + successAlertTimeout = setTimeout(() => { + successDiv.classList.add('d-none'); + successAlertTimeout = null; + }, duration); + } + // --- Character Counter --- const beskrInput = document.getElementById('beskrivelse'); if (beskrInput) { @@ -415,12 +430,17 @@ } // --- Selection Logic --- - function selectCustomer(id, name) { + function selectCustomer(id, name, skipAlert = false) { selectedCustomer = { id, name }; document.getElementById('customer_id').value = id; document.getElementById('customerSearch').value = ''; document.getElementById('customerResults').classList.add('d-none'); renderSelections(); + + // Show notification + if (!skipAlert) { + showSuccessAlert(`Valgte firma: ${name}`); + } } function removeCustomer() { @@ -430,7 +450,8 @@ } async function selectContact(id, name) { - if (!selectedContacts[id]) { + const isNewContact = !selectedContacts[id]; + if (isNewContact) { selectedContacts[id] = { id, name }; } document.getElementById('contactSearch').value = ''; @@ -446,19 +467,24 @@ if (data.companies && data.companies.length === 1) { const company = data.companies[0]; - if (!selectedCustomer) { - selectCustomer(company.id, company.name); - - // Show brief notification - const successDiv = document.getElementById('success'); - successDiv.classList.remove('d-none'); - document.getElementById('success-text').textContent = `Valgte automatisk kunde: ${company.name}`; - setTimeout(() => successDiv.classList.add('d-none'), 3000); + if (!selectedCustomer && isNewContact) { + // Auto-select company silently, then show combined alert + selectCustomer(company.id, company.name, true); + showSuccessAlert(`Valgte kontakt: ${name} + firma: ${company.name}`, 4000); + } else if (isNewContact) { + // Just show contact alert + showSuccessAlert(`Valgte kontakt: ${name}`); } + } else if (isNewContact) { + // No auto-select, just show contact alert + showSuccessAlert(`Valgte kontakt: ${name}`); } } } catch (e) { console.error("Auto-select company failed", e); + if (isNewContact) { + showSuccessAlert(`Valgte kontakt: ${name}`); + } } loadHardwareForContacts(); diff --git a/app/modules/sag/templates/detail.html b/app/modules/sag/templates/detail.html index bf8ae51..44a4afe 100644 --- a/app/modules/sag/templates/detail.html +++ b/app/modules/sag/templates/detail.html @@ -81,12 +81,6 @@ border: 1px solid rgba(0,0,0,0.1); } - /* Wider tooltip for relation type explanations */ - .tooltip-wide .tooltip-inner { - max-width: 400px; - text-align: left; - } - .tag-closed { background-color: #e0e0e0; color: #666; @@ -959,15 +953,15 @@
-
- đź”— Relationer - +
đź”— Relationer
+ -
+ data-bs-placement="right" + title="Hvad betyder relationstyper?

Relateret til: Faglig kobling uden direkte afhængighed.
Afledt af: Denne sag er opstĂĄet pĂĄ baggrund af en anden sag.
Ă…rsag til: Denne sag er ĂĄrsagen til en anden sag.
Blokkerer: Arbejde i en sag stopper fremdrift i den anden.">
+
@@ -585,6 +588,7 @@ // Keyboard shortcut: Cmd+K or Ctrl+K document.addEventListener('keydown', (e) => { + // Cmd+K / Ctrl+K for global search if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); console.log('Cmd+K pressed - opening search modal'); // Debug @@ -598,12 +602,43 @@ }, 300); } + // '+' key for QuickCreate (not in input fields) + if (e.key === '+' && !e.ctrlKey && !e.metaKey && !e.shiftKey) { + if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return; + e.preventDefault(); + openQuickCreateModal(); + } + + // Cmd+Shift+C / Ctrl+Shift+C for QuickCreate + if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'c') { + e.preventDefault(); + openQuickCreateModal(); + } + // ESC to close if (e.key === 'Escape') { searchModal.hide(); } }); + // QuickCreate modal opener function + function openQuickCreateModal() { + const quickCreateModal = new bootstrap.Modal(document.getElementById('quickCreateModal')); + quickCreateModal.show(); + setTimeout(() => { + const textInput = document.getElementById('quickCreateText'); + if (textInput) { + textInput.focus(); + } + }, 300); + } + + // QuickCreate button click handler + document.getElementById('quickCreateBtn')?.addEventListener('click', (e) => { + e.preventDefault(); + openQuickCreateModal(); + }); + // Reset search when modal is closed document.getElementById('globalSearchModal').addEventListener('hidden.bs.modal', () => { if (globalSearchInput) { @@ -1049,6 +1084,9 @@ }); + +{% include "shared/frontend/quick_create_modal.html" %} +