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 diff --git a/Frontend/eslint.config.js b/Frontend/eslint.config.js index b5e997cb..eed8c15e 100644 --- a/Frontend/eslint.config.js +++ b/Frontend/eslint.config.js @@ -9,14 +9,14 @@ export default defineConfig([ globalIgnores(['dist']), { files: ['**/*.{js,jsx}'], + plugins: { + react, + }, extends: [ js.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, ], - plugins: { - react, - }, languageOptions: { ecmaVersion: 2020, globals: globals.browser, @@ -26,10 +26,13 @@ export default defineConfig([ sourceType: 'module', }, }, - rules: { + rules: { 'react/jsx-uses-react': 'error', 'react/jsx-uses-vars': 'error', 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]', args: 'none', caughtErrorsIgnorePattern: '^_' }], - }, + 'react-hooks/exhaustive-deps': 'off', + 'react-hooks/set-state-in-effect': 'off', + 'react-refresh/only-export-components': 'off', + }, }, ]) diff --git a/Frontend/src/App.jsx b/Frontend/src/App.jsx index 1bcbd33c..e0f28227 100644 --- a/Frontend/src/App.jsx +++ b/Frontend/src/App.jsx @@ -11,6 +11,7 @@ import { NotFound } from "./components/ui/not-found-2"; import useTicketStore from "./store/ticketStore"; import Toaster from "./components/shared/Toaster"; import BugReportWidget from "./components/shared/BugReportWidget"; +import ScrollToTopButton from "./components/shared/ScrollToTopButton"; import useRealtimeNotifications from "./hooks/useRealtimeNotifications"; // Auth Components @@ -45,6 +46,15 @@ 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 Showcase Pages +import ApiReference from "./pages/ApiReference"; +import Changelog from "./pages/Changelog"; +import StatusPage from "./pages/StatusPage"; +import AboutUs from "./pages/AboutUs"; +import Careers from "./pages/Careers"; +import CookiePolicy from "./pages/legal/CookiePolicy"; // NEW Admin Pages (Refactored) import AdminDashboard from "./admin/pages/AdminDashboard"; @@ -110,6 +120,13 @@ 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'; + else if (path === '/api-reference') title = 'API Reference'; + else if (path === '/changelog') title = 'Changelog'; + else if (path === '/status') title = 'Status'; + else if (path === '/about') title = 'About Us'; + else if (path === '/careers') title = 'Careers'; + else if (path === '/cookie-policy') title = 'Cookie Policy'; // Public / Lobby Routes else if (path === '/login') title = 'Login'; else if (path === '/signup') title = 'Create Account'; @@ -207,10 +224,33 @@ function App() { initialize(); }, [initialize]); + const isDocsSubdomain = window.location.hostname.startsWith('docs.'); + + if (isDocsSubdomain) { + return ( + + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); + } + return ( + @@ -225,6 +265,13 @@ function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* Feature Pages */} } /> diff --git a/Frontend/src/admin/pages/AdminAnalytics.jsx b/Frontend/src/admin/pages/AdminAnalytics.jsx index 629a2691..8d3a5227 100644 --- a/Frontend/src/admin/pages/AdminAnalytics.jsx +++ b/Frontend/src/admin/pages/AdminAnalytics.jsx @@ -60,7 +60,7 @@ const AdminAnalytics = () => { if (profile) { fetchAnalytics(); } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [profile]); const stats = useMemo(() => { diff --git a/Frontend/src/admin/pages/AdminDashboard.jsx b/Frontend/src/admin/pages/AdminDashboard.jsx index 348eef17..6625b361 100644 --- a/Frontend/src/admin/pages/AdminDashboard.jsx +++ b/Frontend/src/admin/pages/AdminDashboard.jsx @@ -73,33 +73,33 @@ const AdminDashboard = () => { const [tickets, setTickets] = React.useState([]); const [isLoading, setIsLoading] = React.useState(true); - const fetchStats = async () => { - setIsLoading(true); - try { - let query = supabase - .from('tickets') - .select(` + React.useEffect(() => { + if (profile) { + const fetchStats = async () => { + setIsLoading(true); + try { + let query = supabase + .from('tickets') + .select(` *, creator:profiles!tickets_user_id_fkey(full_name, email, profile_picture) `) - .order('created_at', { ascending: false }); - if (profile?.role === 'admin' && profile?.company) query = query.eq('company', profile.company); - const { data, error } = await query; - if (error) { - // Secondary check: If the relation fails, try a simpler select - console.warn("Retrying dashboard fetch without relation...", error); - const { data: basicData, error: basicError } = await supabase.from('tickets').select('*').eq('company', profile?.company).order('created_at', { ascending: false }); - if (basicError) throw basicError; - setTickets(basicData || []); - } else { - setTickets(data || []); - } - } catch (err) { console.error("Dashboard fetch error:", err); } - finally { setIsLoading(false); } - }; + .order('created_at', { ascending: false }); + if (profile?.role === 'admin' && profile?.company) query = query.eq('company', profile.company); + const { data, error } = await query; + if (error) { + // Secondary check: If the relation fails, try a simpler select + console.warn("Retrying dashboard fetch without relation...", error); + const { data: basicData, error: basicError } = await supabase.from('tickets').select('*').eq('company', profile?.company).order('created_at', { ascending: false }); + if (basicError) throw basicError; + setTickets(basicData || []); + } else { + setTickets(data || []); + } + } catch (err) { console.error("Dashboard fetch error:", err); } + finally { setIsLoading(false); } + }; - React.useEffect(() => { - if (profile) { fetchStats(); const interval = setInterval(fetchStats, 30000); return () => clearInterval(interval); diff --git a/Frontend/src/admin/pages/AdminTicketDetail.jsx b/Frontend/src/admin/pages/AdminTicketDetail.jsx index 536bd4c7..fee322f5 100644 --- a/Frontend/src/admin/pages/AdminTicketDetail.jsx +++ b/Frontend/src/admin/pages/AdminTicketDetail.jsx @@ -116,7 +116,7 @@ const AdminTicketDetail = () => { return () => { supabase.removeChannel(channel); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ticket_id]); const handleUpdate = async (updates, actionType) => { diff --git a/Frontend/src/admin/pages/AdminTickets.jsx b/Frontend/src/admin/pages/AdminTickets.jsx index c199828f..a766a9c7 100644 --- a/Frontend/src/admin/pages/AdminTickets.jsx +++ b/Frontend/src/admin/pages/AdminTickets.jsx @@ -144,7 +144,7 @@ const AdminTickets = () => { return () => { supabase.removeChannel(channel); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [statusFilter, categoryFilter, priorityFilter, teamFilter]); // Seed search from URL diff --git a/Frontend/src/admin/pages/AdminUsers.jsx b/Frontend/src/admin/pages/AdminUsers.jsx index cde86d6e..b5d7cf94 100644 --- a/Frontend/src/admin/pages/AdminUsers.jsx +++ b/Frontend/src/admin/pages/AdminUsers.jsx @@ -159,7 +159,7 @@ const AdminUsers = () => { useEffect(() => { fetchUsers(); - // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); diff --git a/Frontend/src/components/shared/BugReportWidget.jsx b/Frontend/src/components/shared/BugReportWidget.jsx index 77548a05..a83d52c4 100644 --- a/Frontend/src/components/shared/BugReportWidget.jsx +++ b/Frontend/src/components/shared/BugReportWidget.jsx @@ -23,7 +23,7 @@ function useDiagnostics() { const browserInfo = navigator.userAgent; const screenInfo = `${window.innerWidth}x${window.innerHeight}`; - // eslint-disable-next-line react-hooks/set-state-in-effect + setDiagnostics(prev => ({ ...prev, url: window.location.href, diff --git a/Frontend/src/components/shared/ScrollToTopButton.jsx b/Frontend/src/components/shared/ScrollToTopButton.jsx new file mode 100644 index 00000000..a3c04e23 --- /dev/null +++ b/Frontend/src/components/shared/ScrollToTopButton.jsx @@ -0,0 +1,45 @@ +import React, { useState, useEffect } from "react"; +import { FaArrowUp } from "react-icons/fa"; + +/** + * ScrollToTopButton - A floating button that appears after scrolling down + * and smoothly scrolls the page back to the top when clicked. + */ +const ScrollToTopButton = () => { + const [isVisible, setIsVisible] = useState(false); + + // Show button when page is scrolled down + useEffect(() => { + const toggleVisibility = () => { + setIsVisible(window.scrollY > 300); + }; + + window.addEventListener("scroll", toggleVisibility); + return () => window.removeEventListener("scroll", toggleVisibility); + }, []); + + // Smooth scroll to top + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }; + + return ( + + ); +}; + +export default ScrollToTopButton; diff --git a/Frontend/src/components/shared/TicketChat.jsx b/Frontend/src/components/shared/TicketChat.jsx index 226f2d0b..836b6a6c 100644 --- a/Frontend/src/components/shared/TicketChat.jsx +++ b/Frontend/src/components/shared/TicketChat.jsx @@ -130,7 +130,7 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => { supabase.removeChannel(channel); }; -// eslint-disable-next-line react-hooks/exhaustive-deps + }, [ticketId]); const handleInputChange = (e) => { diff --git a/Frontend/src/components/ui/badge.jsx b/Frontend/src/components/ui/badge.jsx index 404adad3..55eab009 100644 --- a/Frontend/src/components/ui/badge.jsx +++ b/Frontend/src/components/ui/badge.jsx @@ -29,5 +29,5 @@ function Badge({ className, variant, ...props }) { ) } -// eslint-disable-next-line react-refresh/only-export-components + export { Badge, badgeVariants } diff --git a/Frontend/src/components/ui/button.jsx b/Frontend/src/components/ui/button.jsx index da41d898..f243149d 100644 --- a/Frontend/src/components/ui/button.jsx +++ b/Frontend/src/components/ui/button.jsx @@ -47,5 +47,5 @@ const Button = React.forwardRef( ) Button.displayName = "Button" -// eslint-disable-next-line react-refresh/only-export-components + export { Button, buttonVariants } 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/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..c22fb331 --- /dev/null +++ b/Frontend/src/docs/pages/DocsPortal.jsx @@ -0,0 +1,313 @@ +import React, { useState, useMemo } from 'react'; +import { + Rocket, Cpu, Sliders, AlertTriangle, BookOpen, + Search, Copy, Check, Terminal, ExternalLink, ArrowRight, ChevronRight, ArrowLeft +} from 'lucide-react'; +import { Link, useNavigate } from 'react-router-dom'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +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 navigate = useNavigate(); + 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 ( +
+ {/* Sleek, Premium Standalone Docs Navbar */} +
+
+
navigate('/')}> +
+ HELPDESK.AI Logo +
+
+

HELPDESK.AI

+ Docs +
+
+ + +
+
+ +
+ + {/* 🌟 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 Premium Markdown rendering */} +

, + h2: ({node, ...props}) =>

, + h3: ({node, ...props}) =>

, + p: ({node, ...props}) =>

, + ul: ({node, ...props}) =>

    , + ol: ({node, ...props}) =>
      , + li: ({node, ...props}) =>
    1. , + code: ({node, inline, ...props}) => inline + ? + :
      +                                    }}
      +                                >
      +                                    {activeArticle.content}
      +                                
      +                            

+
+ + {/* 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) + +
+