From 122fbec3ac41be6a46490c92e28bdf4bd312fd77 Mon Sep 17 00:00:00 2001 From: Naman Singh Date: Fri, 22 May 2026 11:30:49 +0530 Subject: [PATCH 01/17] Privacy: pseudonymize user_id in audit logs - Replace raw user_id with SHA256 hash (8-char prefix) in all log statements - Maintains audit trail capability while protecting user identifiers (PII) - Complies with GDPR/CCPA privacy requirements - Hash is deterministic for correlation without exposing PII Resolves CodeRabbit PII logging concern --- backend/main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index a77a44cc..272e8f4f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,7 @@ import traceback import warnings import logging +import hashlib from contextlib import asynccontextmanager # Suppress harmless PyTorch CPU pin_memory warning @@ -535,7 +536,8 @@ async def save_ticket(request_body: TicketSaveRequest): except HTTPException: raise except Exception as profile_error: - logger.error(f"Tenant resolution error for user {request_body.user_id}: {profile_error}") + user_hash = hashlib.sha256(str(request_body.user_id).encode()).hexdigest()[:8] + logger.error(f"Tenant resolution error for user {user_hash}: {profile_error}") raise HTTPException(status_code=503, detail="Failed to resolve tenant linkage") from profile_error # Validate tenant consistency and authorization. @@ -543,7 +545,8 @@ async def save_ticket(request_body: TicketSaveRequest): if final_data.get("company_id"): # User provided company_id: verify it matches their profile. if profile_company_id and final_data["company_id"] != profile_company_id: - logger.warning(f"Tenant mismatch: user {request_body.user_id} attempted {final_data['company_id']}, assigned to {profile_company_id}") + user_hash = hashlib.sha256(str(request_body.user_id).encode()).hexdigest()[:8] + logger.warning(f"Tenant mismatch: user {user_hash} attempted {final_data['company_id']}, assigned to {profile_company_id}") raise HTTPException(status_code=403, detail="User not authorized for this tenant") elif profile_company_id: # Backfill company_id from profile. @@ -556,7 +559,9 @@ async def save_ticket(request_body: TicketSaveRequest): if not final_data.get("company") and profile.get("company"): final_data["company"] = profile["company"] - logger.info(f"Tenant linkage: user_id={request_body.user_id}, company_id={final_data.get('company_id')}") + user_hash = hashlib.sha256(str(request_body.user_id).encode()).hexdigest()[:8] + logger.info(f"Tenant linkage: user_hash={user_hash}, company_id={final_data.get('company_id')}") + res = supabase.table("tickets").insert(final_data).execute() From 9978402f09ea02095cd8f2149a7165922b8afc3c Mon Sep 17 00:00:00 2001 From: Achiever199 Date: Fri, 22 May 2026 14:26:58 +0530 Subject: [PATCH 02/17] feat: add realtime ticket dashboard updates via Supabase channels --- .../src/hooks/useRealtimeNotifications.js | 157 +++---------- Frontend/src/legacy_ui/Dashboard.jsx | 215 +++++++++++++----- Frontend/src/store/ticketStore.js | 10 + 3 files changed, 194 insertions(+), 188 deletions(-) diff --git a/Frontend/src/hooks/useRealtimeNotifications.js b/Frontend/src/hooks/useRealtimeNotifications.js index 1a6c98db..1496d434 100644 --- a/Frontend/src/hooks/useRealtimeNotifications.js +++ b/Frontend/src/hooks/useRealtimeNotifications.js @@ -3,144 +3,49 @@ import { supabase } from '../lib/supabaseClient'; import useAuthStore from '../store/authStore'; import useTicketStore from '../store/ticketStore'; -// We keep track of processed payload timestamps to avoid duplicate real-time notifications -const processedPayloads = new Set(); - -const useRealtimeNotifications = () => { +const useTicketsRealtime = () => { const { user, profile } = useAuthStore(); - const { addNotification } = useTicketStore(); + const { addTicket, updateTicket, removeTicket } = useTicketStore(); useEffect(() => { if (!user || !profile) return; - const handleTicketChange = (payload) => { - const { eventType, new: newRecord, old: oldRecord } = payload; - - // Deduplication logic using the internal commit timestamp - const commitTs = payload.commit_timestamp; - if (commitTs && processedPayloads.has(commitTs)) return; - if (commitTs) processedPayloads.add(commitTs); - - const isAdmin = profile.role === 'admin' || profile.role === 'master_admin'; - const isOwner = newRecord.user_id === user.id; - - // 1. NEW TICKET CREATED -> Notify Admins - if (eventType === 'INSERT') { - if (isAdmin) { - addNotification({ - title: 'New Ticket Received', - message: `A new ${newRecord.category || 'Support'} ticket requires triage.`, - ticketId: newRecord.id, - type: 'new_ticket', - recipientRole: 'admin' - }); - } - return; - } - - // 2. UPDATES - if (eventType === 'UPDATE' && oldRecord) { - // Determine what changed - const statusChanged = oldRecord.status !== newRecord.status; - const teamChanged = oldRecord.assigned_team !== newRecord.assigned_team; - - // For nested JSON/JSONB updates (messages) - const oldMessagesLen = Array.isArray(oldRecord.messages) ? oldRecord.messages.length : 0; - const newMessagesLen = Array.isArray(newRecord.messages) ? newRecord.messages.length : 0; -// eslint-disable-next-line no-unused-vars - const newlyAddedMessage = newMessagesLen > oldMessagesLen - ? newRecord.messages[newMessagesLen - 1] - : null; - - // STATUS CHANGE -> Notify User (e.g., Resolved, In Progress) - if (statusChanged && isOwner) { - addNotification({ - title: `Ticket ${newRecord.status}`, - message: `Your ticket status was updated to ${newRecord.status}.`, - ticketId: newRecord.id, - type: newRecord.status?.toLowerCase().includes('resolv') ? 'resolution' : 'update', - recipientRole: 'user' - }); - } - - // RE-ASSIGNMENT -> Notify User - if (teamChanged && isOwner && newRecord.assigned_team) { - addNotification({ - title: 'Ticket Re-Assigned', - message: `Your ticket is now being handled by ${newRecord.assigned_team}.`, - ticketId: newRecord.id, - type: 'update', - recipientRole: 'user' - }); - } - } - }; - - const handleMessageChange = (payload) => { - const { eventType, new: newMessage } = payload; - if (eventType !== 'INSERT') return; + // Only admins see the live ticket queue + const isAdmin = profile.role === 'admin' || profile.role === 'master_admin'; + if (!isAdmin) return; - // Use same deduplication logic - const commitTs = payload.commit_timestamp; - if (commitTs && processedPayloads.has(commitTs)) return; - if (commitTs) processedPayloads.add(commitTs); - - const isFromAdmin = newMessage.sender_role === 'admin' || newMessage.sender_role === 'super_admin' || newMessage.sender_role === 'master_admin'; - const isAdmin = profile.role === 'admin' || profile.role === 'master_admin'; - - // Note: In a real app, we should check if the current user is the owner of the ticket - // But for notifications, if I'm the recipient (admin or owner), I should see it. - // For now, we rely on recipientRole filter in the UI components. - - if (isFromAdmin) { - // Sent by Admin -> Notify User (only if it's their ticket) - // Note: ideally we'd check isOwner here, but for now we filter by role - if (profile.role === 'user') { - addNotification({ - title: 'New Response from Support', - message: newMessage.message?.length > 120 ? newMessage.message.substring(0, 120) + "..." : (newMessage.message || "An agent replied to your ticket."), - ticketId: newMessage.ticket_id, - type: 'message', - recipientRole: 'user' - }); - } - } else { - // Sent by User -> Notify Admin - if (isAdmin) { - addNotification({ - title: 'New Message from User', - message: newMessage.message || "A user replied to their ticket.", - ticketId: newMessage.ticket_id, - type: 'message', - recipientRole: 'admin' - }); - } - } - }; - - const ticketChannel = supabase - .channel('ticket-notifications') + const channel = supabase + .channel('tickets-realtime-dashboard') .on( 'postgres_changes', - { event: '*', schema: 'public', table: 'tickets' }, - handleTicketChange - ) - .subscribe(); - - const messageChannel = supabase - .channel('message-notifications') - .on( - 'postgres_changes', - { event: 'INSERT', schema: 'public', table: 'ticket_messages' }, - handleMessageChange + { + event: '*', + schema: 'public', + table: 'tickets', + filter: `company_id=eq.${profile.company_id}`, + }, + (payload) => { + const { eventType, new: newRecord, old: oldRecord } = payload; + + if (eventType === 'INSERT') { + addTicket(newRecord); + } + + if (eventType === 'UPDATE') { + updateTicket(newRecord.ticket_id, newRecord); + } + + if (eventType === 'DELETE') { + removeTicket(oldRecord.ticket_id); + } + } ) .subscribe(); return () => { - supabase.removeChannel(ticketChannel); - supabase.removeChannel(messageChannel); + supabase.removeChannel(channel); }; - }, [user, profile, addNotification]); + }, [user, profile, addTicket, updateTicket, removeTicket]); }; -export default useRealtimeNotifications; +export default useTicketsRealtime; \ No newline at end of file diff --git a/Frontend/src/legacy_ui/Dashboard.jsx b/Frontend/src/legacy_ui/Dashboard.jsx index ccfcc3d8..ea5ec375 100644 --- a/Frontend/src/legacy_ui/Dashboard.jsx +++ b/Frontend/src/legacy_ui/Dashboard.jsx @@ -1,26 +1,46 @@ import { - BarChart, - Bar, - XAxis, - YAxis, - Tooltip, - PieChart, - Pie, - Cell, - ResponsiveContainer, - Legend, + BarChart, Bar, XAxis, YAxis, Tooltip, + PieChart, Pie, Cell, ResponsiveContainer, Legend, } from "recharts"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { api } from "../services/api"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; import { ExpandableTabs } from "../components/ui/expandable-tabs"; -import { Bell, Home, Settings, HelpCircle, Shield, Activity, Zap, Users, AlertCircle } from "lucide-react"; +import { Bell, Home, Settings, HelpCircle, Shield, Activity, Zap, Users } from "lucide-react"; +import useTicketStore from "../store/ticketStore"; +import useTicketsRealtime from "../hooks/useTicketsRealtime"; const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#8884d8"]; +// Tracks which ticket IDs were recently updated for highlight animation +const useRecentlyUpdated = () => { + const [recentIds, setRecentIds] = useState(new Set()); + + const markUpdated = (id) => { + setRecentIds((prev) => new Set([...prev, id])); + setTimeout(() => { + setRecentIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, 2000); // highlight lasts 2 seconds + }; + + return { recentIds, markUpdated }; +}; + const Dashboard = () => { - const [tickets, setTickets] = useState([]); const [loading, setLoading] = useState(true); + const { recentIds, markUpdated } = useRecentlyUpdated(); + + // Pull live tickets from Zustand store + const tickets = useTicketStore((state) => state.tickets); + const addTicket = useTicketStore((state) => state.addTicket); + const prevTicketsRef = useRef([]); + + // Activate realtime subscription + useTicketsRealtime(); const tabs = [ { title: "Dashboard", icon: Home }, @@ -30,11 +50,12 @@ const Dashboard = () => { { title: "Security", icon: Shield }, ]; + // Initial fetch — populate store on first load useEffect(() => { const fetchTickets = async () => { try { const data = await api.getTickets(); - setTickets(data); + data.forEach((t) => addTicket(t)); } catch (error) { console.error("Failed to fetch tickets", error); } finally { @@ -42,26 +63,41 @@ const Dashboard = () => { } }; fetchTickets(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Detect newly inserted or updated tickets for highlight animation + useEffect(() => { + const prevIds = new Set(prevTicketsRef.current.map((t) => t.ticket_id)); + + tickets.forEach((t) => { + if (!prevIds.has(t.ticket_id)) { + // Brand new ticket + markUpdated(t.ticket_id); + } else { + // Check if it was updated + const prev = prevTicketsRef.current.find((p) => p.ticket_id === t.ticket_id); + if (prev && JSON.stringify(prev) !== JSON.stringify(t)) { + markUpdated(t.ticket_id); + } + } + }); + + prevTicketsRef.current = tickets; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tickets]); + // Summary Counts const totalTickets = tickets.length; const openTickets = tickets.filter( (t) => t.status === "Open" || t.Resolution_Status === "Open" ).length; -// eslint-disable-next-line no-unused-vars - const resolvedTickets = tickets.filter( - (t) => t.status === "Resolved" || t.Resolution_Status === "Resolved" || t.Resolution_Status === "Auto-Resolved" || t.Auto_Resolve - ).length; const autoResolvedTickets = tickets.filter( (t) => t.Auto_Resolve === true || t.Resolution_Status === "Auto-Resolved" ).length; - - // Automation Rate const automationRate = totalTickets > 0 ? (autoResolvedTickets / totalTickets) * 100 : 0; - // Transform data for charts const categoryData = Object.entries( tickets.reduce((acc, ticket) => { const cat = ticket.category || "Unknown"; @@ -88,29 +124,44 @@ const Dashboard = () => { return (
+ {/* Highlight animation style */} + +

Executive Overview

-

Global helpdesk status and AI performance across all channels

+

+ Global helpdesk status and AI performance across all channels +

{tickets.length === 0 ? ( -

No ticket data available yet. Submit your first ticket to see analytics.

+

+ No ticket data available yet. Submit your first ticket to see analytics. +

) : ( <> - {/* Summary Statistics Cards */} + {/* Summary Cards */}
- Total Tickets - + Total Tickets @@ -121,8 +172,7 @@ const Dashboard = () => { - Auto-Resolved - + Auto-Resolved @@ -133,8 +183,7 @@ const Dashboard = () => { - Open Tickets - + Open Tickets @@ -145,8 +194,7 @@ const Dashboard = () => { - Automation Rate - + Automation Rate @@ -155,6 +203,66 @@ const Dashboard = () => {
+ {/* Live Ticket Queue */} + + + + + Live Ticket Queue + + + LIVE + + + + +
+ + + + + + + + + + + {tickets.slice(0, 10).map((ticket) => ( + + + + + + + ))} + +
IDCategoryStatusAssignee
+ #{String(ticket.ticket_id).slice(0, 8)} + + {ticket.category || "—"} + + + {ticket.status || ticket.Resolution_Status || "—"} + + + {ticket.assigned_team || "Unassigned"} +
+
+
+
+ + {/* Charts */}
@@ -168,9 +276,7 @@ const Dashboard = () => { - + @@ -188,23 +294,12 @@ const Dashboard = () => {
- - `${name} ${(percent * 100).toFixed(0)}%` - } + `${name} ${(percent * 100).toFixed(0)}%`} > {statusData.map((entry, index) => ( - + ))} @@ -216,13 +311,11 @@ const Dashboard = () => {
- {/* Additional Insights */} + {/* Efficiency + AI */}
- - Efficiency Highlights - + Efficiency Highlights
@@ -244,23 +337,21 @@ const Dashboard = () => { - - AI Performance - + AI Performance
- {tickets.filter(t => t.confidence > 0.8).length} + {tickets.filter((t) => t.confidence > 0.8).length}
-
High Confidence
+
High Confidence
- {tickets.filter(t => (t.Duplicate_Probability || t.duplicate_probability || 0) > 0.7).length} + {tickets.filter((t) => (t.Duplicate_Probability || t.duplicate_probability || 0) > 0.7).length}
-
Potential Dupes
+
Potential Dupes
@@ -272,4 +363,4 @@ const Dashboard = () => { ); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/Frontend/src/store/ticketStore.js b/Frontend/src/store/ticketStore.js index a11b12aa..1b88dddf 100644 --- a/Frontend/src/store/ticketStore.js +++ b/Frontend/src/store/ticketStore.js @@ -36,11 +36,21 @@ const useTicketStore = create( const updatedTickets = state.tickets.map(t => t.ticket_id === ticketId ? { ...t, ...updates } : t); const shouldUpdateActive = state.activeTicket?.ticket_id === ticketId; + + + return { tickets: updatedTickets, activeTicket: shouldUpdateActive ? { ...state.activeTicket, ...updates } : state.activeTicket }; }), + + removeTicket: (ticketId) => set((state) => ({ + tickets: state.tickets.filter(t => t.ticket_id !== ticketId), + activeTicket: state.activeTicket?.ticket_id === ticketId + ? null + : state.activeTicket +})), appendMessage: (ticketId, message) => set((state) => { const updatedTickets = state.tickets.map(t => t.ticket_id === ticketId From c9947d7d9cc5ff9bc5dad865dd5e0eaa94aab307 Mon Sep 17 00:00:00 2001 From: ritesh-1918 Date: Tue, 26 May 2026 22:40:41 +0530 Subject: [PATCH 03/17] feat(auth): add clean self-contained cookie auth endpoints --- backend/main.py | 142 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/backend/main.py b/backend/main.py index 1139cc07..b20937ca 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1064,3 +1064,145 @@ async def analyze_ticket_v2(request: TicketRequest): } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + +# --------------------------------------------------------------------------- +# Clean cookie-based Supabase Auth endpoints for /auth/me backward-compatibility +# --------------------------------------------------------------------------- +ACCESS_COOKIE = "access_token" +REFRESH_COOKIE = "refresh_token" +ACCESS_MAX_AGE = 60 * 60 +REFRESH_MAX_AGE = 60 * 60 * 24 * 7 + +def _cookie_kwargs() -> dict: + secure = os.getenv("ENV", "production").lower() != "development" + return { + "httponly": True, + "secure": secure, + "samesite": "strict", + "path": "/", + } + +def extract_token(request: Request) -> str | None: + cookie_token = request.cookies.get(ACCESS_COOKIE) + if cookie_token: + return cookie_token + auth = request.headers.get("authorization") or request.headers.get("Authorization") + if auth and auth.lower().startswith("bearer "): + return auth.split(" ", 1)[1].strip() or None + return None + +def _set_session_cookies(response: Response, session) -> None: + if not session or not getattr(session, "access_token", None): + return + response.set_cookie( + ACCESS_COOKIE, + session.access_token, + max_age=ACCESS_MAX_AGE, + **_cookie_kwargs(), + ) + refresh = getattr(session, "refresh_token", None) + if refresh: + response.set_cookie( + REFRESH_COOKIE, + refresh, + max_age=REFRESH_MAX_AGE, + **_cookie_kwargs(), + ) + +def _clear_session_cookies(response: Response) -> None: + kwargs = _cookie_kwargs() + response.delete_cookie(ACCESS_COOKIE, path=kwargs["path"]) + response.delete_cookie(REFRESH_COOKIE, path=kwargs["path"]) + +async def get_current_user(request: Request) -> dict: + token = extract_token(request) + if not token: + raise HTTPException(status_code=401, detail="Not authenticated") + if not supabase: + raise HTTPException(status_code=503, detail="Database connection offline") + try: + result = supabase.auth.get_user(token) + except Exception as exc: + raise HTTPException( + status_code=401, + detail=f"Invalid session: {exc}", + ) from exc + user = getattr(result, "user", None) or (result.get("user") if isinstance(result, dict) else None) + if not user: + raise HTTPException(status_code=401, detail="Invalid session") + if hasattr(user, "model_dump"): + return user.model_dump() + if hasattr(user, "dict"): + return user.dict() + return dict(user) + +class LoginBody(BaseModel): + email: str + password: str + +class SignupBody(BaseModel): + email: str + password: str + full_name: str | None = None + role: str | None = "user" + company: str | None = None + +@app.post("/auth/login") +async def auth_login(body: LoginBody, response: Response): + if not supabase: + raise HTTPException(status_code=503, detail="Database connection offline") + try: + result = supabase.auth.sign_in_with_password( + {"email": body.email, "password": body.password} + ) + except Exception as exc: + raise HTTPException(status_code=401, detail=str(exc)) from exc + + session = getattr(result, "session", None) + user = getattr(result, "user", None) + if not session or not user: + raise HTTPException(status_code=401, detail="Invalid credentials") + + _set_session_cookies(response, session) + user_payload = user.model_dump() if hasattr(user, "model_dump") else dict(user) + return {"user": user_payload, "message": "Session cookies set"} + +@app.post("/auth/signup") +async def auth_signup(body: SignupBody, response: Response): + if not supabase: + raise HTTPException(status_code=503, detail="Database connection offline") + metadata = {} + if body.full_name: + metadata["full_name"] = body.full_name + if body.role: + metadata["role"] = body.role + if body.company: + metadata["company"] = body.company + + try: + result = supabase.auth.sign_up( + { + "email": body.email, + "password": body.password, + "options": {"data": metadata} if metadata else {}, + } + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + session = getattr(result, "session", None) + user = getattr(result, "user", None) + if session: + _set_session_cookies(response, session) + user_payload = user.model_dump() if user and hasattr(user, "model_dump") else None + return {"user": user_payload, "message": "Signup complete"} + +@app.post("/auth/logout") +async def auth_logout(response: Response): + _clear_session_cookies(response) + return {"ok": True} + +@app.get("/auth/me") +async def auth_me(user: dict = Depends(get_current_user)): + return {"user": user} + From 0bd500dd9daae7a15069ca7d7884aad181f3b12d Mon Sep 17 00:00:00 2001 From: ritesh-1918 Date: Wed, 27 May 2026 10:24:23 +0530 Subject: [PATCH 04/17] fix(media): resolve image attachment not showing in ticket details --- Frontend/src/user/pages/AIProcessing.jsx | 3 ++- backend/main.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Frontend/src/user/pages/AIProcessing.jsx b/Frontend/src/user/pages/AIProcessing.jsx index 9b0a8ed2..9d1f1ffa 100644 --- a/Frontend/src/user/pages/AIProcessing.jsx +++ b/Frontend/src/user/pages/AIProcessing.jsx @@ -344,7 +344,8 @@ const AIProcessing = () => { highlights: [], originalIssue: text, capturedFileBase64: image_base64, - ocrText: image_text + ocrText: image_text, + image_url: uploadedImageUrl }; setAITicket(fallbackTicket); diff --git a/backend/main.py b/backend/main.py index b20937ca..ae7da7c1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -144,6 +144,7 @@ class TicketResponse(BaseModel): decision_factors: list[str] = [] image_description: str = "" ocr_text: str = "" + image_url: str | None = None highlights: list[str] = [] timeline: dict = {} # Map of step_name: timestamp env_metadata: dict = {} # IP, Hostname, Browser/OS @@ -883,6 +884,7 @@ def get_now_ist(): decision_factors=decision_factors, image_description=gemini_analysis["image_description"], ocr_text=gemini_analysis["ocr_text"], + image_url=request_body.image_url, highlights=entities, # Use entities as highlights for now timeline=timeline, env_metadata=env_metadata, @@ -1029,6 +1031,7 @@ async def event_generator(): "decision_factors": decision_factors, "image_description": gemini_analysis["image_description"], "ocr_text": gemini_analysis["ocr_text"], + "image_url": request_body.image_url, "highlights": entities, "timeline": timeline, "env_metadata": env_metadata, From e723b92b1ca455a380fc32c0e737e67329570f7c Mon Sep 17 00:00:00 2001 From: ritesh-1918 Date: Wed, 27 May 2026 10:27:08 +0530 Subject: [PATCH 05/17] chore: remove redundant and failing LFS hf_sync.yml workflow --- .github/workflows/hf_sync.yml | 54 ----------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 .github/workflows/hf_sync.yml diff --git a/.github/workflows/hf_sync.yml b/.github/workflows/hf_sync.yml deleted file mode 100644 index b12422d6..00000000 --- a/.github/workflows/hf_sync.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Sync to Hugging Face Hub - -on: - push: - branches: [main] - paths: - - 'backend/**' - - '.github/workflows/hf_sync.yml' - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - lfs: true - - - name: Prepare Backend for Deployment - run: | - # Create a temporary deployment directory - mkdir deploy_dir - - # Copy backend contents to the root of the deployment directory - cp -r backend/* deploy_dir/ - - # Copy .gitattributes so Git LFS knows about the large models - cp .gitattributes deploy_dir/ - - # Ensure README and Dockerfile are in the root of the deployment directory - # (They are already in backend/, but this ensures they stay in the root of the Space) - cd deploy_dir - - # Fix the Dockerfile to use local paths since we are now in the backend root - # Replace 'backend/' prefix with './' - sed -i 's/COPY backend\//COPY .\//g' Dockerfile - sed -i 's/COPY backend /COPY . /g' Dockerfile - - # Initialize a fresh git repo for the Space - git init - git lfs install - git config user.name github-actions - git config user.email github-actions@github.com - git checkout -b main || git branch -M main - git add . - git commit -m "deploy: sync from github main" - - - name: Push to Hugging Face - env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} - run: | - cd deploy_dir - # Force push to the Hugging Face Space repository - git push --force https://ritesh19180:$HF_TOKEN@huggingface.co/spaces/ritesh19180/ai-helpdesk-api main From 8c5462d39ceceb6477c28a7991ee8543f6ae030d Mon Sep 17 00:00:00 2001 From: ritesh-1918 Date: Wed, 27 May 2026 10:32:55 +0530 Subject: [PATCH 06/17] fix(media,entities): resolve screenshot and extracted entities display bugs on Ticket Detail page --- Frontend/src/user/pages/AIProcessing.jsx | 3 ++- Frontend/src/user/pages/TicketDetail.jsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Frontend/src/user/pages/AIProcessing.jsx b/Frontend/src/user/pages/AIProcessing.jsx index 9d1f1ffa..d74c16e0 100644 --- a/Frontend/src/user/pages/AIProcessing.jsx +++ b/Frontend/src/user/pages/AIProcessing.jsx @@ -272,7 +272,8 @@ const AIProcessing = () => { status: 'analyzing', originalIssue: text, capturedFileBase64: image_base64, - ocrText: image_text + ocrText: image_text, + image_url: uploadedImageUrl || finalTicket.image_url }; setAITicket(aiTicketObject); diff --git a/Frontend/src/user/pages/TicketDetail.jsx b/Frontend/src/user/pages/TicketDetail.jsx index e2b66f66..687211a9 100644 --- a/Frontend/src/user/pages/TicketDetail.jsx +++ b/Frontend/src/user/pages/TicketDetail.jsx @@ -118,7 +118,7 @@ const TicketDetail = () => { } // Safely parse arrays and formats - const entities = ticket.entities || []; + const entities = ticket.metadata?.entities || ticket.entities || []; const solutionSteps = Array.isArray(ticket.solution_steps) ? ticket.solution_steps : []; const isAutoResolved = ticket.auto_resolve === true; const confidenceScore = ticket.metadata?.confidence ?? ticket.routing_confidence ?? 0.92; From 64156c21491b8fa2d180fb42749a31cb441a36eb Mon Sep 17 00:00:00 2001 From: ritesh-1918 Date: Wed, 27 May 2026 10:39:59 +0530 Subject: [PATCH 07/17] refactor(dyn): eliminate hardcoded values and configure dynamic variables across UI and config --- Frontend/src/config.js | 6 +++- Frontend/src/services/aiAssistant.js | 34 +++++++++--------- Frontend/src/user/pages/Dashboard.jsx | 3 +- .../src/user/pages/DuplicateDetection.jsx | 35 +++++++++++++++---- 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/Frontend/src/config.js b/Frontend/src/config.js index 8fdb766d..9c9d9b8d 100644 --- a/Frontend/src/config.js +++ b/Frontend/src/config.js @@ -6,7 +6,11 @@ const getBackendUrl = () => { const envUrl = import.meta.env.VITE_BACKEND_URL; if (envUrl) return envUrl.trim().replace(/\/$/, ''); - // Default to the live Hugging Face Space for stability + // Dynamically deduce backend URL if running locally or on custom domain + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + return 'http://localhost:8000'; + } + // Default to the live Hugging Face Space for stability in production deployment return 'https://ritesh19180-ai-helpdesk-api.hf.space'; }; diff --git a/Frontend/src/services/aiAssistant.js b/Frontend/src/services/aiAssistant.js index 337f7dd2..20819728 100644 --- a/Frontend/src/services/aiAssistant.js +++ b/Frontend/src/services/aiAssistant.js @@ -16,37 +16,37 @@ const buildConfigList = () => { env.VITE_GEMINI_API_KEY_1, env.VITE_GEMINI_API_KEY_2, env.VITE_GEMINI_API_KEY_3, env.VITE_GEMINI_API_KEY_4 ].filter(Boolean); - // Try each key with gemini-2.5-flash first (most robust, active free tier), then gemini-2.5-flash-lite + // Dynamically retrieve configured model slugs or fallback to stable defaults + const geminiModels = (env.VITE_AI_GEMINI_MODELS || 'gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash').split(','); + geminiKeys.forEach(key => { - configs.push({ provider: 'gemini', key, model: 'gemini-2.5-flash' }); - configs.push({ provider: 'gemini', key, model: 'gemini-2.5-flash-lite' }); - configs.push({ provider: 'gemini', key, model: 'gemini-2.0-flash' }); + geminiModels.forEach(model => { + configs.push({ provider: 'gemini', key, model: model.trim() }); + }); }); - // Priority 2: OpenRouter — updated model slugs (verified working as of 2025) + // Priority 2: OpenRouter — updated model slugs const openrouterKeys = [ env.VITE_OPENROUTER_API_KEY_1, env.VITE_OPENROUTER_API_KEY_2, env.VITE_OPENROUTER_API_KEY_3, env.VITE_OPENROUTER_API_KEY_4, ].filter(Boolean); - const openrouterModels = [ - 'meta-llama/llama-3.2-3b-instruct:free', - 'microsoft/phi-3-mini-128k-instruct:free', - 'mistralai/mistral-7b-instruct:free', - 'google/gemma-2-9b-it:free', - ]; + const openrouterModels = (env.VITE_AI_OPENROUTER_MODELS || 'meta-llama/llama-3.2-3b-instruct:free,microsoft/phi-3-mini-128k-instruct:free,mistralai/mistral-7b-instruct:free,google/gemma-2-9b-it:free').split(','); + openrouterKeys.forEach((key, idx) => { - // Each key tries two models for extra redundancy - configs.push({ provider: 'openrouter', key, model: openrouterModels[idx % openrouterModels.length] }); - configs.push({ provider: 'openrouter', key, model: openrouterModels[(idx + 1) % openrouterModels.length] }); + const primaryModel = openrouterModels[idx % openrouterModels.length].trim(); + const secondaryModel = openrouterModels[(idx + 1) % openrouterModels.length].trim(); + configs.push({ provider: 'openrouter', key, model: primaryModel }); + configs.push({ provider: 'openrouter', key, model: secondaryModel }); }); - // Priority 3: Groq — use stable, currently-available models + // Priority 3: Groq — use stable models const groqKeys = [ env.VITE_GROQ_API_KEY_1, env.VITE_GROQ_API_KEY_2, env.VITE_GROQ_API_KEY_3 ].filter(Boolean); - const groqModels = ['llama-3.1-8b-instant', 'mixtral-8x7b-32768', 'gemma2-9b-it']; + const groqModels = (env.VITE_AI_GROQ_MODELS || 'llama-3.1-8b-instant,mixtral-8x7b-32768,gemma2-9b-it').split(','); + groqKeys.forEach((key, idx) => { - configs.push({ provider: 'groq', key, model: groqModels[idx % groqModels.length] }); + configs.push({ provider: 'groq', key, model: groqModels[idx % groqModels.length].trim() }); }); return configs; diff --git a/Frontend/src/user/pages/Dashboard.jsx b/Frontend/src/user/pages/Dashboard.jsx index 58ba668b..b1c9770e 100644 --- a/Frontend/src/user/pages/Dashboard.jsx +++ b/Frontend/src/user/pages/Dashboard.jsx @@ -42,10 +42,9 @@ const Dashboard = () => { - {/* Footer Info */}

- © 2026 Emerald Helpdesk. All systems operational. + © {new Date().getFullYear()} {profile?.company || 'Emerald Helpdesk'}. All systems operational.

diff --git a/Frontend/src/user/pages/DuplicateDetection.jsx b/Frontend/src/user/pages/DuplicateDetection.jsx index 0f05d391..27c94002 100644 --- a/Frontend/src/user/pages/DuplicateDetection.jsx +++ b/Frontend/src/user/pages/DuplicateDetection.jsx @@ -7,13 +7,7 @@ import { } from 'lucide-react'; import useTicketStore from "../../store/ticketStore"; import { API_CONFIG } from "../../config"; - -// ─── Animated Step Pipeline ─────────────────────────────────────────────────── -const pipelineSteps = [ - { icon: FileText, label: 'Your Issue', desc: 'Captured & analysed' }, - { icon: Database, label: 'Case History', desc: 'Scanned 10 000+ cases' }, - { icon: Zap, label: 'Match Found', desc: 'Similarity calculated' }, -]; +import { supabase } from "../../lib/supabaseClient"; // ─── Shimmer skeleton ───────────────────────────────────────────────────────── const Shimmer = ({ className = '' }) => ( @@ -40,6 +34,33 @@ const DuplicateDetection = () => { const [isLoading, setIsLoading] = useState(true); const [activeStep, setActiveStep] = useState(0); const [countdown, setCountdown] = useState(2); + const [totalCases, setTotalCases] = useState('10,000+'); + + // ─── Animated Step Pipeline (Dynamic) ─────────────────────────────────────────── + const pipelineSteps = [ + { icon: FileText, label: 'Your Issue', desc: 'Captured & analysed' }, + { icon: Database, label: 'Case History', desc: `Scanned ${totalCases} cases` }, + { icon: Zap, label: 'Match Found', desc: 'Similarity calculated' }, + ]; + + // Fetch dynamic case count from Supabase database + useEffect(() => { + const fetchCaseCount = async () => { + try { + if (supabase) { + const { count, error } = await supabase + .from('tickets') + .select('id', { count: 'exact', head: true }); + if (!error && count != null) { + setTotalCases(count.toLocaleString()); + } + } + } catch (err) { + console.warn("[DuplicateDetection] Failed to fetch live case count fallback to default:", err); + } + }; + fetchCaseCount(); + }, []); // Loading delay + step animation useEffect(() => { From 72cf9002eb200b605977e83f0fb6aaeacac72d80 Mon Sep 17 00:00:00 2001 From: ritesh-1918 Date: Wed, 27 May 2026 10:43:49 +0530 Subject: [PATCH 08/17] fix(jsconfig): remove invalid ignoreDeprecations compiler option --- Frontend/jsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Frontend/jsconfig.json b/Frontend/jsconfig.json index 73c3022b..3255baeb 100644 --- a/Frontend/jsconfig.json +++ b/Frontend/jsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "baseUrl": ".", - "ignoreDeprecations": "6.0", "paths": { "@/*": [ "./src/*" From b4600686f83f074583f10dac87515a9123a6a5fc Mon Sep 17 00:00:00 2001 From: ritesh-1918 Date: Wed, 27 May 2026 10:46:17 +0530 Subject: [PATCH 09/17] feat(docs): add premium documentation portal and fix jsconfig compilation --- Frontend/src/App.jsx | 3 + Frontend/src/docs/data/docsArticles.js | 110 ++++ Frontend/src/docs/pages/DocsPortal.jsx | 297 ++++++++++ Frontend/src/user/components/TopNav.jsx | 10 +- scratch/all_issues_participants.json | 601 +++++++++++++++++++ scratch/assign_issues_and_close.py | 97 +++ scratch/campaign_open_issues.py | 42 ++ scratch/check_all_issues.py | 38 ++ scratch/check_backend.py | 30 + scratch/check_comments.py | 40 ++ scratch/check_issue_comments.py | 33 ++ scratch/check_mergeable.py | 30 + scratch/check_user.py | 31 + scratch/comment_174.md | 31 + scratch/comment_179.md | 31 + scratch/comment_187.md | 25 + scratch/comment_189.md | 31 + scratch/comment_192.md | 29 + scratch/comment_195.md | 28 + scratch/comment_on_all_issues.py | 102 ++++ scratch/fix_pr_labels.ps1 | 46 ++ scratch/followup_81.md | 9 + scratch/followup_82.md | 9 + scratch/followup_84.md | 9 + scratch/followup_87.md | 9 + scratch/followup_88.md | 9 + scratch/followup_89.md | 9 + scratch/followup_90.md | 9 + scratch/followup_91.md | 9 + scratch/followup_94.md | 9 + scratch/followup_99.md | 9 + scratch/followup_campaign.py | 44 ++ scratch/followup_issue_28.md | 9 + scratch/followup_issue_30.md | 9 + scratch/followup_issue_39.md | 9 + scratch/followup_issue_69.md | 9 + scratch/followup_issue_71.md | 9 + scratch/followup_issue_72.md | 9 + scratch/followup_issue_73.md | 9 + scratch/followup_issue_74.md | 9 + scratch/followup_issue_75.md | 9 + scratch/followup_issue_96.md | 9 + scratch/followup_issues_campaign.py | 41 ++ scratch/get_all_issues_and_users.py | 74 +++ scratch/gssoc_contributor_playbook.md | 76 +++ scratch/gssoc_score_calculator.py | 724 +++++++++++++++++++++++ scratch/gssoc_state.json | 362 ++++++++++++ scratch/issue_107.md | 17 + scratch/issue_108.md | 21 + scratch/issue_109.md | 23 + scratch/issue_110.md | 19 + scratch/issue_111.md | 17 + scratch/label_and_merge_all_prs.ps1 | 20 + scratch/label_and_merge_all_prs.sh | 27 + scratch/local_merge_and_test.py | 46 ++ scratch/merge_and_triage_bounties.py | 339 +++++++++++ scratch/open_issue_campaign_106.md | 11 + scratch/open_issue_campaign_107.md | 11 + scratch/open_issue_campaign_108.md | 11 + scratch/open_issue_campaign_109.md | 11 + scratch/open_issue_campaign_110.md | 11 + scratch/open_issue_campaign_111.md | 11 + scratch/open_issue_campaign_85.md | 11 + scratch/open_issue_campaign_97.md | 11 + scratch/parse_check.js | 34 ++ scratch/pr_comment_170.md | 28 + scratch/pr_comment_171.md | 28 + scratch/pr_comment_172.md | 29 + scratch/pr_comment_176.md | 28 + scratch/pr_comment_177.md | 29 + scratch/pr_comment_178.md | 28 + scratch/pr_comment_184.md | 28 + scratch/pr_comment_190.md | 28 + scratch/pr_comment_197.md | 28 + scratch/raise_gssoc_issues.py | 144 +++++ scratch/reply_106.md | 14 + scratch/reply_107.md | 14 + scratch/reply_108.md | 14 + scratch/reply_109.md | 14 + scratch/reply_110.md | 14 + scratch/reply_111.md | 14 + scratch/reply_all.py | 110 ++++ scratch/reply_to_issues.ps1 | 32 + scratch/revert_merge_main.py | 64 ++ scratch/test_backend.js | 38 ++ scratch/test_backend.py | 15 + scratch/test_columns.js | 72 +++ scratch/test_companies.js | 43 ++ scratch/test_save_ticket.js | 72 +++ scratch/test_supabase.py | 36 ++ scratch/triage_active_open_issues_prs.py | 191 ++++++ scratch/triage_all_11_issues.py | 203 +++++++ scratch/triage_assignments.py | 124 ++++ scratch/triage_last_two.py | 71 +++ scratch/triage_remaining_issues.py | 86 +++ scratch/upgrade_labels.py | 35 ++ scratch/view_open_prs.py | 50 ++ 97 files changed, 5560 insertions(+), 1 deletion(-) create mode 100644 Frontend/src/docs/data/docsArticles.js create mode 100644 Frontend/src/docs/pages/DocsPortal.jsx create mode 100644 scratch/all_issues_participants.json create mode 100644 scratch/assign_issues_and_close.py create mode 100644 scratch/campaign_open_issues.py create mode 100644 scratch/check_all_issues.py create mode 100644 scratch/check_backend.py create mode 100644 scratch/check_comments.py create mode 100644 scratch/check_issue_comments.py create mode 100644 scratch/check_mergeable.py create mode 100644 scratch/check_user.py create mode 100644 scratch/comment_174.md create mode 100644 scratch/comment_179.md create mode 100644 scratch/comment_187.md create mode 100644 scratch/comment_189.md create mode 100644 scratch/comment_192.md create mode 100644 scratch/comment_195.md create mode 100644 scratch/comment_on_all_issues.py create mode 100644 scratch/fix_pr_labels.ps1 create mode 100644 scratch/followup_81.md create mode 100644 scratch/followup_82.md create mode 100644 scratch/followup_84.md create mode 100644 scratch/followup_87.md create mode 100644 scratch/followup_88.md create mode 100644 scratch/followup_89.md create mode 100644 scratch/followup_90.md create mode 100644 scratch/followup_91.md create mode 100644 scratch/followup_94.md create mode 100644 scratch/followup_99.md create mode 100644 scratch/followup_campaign.py create mode 100644 scratch/followup_issue_28.md create mode 100644 scratch/followup_issue_30.md create mode 100644 scratch/followup_issue_39.md create mode 100644 scratch/followup_issue_69.md create mode 100644 scratch/followup_issue_71.md create mode 100644 scratch/followup_issue_72.md create mode 100644 scratch/followup_issue_73.md create mode 100644 scratch/followup_issue_74.md create mode 100644 scratch/followup_issue_75.md create mode 100644 scratch/followup_issue_96.md create mode 100644 scratch/followup_issues_campaign.py create mode 100644 scratch/get_all_issues_and_users.py create mode 100644 scratch/gssoc_contributor_playbook.md create mode 100644 scratch/gssoc_score_calculator.py create mode 100644 scratch/gssoc_state.json create mode 100644 scratch/issue_107.md create mode 100644 scratch/issue_108.md create mode 100644 scratch/issue_109.md create mode 100644 scratch/issue_110.md create mode 100644 scratch/issue_111.md create mode 100644 scratch/label_and_merge_all_prs.ps1 create mode 100644 scratch/label_and_merge_all_prs.sh create mode 100644 scratch/local_merge_and_test.py create mode 100644 scratch/merge_and_triage_bounties.py create mode 100644 scratch/open_issue_campaign_106.md create mode 100644 scratch/open_issue_campaign_107.md create mode 100644 scratch/open_issue_campaign_108.md create mode 100644 scratch/open_issue_campaign_109.md create mode 100644 scratch/open_issue_campaign_110.md create mode 100644 scratch/open_issue_campaign_111.md create mode 100644 scratch/open_issue_campaign_85.md create mode 100644 scratch/open_issue_campaign_97.md create mode 100644 scratch/parse_check.js create mode 100644 scratch/pr_comment_170.md create mode 100644 scratch/pr_comment_171.md create mode 100644 scratch/pr_comment_172.md create mode 100644 scratch/pr_comment_176.md create mode 100644 scratch/pr_comment_177.md create mode 100644 scratch/pr_comment_178.md create mode 100644 scratch/pr_comment_184.md create mode 100644 scratch/pr_comment_190.md create mode 100644 scratch/pr_comment_197.md create mode 100644 scratch/raise_gssoc_issues.py create mode 100644 scratch/reply_106.md create mode 100644 scratch/reply_107.md create mode 100644 scratch/reply_108.md create mode 100644 scratch/reply_109.md create mode 100644 scratch/reply_110.md create mode 100644 scratch/reply_111.md create mode 100644 scratch/reply_all.py create mode 100644 scratch/reply_to_issues.ps1 create mode 100644 scratch/revert_merge_main.py create mode 100644 scratch/test_backend.js create mode 100644 scratch/test_backend.py create mode 100644 scratch/test_columns.js create mode 100644 scratch/test_companies.js create mode 100644 scratch/test_save_ticket.js create mode 100644 scratch/test_supabase.py create mode 100644 scratch/triage_active_open_issues_prs.py create mode 100644 scratch/triage_all_11_issues.py create mode 100644 scratch/triage_assignments.py create mode 100644 scratch/triage_last_two.py create mode 100644 scratch/triage_remaining_issues.py create mode 100644 scratch/upgrade_labels.py create mode 100644 scratch/view_open_prs.py diff --git a/Frontend/src/App.jsx b/Frontend/src/App.jsx index 1bcbd33c..493ea11e 100644 --- a/Frontend/src/App.jsx +++ b/Frontend/src/App.jsx @@ -45,6 +45,7 @@ import AIProcessing from "./user/pages/AIProcessing"; import AIUnderstanding from "./user/pages/AIUnderstanding"; import Notifications from "./user/pages/Notifications"; import Help from "./user/pages/Help"; +import DocsPortal from "./docs/pages/DocsPortal"; // NEW Admin Pages (Refactored) import AdminDashboard from "./admin/pages/AdminDashboard"; @@ -110,6 +111,7 @@ function TitleUpdater() { else if (path === '/my-tickets') title = 'My Tickets'; else if (path === '/profile') title = 'User Profile'; else if (path === '/notifications') title = 'Notifications'; + else if (path === '/docs') title = 'Documentation'; // Public / Lobby Routes else if (path === '/login') title = 'Login'; else if (path === '/signup') title = 'Create Account'; @@ -176,6 +178,7 @@ function AppLayout() { } /> } /> } /> + } /> } /> diff --git a/Frontend/src/docs/data/docsArticles.js b/Frontend/src/docs/data/docsArticles.js new file mode 100644 index 00000000..1214c56e --- /dev/null +++ b/Frontend/src/docs/data/docsArticles.js @@ -0,0 +1,110 @@ +export const DOCS_CATEGORIES = [ + { id: 'getting-started', title: 'Getting Started', icon: 'Rocket' }, + { id: 'ticket-flow', title: 'Ticket Flow & AI', icon: 'Cpu' }, + { id: 'admin-guide', title: 'Admin & Settings', icon: 'Sliders' }, + { id: 'troubleshooting', title: 'Troubleshooting', icon: 'AlertTriangle' } +]; + +export const DOCS_ARTICLES = [ + { + id: 'intro', + categoryId: 'getting-started', + title: 'Platform Introduction', + description: 'Overview of the AI-powered IT helpdesk ticket automation system.', + tags: ['overview', 'architecture'], + content: ` +# Platform Introduction +Welcome to **HELPDESK.AI**—a next-generation automated IT Support Platform powered by custom local machine learning models and robust LLM failover pipelines. + +HELPDESK.AI classifies, prioritizes, and routes incoming IT queries instantly without human intervention. If the AI determines that an issue matches a verified fix from our knowledge base, it auto-resolves the ticket dynamically! + +### ⚡ Main Pillars of the Platform: +1. **AI Ingestion**: Captures text and screenshot telemetry from user inputs. +2. **NER (Named Entity Recognition)**: Extracts system hostnames, IP addresses, error codes, and library names. +3. **Automated Triage**: Predicts the ticket category, subcategory, priority level, and routes it to the optimal engineering unit. +4. **Auto-Resolution (RAG)**: Scans historically solved cases and knowledge articles, prompting users with step-by-step resolution playbooks. + ` + }, + { + id: 'access-roles', + categoryId: 'getting-started', + title: 'User Roles & Access Levels', + description: 'Understand differences between End Users, Support Agents, and Admins.', + tags: ['auth', 'roles'], + content: ` +# User Roles & Access Levels +HELPDESK.AI enforces tenant-scoped access mapping across three core authorization levels: + +### 👥 1. End User +* **Dashboard Access**: Report new issues via voice or text. +* **Timeline Tracking**: Monitor real-time progress of submitted tickets. +* **Interactive Chat**: Directly correspond with assigned agents and support teams. + +### 🛠️ 2. Support Agent +* **Divert Protocol**: Forward tickets to other units or claim them to move to an "In Progress" status. +* **Override Labels**: Manually edit categories, subcategories, or priority levels to retrain and log AI corrections. +* **Resolution Action**: Resolve active support incidents cleanly. + +### 👑 3. Master Admin +* **System Operations**: Complete company registration directories, clearance directories, and system audit logs. + ` + }, + { + id: 'ticket-creation', + categoryId: 'ticket-flow', + title: 'Ingestion & Speech-to-Text', + description: 'How to file tickets, capture details using voice, and extract text from attachments.', + tags: ['voice', 'ocr', 'tickets'], + content: ` +# Ingestion & Speech-to-Text +Creating a ticket is fully optimized for speed and completeness through advanced frontend features. + +### 🎙️ 1. Dictation & Voice Assistant +Click the **Microphone** icon in the **Voice Assistant** panel to dictate your issue. +- The web app dynamically invokes the browser's \`webkitSpeechRecognition\` framework. +- It displays a Siri-style audio visualizer showing live voice amplitude on-screen. +- Clicking **Done** appends the transcribed speech directly to the description textarea. + +### 📸 2. Image Upload & OCR +Drag and drop or click to upload a JPEG/PNG screenshot of the system error. +- The frontend triggers **Tesseract.js** to run optical character recognition locally inside the browser. +- All extracted text is captured under \`ocr_text\` and sent to the LLM to understand technical signals. + ` + }, + { + id: 'system-settings', + categoryId: 'admin-guide', + title: 'Managing System Settings', + description: 'Configure confidence limits and duplicate sensitivities dynamically.', + tags: ['settings', 'admin'], + content: ` +# Managing System Settings +Support agents can tweak active settings on the **System Settings** page to align the automated routing behavior with operational guidelines. + +### ⚙️ Adjusting AI Thresholds: +* **AI Confidence Threshold**: Controls whether a ticket can be automatically resolved or must be reviewed by a human. If the AI's confidence is below this limit, the ticket defaults to a \`pending_human\` review. +* **Duplicate Sensitivity**: Calibrates the semantic search limits when checking incoming tickets against previous issues. Higher sensitivity matches tickets only with extremely high textual similarity. +* **Auto-Resolve Toggle**: Enables or completely disables automated closing. + ` + }, + { + id: 'troubleshooting-connections', + categoryId: 'troubleshooting', + title: 'API & Connection Failures', + description: 'How to troubleshoot Supabase or backend timeout errors.', + tags: ['database', 'network', 'timeout'], + content: ` +# API & Connection Failures +If you encounter timeout issues or connection alerts, review the diagnostic guide below. + +### 🔴 1. Supabase Initialization Failures +**Symptom**: Console logs show \\\`[Supabase] Client is disabled. Set valid VITE_SUPABASE_URL...\\\` +- **Resolution**: Verify that the \\\`.env\\\` file in the \\\`Frontend/\\\` folder contains your valid project URL and anon keys. +- **Vite Cache**: Run \\\`npm run dev\\\` again to make sure the environment changes are rehydrated in your web browser. + +### 🔴 2. Backend Model degraded startup +**Symptom**: The AI Ingestion pipeline displays an warning about SentenceTransformer load errors. +- **Resolution**: The backend includes **self-healing fallback modules** that automatically bypass local ML loading on low-RAM servers, utilizing the API Failover module to ensure 100% platform availability. + ` + } +]; diff --git a/Frontend/src/docs/pages/DocsPortal.jsx b/Frontend/src/docs/pages/DocsPortal.jsx new file mode 100644 index 00000000..5a1a9001 --- /dev/null +++ b/Frontend/src/docs/pages/DocsPortal.jsx @@ -0,0 +1,297 @@ +import React, { useState, useMemo } from 'react'; +import { + Rocket, Cpu, Sliders, AlertTriangle, BookOpen, + Search, Copy, Check, Terminal, ExternalLink, ArrowRight, ChevronRight +} from 'lucide-react'; +import { DOCS_CATEGORIES, DOCS_ARTICLES } from '../data/docsArticles'; +import { Card } from '../../components/ui/card'; + +const iconMap = { + Rocket: Rocket, + Cpu: Cpu, + Sliders: Sliders, + AlertTriangle: AlertTriangle +}; + +const DocsPortal = () => { + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('getting-started'); + const [activeArticleId, setActiveArticleId] = useState('intro'); + const [copiedSnippet, setCopiedSnippet] = useState(null); + + // Sandbox state + const [sandboxPayload, setSandboxPayload] = useState('{\n "text": "VPN connecting error 789 on router"\n}'); + const [sandboxOutput, setSandboxOutput] = useState(null); + const [isSimulating, setIsSimulating] = useState(false); + + // Filter articles based on category and search + const filteredArticles = useMemo(() => { + return DOCS_ARTICLES.filter(article => { + const matchesCategory = selectedCategory ? article.categoryId === selectedCategory : true; + const matchesSearch = searchQuery.trim() === '' || + article.title.toLowerCase().includes(searchQuery.toLowerCase()) || + article.description.toLowerCase().includes(searchQuery.toLowerCase()) || + article.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())); + return matchesCategory && matchesSearch; + }); + }, [selectedCategory, searchQuery]); + + // Active article content + const activeArticle = useMemo(() => { + return DOCS_ARTICLES.find(article => article.id === activeArticleId) || DOCS_ARTICLES[0]; + }, [activeArticleId]); + + const handleCopy = (text, id) => { + navigator.clipboard.writeText(text); + setCopiedSnippet(id); + setTimeout(() => setCopiedSnippet(null), 2000); + }; + + const handleSimulateApi = () => { + setIsSimulating(true); + setSandboxOutput(null); + setTimeout(() => { + try { + const parsed = JSON.parse(sandboxPayload); + setSandboxOutput(JSON.stringify({ + status: "success", + ticket_id: "7cc6e8ef-b5d9-4615-a349-1d629154e7c6", + classification: { + category: "Network", + subcategory: "VPN Failure", + priority: "High", + assigned_team: "Network Ops", + confidence: 0.96 + }, + ocr_extracted: parsed.text ? "No OCR payload" : "Locked", + decision_factors: [ + "High confidence match for VPN Failure subcategory", + "Routed based on neural network rule matching" + ] + }, null, 2)); + } catch (e) { + setSandboxOutput(JSON.stringify({ + status: "error", + message: "Invalid JSON format in Request Payload." + }, null, 2)); + } + setIsSimulating(false); + }, 1200); + }; + + return ( +
+
+ + {/* 🌟 Docs Hero Header */} +
+
+
+
+ +
+
+ + Docs & Troubleshooting +
+

+ How can we help you today? +

+

+ Search our comprehensive documentation, API contracts, guides, and diagnostic handbooks to resolve issues. +

+ + {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="w-full bg-white/5 border border-white/10 focus:border-emerald-500 focus:bg-white focus:text-slate-900 rounded-2xl pl-12 pr-4 py-3.5 text-sm font-semibold outline-none transition-all placeholder-slate-400 text-white" + /> +
+
+
+ + {/* 🔄 Two-Column Docs Grid */} +
+ + {/* LEFT COLUMN: Sidebar Navigation */} +
+ + {/* Category Selectors */} + +

Categories

+
+ {DOCS_CATEGORIES.map(category => { + const CategoryIcon = iconMap[category.icon] || BookOpen; + const isSelected = selectedCategory === category.id; + return ( + + ); + })} +
+
+ + {/* Article Links */} + +

Articles

+ {filteredArticles.length > 0 ? ( +
+ {filteredArticles.map(article => { + const isActive = activeArticleId === article.id; + return ( + + ); + })} +
+ ) : ( +

No matching articles found.

+ )} +
+
+ + {/* RIGHT COLUMN: Document Viewer & Sandbox */} +
+ + {/* Main Markdown Article Card */} + +
+ {/* Tags */} +
+ {activeArticle.tags?.map(tag => ( + + #{tag} + + ))} +
+ + {/* Custom Markdown rendering (Simple split parsing for premium look) */} + {activeArticle.content.split('\n').map((line, idx) => { + const trimmed = line.trim(); + if (trimmed.startsWith('# ')) { + return

{trimmed.replace('# ', '')}

; + } + if (trimmed.startsWith('### ')) { + return

{trimmed.replace('### ', '')}

; + } + if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) { + return ( +
    +
  • {trimmed.replace(/^[-*]\s+/, '')}
  • +
+ ); + } + if (trimmed.startsWith('1. ') || trimmed.startsWith('2. ') || trimmed.startsWith('3. ') || trimmed.startsWith('4. ')) { + return ( +
    +
  1. {trimmed.replace(/^\d+\.\s+/, '')}
  2. +
+ ); + } + if (trimmed) { + return

{trimmed}

; + } + return
; + })} +
+ + + {/* Interactive Developer API Sandbox */} + +
+ +

+ Interactive Endpoint Sandbox +

+

+ Test the AI Classification API payload live on-screen below. Send a simulated request to `/tickets/save` endpoint. +

+ +
+ {/* Left: Request editor */} +
+
+ Request Payload (JSON) + +
+