<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nitin7414</title>
    <description>The latest articles on DEV Community by nitin7414 (@nitin7414).</description>
    <link>https://dev.to/nitin7414</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F744637%2Ffbe78bee-d517-4be4-9bf0-211fc1f5561d.jpeg</url>
      <title>DEV Community: nitin7414</title>
      <link>https://dev.to/nitin7414</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nitin7414"/>
    <language>en</language>
    <item>
      <title>Building a Production AI Chatbot for an Educational Institute: Architecture, Lessons &amp; Full Stack Deep-Dive</title>
      <dc:creator>nitin7414</dc:creator>
      <pubDate>Sat, 23 May 2026 10:07:25 +0000</pubDate>
      <link>https://dev.to/nitin7414/building-a-production-ai-chatbot-for-an-educational-institute-architecture-lessons-full-stack-32kn</link>
      <guid>https://dev.to/nitin7414/building-a-production-ai-chatbot-for-an-educational-institute-architecture-lessons-full-stack-32kn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a full-stack AI chatbot platform for IFDA, an Indian educational institute. It handles course discovery, lead capture, appointment scheduling, WhatsApp messaging, and comes with a complete admin CRM — all deployed on Vercel with a Neon PostgreSQL backend. Here's everything I learned, every system I built, and why I made the choices I did.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why This Project Exists
&lt;/h2&gt;

&lt;p&gt;Educational institutes spend enormous resources on human admissions counselors. Prospects ask the same questions repeatedly: &lt;em&gt;What courses do you offer? How long is the program? What will I earn afterwards?&lt;/em&gt; And every unanswered query at 11 PM is a lost lead.&lt;/p&gt;

&lt;p&gt;IFDA needed something smarter than a static FAQ page and cheaper than a 24/7 call center. The solution: a hybrid AI chatbot that could hold intelligent conversations about courses, capture leads, book counseling appointments, and hand off hot prospects to human staff — all while syncing with WhatsApp.&lt;/p&gt;

&lt;p&gt;This is the full technical story of how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture at a Glance
&lt;/h2&gt;

&lt;p&gt;The project is a &lt;strong&gt;Next.js 14 App Router monorepo&lt;/strong&gt; — one codebase serving the public-facing chatbot widget, the admin CRM dashboard, and all API routes. Every component was chosen deliberately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript throughout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Neon PostgreSQL + Prisma ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI / LLM&lt;/td&gt;
&lt;td&gt;OpenAI (GPT) for intent, embeddings, and response generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Vercel (Edge-compatible)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp&lt;/td&gt;
&lt;td&gt;DoubleTick API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Custom JWT + RBAC middleware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session&lt;/td&gt;
&lt;td&gt;Server-side session store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The graph of this codebase has &lt;strong&gt;1,259 nodes&lt;/strong&gt; (files, functions, types) and &lt;strong&gt;3,276 edges&lt;/strong&gt; (calls, contains, references) distributed across &lt;strong&gt;55+ community clusters&lt;/strong&gt; — from the bot engine core, to the Prisma edge runtime, to the admin dashboard pages. Let's walk through each major system.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 1: The Bot Engine (&lt;code&gt;lib/bot/engine.ts&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The heart of the project is &lt;code&gt;processMessage()&lt;/code&gt; in &lt;code&gt;lib/bot/engine.ts&lt;/code&gt;. This single function is the traffic controller for every incoming message — whether it arrives from the web widget or WhatsApp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid Scripted + LLM Architecture
&lt;/h3&gt;

&lt;p&gt;The bot does &lt;strong&gt;not&lt;/strong&gt; send every message to GPT. That would be slow and expensive. Instead, it uses a two-tier approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scripted funnel stages&lt;/strong&gt; — structured flows for lead capture (name, phone, course interest, city) where deterministic logic is faster and more reliable than an LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM fallback&lt;/strong&gt; — for open-ended questions, course comparisons, career queries, or anything outside a defined stage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The stage management lives in &lt;code&gt;lib/ai/intent.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// intent.ts — simplified&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveStage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FunnelStage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMissingFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;LeadField&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getNextQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LeadField&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isLeadComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Intent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;analyzeMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Analysis&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;analyzeMessage()&lt;/code&gt; is the decision function. It evaluates the message in context of the current session stage and decides whether to invoke the scripted path, call &lt;code&gt;getAIResponse()&lt;/code&gt; from &lt;code&gt;lib/ai/llm.ts&lt;/code&gt;, or trigger a carousel/quick-reply block.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Message Components
&lt;/h3&gt;

&lt;p&gt;Responses are not plain strings. They're typed message blocks that the renderer layer converts to channel-specific formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/ai/llm.ts — block builders&lt;/span&gt;
&lt;span class="nf"&gt;textBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TextBlock&lt;/span&gt;
&lt;span class="nf"&gt;domainListBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Domain&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;DomainListBlock&lt;/span&gt;
&lt;span class="nf"&gt;carouselBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CourseCarouselItem&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;CarouselBlock&lt;/span&gt;
&lt;span class="nf"&gt;courseDetailBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CourseDetailBlock&lt;/span&gt;
&lt;span class="nf"&gt;quickRepliesBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;QuickRepliesBlock&lt;/span&gt;
&lt;span class="nf"&gt;visitWebsiteBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;VisitWebsiteBlock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This block-based design means the same engine output drives both the web UI and WhatsApp — two very different rendering surfaces.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 2: Dual-Channel Rendering
&lt;/h2&gt;

&lt;p&gt;The renderer layer translates structured message blocks into channel-specific formats.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Renderer (&lt;code&gt;lib/bot/renderers/web.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;renderToWeb()&lt;/code&gt; converts blocks into React-compatible JSON that the frontend &lt;code&gt;StructureMessage.tsx&lt;/code&gt; component consumes. The component handles scroll behavior, carousels with &lt;code&gt;checkScroll()&lt;/code&gt;, and animated message appearance.&lt;/p&gt;

&lt;h3&gt;
  
  
  WhatsApp Renderer (&lt;code&gt;lib/bot/renderers/whatsapp.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;renderToWhatsApp()&lt;/code&gt; maps the same blocks to the DoubleTick API's message format. WhatsApp has strict message type rules — interactive lists, templates, and plain text are separate API calls with different schemas. The renderer handles all of this through the &lt;code&gt;lib/whatsapp/doubletick.ts&lt;/code&gt; client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// doubletick.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWhatsAppText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWhatsAppTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TemplateParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWhatsAppInteractiveList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InteractiveList&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Incoming WhatsApp messages hit &lt;code&gt;app/api/whatsapp/webhook/route.ts&lt;/code&gt;, which validates the HMAC signature via &lt;code&gt;isValidSignature()&lt;/code&gt;, parses the payload through &lt;code&gt;lib/whatsapp/messageParser.ts&lt;/code&gt;, and feeds it into the same &lt;code&gt;processMessage()&lt;/code&gt; bot engine. One engine, two channels.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 3: Course Intelligence (&lt;code&gt;lib/ai/courseData.ts&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The course catalog is the richest data source in the system. &lt;code&gt;courseData.ts&lt;/code&gt; is a comprehensive module with &lt;strong&gt;25+ exported functions&lt;/strong&gt; that the bot engine calls to build contextually accurate responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;findCourse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nf"&gt;getCourseAbout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCoursePrerequisites&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseSyllabus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseCareerOpportunities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseCareerGrowthRoadmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseProfessionalGrowthLadder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseSkills&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getAllCourseCarouselItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;CourseCarouselItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nf"&gt;getCarouselByIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CourseCarouselItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nf"&gt;getLLMCourseMap&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;LLMCourseMap&lt;/span&gt;        &lt;span class="c1"&gt;// Compact map for GPT context injection&lt;/span&gt;
&lt;span class="nf"&gt;getCourseContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="c1"&gt;// Full context string for LLM prompt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;findCourse()&lt;/code&gt; is the most-called function in the entire application (32 edges in the call graph), supporting fuzzy matching so "digital marketing," "DM course," and "marketing program" all resolve correctly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getLLMCourseMap()&lt;/code&gt; is particularly clever: it generates a compact, token-efficient representation of the course catalog that gets injected into GPT prompts. This gives the LLM accurate knowledge of IFDA's offerings without burning context on verbose descriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 4: The Knowledge Base (&lt;code&gt;lib/ai/knowledge-parser.ts&lt;/code&gt; + Embeddings)
&lt;/h2&gt;

&lt;p&gt;Beyond the structured course catalog, IFDA staff can upload arbitrary knowledge documents — PDFs, CSVs, text files, JSON — through the admin panel. The knowledge pipeline processes these into searchable vector embeddings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parsing Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;knowledge-parser.ts&lt;/code&gt; handles multi-format ingestion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;parseKnowledgeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ParsedKnowledge&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;parsePdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;parseCsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ParsedRow&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nf"&gt;parseJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ParsedKnowledge&lt;/span&gt;
&lt;span class="nf"&gt;parseTxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ParsedKnowledge&lt;/span&gt;
&lt;span class="nf"&gt;chunkText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Semantic Search
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lib/ai/embeddings.ts&lt;/code&gt; handles the vector layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="c1"&gt;// OpenAI text-embedding-3-small&lt;/span&gt;
&lt;span class="nf"&gt;embedAllFaqs&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;                         &lt;span class="c1"&gt;// Bulk embed on import&lt;/span&gt;
&lt;span class="nf"&gt;embedNewFaqs&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;                         &lt;span class="c1"&gt;// Incremental update&lt;/span&gt;
&lt;span class="nf"&gt;searchSimilarFaqs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FAQ&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user asks a question not covered by the scripted engine, &lt;code&gt;getRelevantKnowledge()&lt;/code&gt; fires a vector similarity search against the embedded knowledge base. The top results are injected into the LLM context, giving the bot accurate, up-to-date answers based on admin-uploaded documents — no redeployment required.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 5: Lead Capture &amp;amp; CRM
&lt;/h2&gt;

&lt;p&gt;Lead capture is deeply woven into the conversation flow. As the bot collects information across multiple turns, it progressively builds a lead profile. &lt;code&gt;extractLeadInfo()&lt;/code&gt; in &lt;code&gt;intent.ts&lt;/code&gt; extracts structured data from natural language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// User says: "I'm Rohan from Lucknow, interested in the UI/UX course"&lt;/span&gt;
&lt;span class="c1"&gt;// extractLeadInfo() returns:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rohan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Lucknow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;courseInterest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UI/UX Design&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;verifyHumanName()&lt;/code&gt; guards against bot-abuse by rejecting implausible name strings before they reach the database.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;isLeadComplete()&lt;/code&gt; returns true, &lt;code&gt;pushLeadToCRM()&lt;/code&gt; fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/crm/leads.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pushLeadToCRM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CompleteLead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes to the Neon PostgreSQL database via Prisma and optionally triggers a WhatsApp confirmation template to the lead's number.&lt;/p&gt;

&lt;p&gt;The admin side exposes full CRUD via:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET/POST /api/admin/leads&lt;/code&gt; — list and filter leads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET/PATCH/DELETE /api/admin/leads/[id]&lt;/code&gt; — individual lead management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;AdminLeadsPage&lt;/code&gt; in &lt;code&gt;app/admin/page.tsx&lt;/code&gt; provides a CRM interface with stage tracking — counselors can move leads through the admissions funnel manually, and &lt;code&gt;updateLeadStage()&lt;/code&gt; persists the change.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 6: Appointment Scheduling
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lib/scheduler/calendar.ts&lt;/code&gt; handles the booking flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getAvailableDates&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AvailableDate&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;isSlotAvailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;bookAppointment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TimeSlot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Appointment&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When intent detection identifies scheduling intent (&lt;code&gt;handleScheduling()&lt;/code&gt; in the chatbot route), the bot presents available slots as a quick-reply carousel. The user picks one, and &lt;code&gt;bookAppointment()&lt;/code&gt; writes the appointment to the database and sends a WhatsApp confirmation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/api/schedule/route.ts&lt;/code&gt; is the public endpoint, and &lt;code&gt;app/api/admin/appointments/route.ts&lt;/code&gt; gives admins a view of all upcoming appointments.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 7: Session Management
&lt;/h2&gt;

&lt;p&gt;The bot is stateful across multiple HTTP requests. &lt;code&gt;lib/session/store.ts&lt;/code&gt; manages this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;saveSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;clearSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the client side, &lt;code&gt;app/chatbot/hooks/useChat.ts&lt;/code&gt; manages the &lt;code&gt;sessionId&lt;/code&gt; lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useChat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Generates and persists a sessionId&lt;/span&gt;
  &lt;span class="c1"&gt;// Manages message history&lt;/span&gt;
  &lt;span class="c1"&gt;// Handles loading/error states&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSessionId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session carries funnel stage, partial lead data, conversation history, and the last detected intent — everything &lt;code&gt;processMessage()&lt;/code&gt; needs to pick up exactly where the conversation left off.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 8: Admin Dashboard &amp;amp; RBAC
&lt;/h2&gt;

&lt;p&gt;The admin panel is a full internal application with multiple pages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Functionality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Analytics charts (BarChart), metrics overview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/dashboard/members&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Team member management (add, edit, remove)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lead CRM with stage management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/conversations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full chat history viewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/knowledge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Knowledge file upload/management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/templates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WhatsApp template builder with AI generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Profile management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  RBAC Implementation
&lt;/h3&gt;

&lt;p&gt;The permission system uses a &lt;strong&gt;role-permission matrix&lt;/strong&gt; architecture, documented in &lt;code&gt;implementation_plan.md&lt;/code&gt; and implemented in &lt;code&gt;lib/auth/permissions.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Permission&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDefaultPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminRole&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middleware chain enforces this at the API level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/auth/middleware.ts&lt;/span&gt;
&lt;span class="nf"&gt;requireAdmin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// Verifies JWT, rejects unauthenticated requests&lt;/span&gt;
&lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminRole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Enforces role-based access&lt;/span&gt;
&lt;span class="nf"&gt;getAdminFromRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// Extracts admin context from token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lib/auth/jwt.ts&lt;/code&gt; handles token lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;signToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JWTPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;JWTPayload&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nf"&gt;verifyTokenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;JWTPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sidebar in the admin UI uses &lt;code&gt;hasPermission()&lt;/code&gt; to filter navigation items — counselors see leads and conversations; superadmins see everything including member management and analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  WhatsApp Template Builder
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/admin/templates&lt;/code&gt; page deserves special mention. It's an AI-powered editor where admins compose multi-slide WhatsApp broadcast campaigns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;generateWithAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;// GPT generates slide content from a prompt&lt;/span&gt;
&lt;span class="nf"&gt;addSlide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;removeSlide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// Manage campaign structure&lt;/span&gt;
&lt;span class="nf"&gt;buildWhatsAppText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// Compile slides to WhatsApp text format&lt;/span&gt;
&lt;span class="nf"&gt;buildHTMLBlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;// Compile to HTML preview&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Admins write a brief like "announce the new UI/UX batch starting June 15," and the AI generates formatted, WhatsApp-compliant message slides they can edit before sending.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 9: Analytics
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app/api/admin/analytics/route.ts&lt;/code&gt; aggregates metrics from the database — lead conversion rates, conversation volumes, popular courses, appointment completion rates. The dashboard &lt;code&gt;BarChart&lt;/code&gt; component visualizes these in real time for the admin team.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;types/analytics.ts&lt;/code&gt; defines the typed response schema, and &lt;code&gt;prisma/scripts/testAnalytics.ts&lt;/code&gt; was used during development to seed realistic test data and validate aggregation queries.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Database Schema
&lt;/h2&gt;

&lt;p&gt;All data flows through Prisma ORM on Neon PostgreSQL. Key models include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admin&lt;/strong&gt; — staff accounts with role and permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead&lt;/strong&gt; — prospect data with funnel stage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation / Message&lt;/strong&gt; — full chat history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KnowledgeFile&lt;/strong&gt; — uploaded knowledge documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAQ / FAQEmbedding&lt;/strong&gt; — vector-ready FAQ storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appointment&lt;/strong&gt; — scheduled counseling sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsAppMessage&lt;/strong&gt; — outbound message log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AnalyticsEvent&lt;/strong&gt; — event-level analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prisma generates a full Edge-compatible client (&lt;code&gt;generated/prisma/&lt;/code&gt;) for Vercel's Edge Runtime, enabling low-latency database queries from serverless functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Engineering Challenges &amp;amp; How I Solved Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. GPT Conversation History Corruption
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Multi-turn conversations were getting corrupted when partial lead data was included in the messages array alongside system context. GPT would "remember" previous context injections as user messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Separated session state from conversation history. The &lt;code&gt;messages&lt;/code&gt; array sent to GPT contains only actual user/assistant turns. Lead context and session state are injected exclusively in the system prompt, rebuilt fresh on every request from &lt;code&gt;getSession()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Edge Runtime + Prisma
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Prisma's standard Node.js client uses modules incompatible with Vercel's Edge Runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Used Prisma's WASM-based edge client (&lt;code&gt;generated/prisma/edge.js&lt;/code&gt;) with the &lt;code&gt;wasm-compiler-edge.js&lt;/code&gt; adapter. This required careful configuration in &lt;code&gt;prisma.config.ts&lt;/code&gt; and &lt;code&gt;next.config.ts&lt;/code&gt; to bundle correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Course Carousel → Bot Agent Wiring
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; When a user clicked a course card in the carousel, the click event needed to trigger a bot message as if the user had typed the course name — but the component boundaries between &lt;code&gt;CourseCarousel.tsx&lt;/code&gt; and &lt;code&gt;useChat.ts&lt;/code&gt; made this tricky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Exposed a &lt;code&gt;sendMessage()&lt;/code&gt; method from &lt;code&gt;useChat()&lt;/code&gt; and threaded it down as a prop to &lt;code&gt;CourseCarousel&lt;/code&gt;. Carousel card clicks call &lt;code&gt;sendMessage(course.name)&lt;/code&gt; directly, entering the course into the conversation and triggering the full bot response pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Suggestion Buttons During Lead Capture
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; During lead capture (collecting name, city, etc.), users would go off-script. Raw free text answers led to extraction failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;code&gt;getSuggestionsForNextField()&lt;/code&gt; in the chatbot route generates contextual quick-reply buttons for each lead field. If collecting city, the suggestions show major Indian cities. If collecting course interest, they show relevant domains. Users can tap or type — both paths work.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. WhatsApp Webhook Signature Validation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Public webhooks are targets for spoofed requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;code&gt;isValidSignature()&lt;/code&gt; validates HMAC-SHA256 signatures on every incoming WhatsApp webhook payload using the DoubleTick shared secret. Invalid signatures are rejected with &lt;code&gt;403&lt;/code&gt; before any processing occurs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure Overview (Though it's a private repository)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ifda/
├── app/
│   ├── page.tsx                    # Public landing + chat widget
│   ├── layout.tsx                  # Root layout
│   ├── api/
│   │   ├── chatbot/route.ts        # Core bot API
│   │   ├── whatsapp/
│   │   │   ├── webhook/route.ts    # Incoming WhatsApp
│   │   │   └── send/route.ts       # Outbound WhatsApp
│   │   ├── schedule/route.ts       # Appointment booking
│   │   ├── faq/route.ts
│   │   ├── embed/route.ts
│   │   └── admin/                  # Protected admin APIs
│   ├── chatbot/
│   │   ├── components/
│   │   │   ├── ChatWindow.tsx
│   │   │   ├── InputBox.tsx
│   │   │   ├── CourseCaraousel.tsx
│   │   │   └── StructureMessage.tsx
│   │   └── hooks/useChat.ts
│   └── admin/                      # Admin CRM pages
│       ├── login/
│       ├── dashboard/
│       ├── conversations/
│       ├── knowledge/
│       ├── templates/
│       └── profile/
├── lib/
│   ├── ai/
│   │   ├── courseData.ts           # Course intelligence
│   │   ├── embeddings.ts           # Vector search
│   │   ├── intent.ts               # Intent + lead extraction
│   │   ├── knowledge-parser.ts     # Multi-format doc parsing
│   │   └── llm.ts                  # LLM client + block builders
│   ├── auth/
│   │   ├── jwt.ts
│   │   ├── middleware.ts
│   │   └── permissions.ts          # RBAC
│   ├── bot/
│   │   ├── engine.ts               # Core processMessage()
│   │   └── renderers/
│   │       ├── web.ts
│   │       └── whatsapp.ts
│   ├── crm/leads.ts
│   ├── db/
│   │   ├── prisma.ts
│   │   └── queries.ts
│   ├── scheduler/calendar.ts
│   ├── session/store.ts
│   └── whatsapp/
│       ├── doubletick.ts
│       ├── messageParser.ts
│       └── types.ts
├── config/
│   ├── constants.ts
│   └── prompts.ts                  # All LLM system prompts
├── types/
│   ├── chat.ts
│   ├── lead.ts
│   └── analytics.ts
└── prisma/
    ├── schema.prisma
    └── scripts/                    # Seed + test scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge-first:&lt;/strong&gt; All API routes are Edge-compatible. Vercel deploys them to the closest edge node, cutting latency for Indian users significantly vs. a single-region serverless function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session caching:&lt;/strong&gt; &lt;code&gt;getSession()&lt;/code&gt; uses a lightweight in-memory cache layer before hitting PostgreSQL, reducing DB round-trips on sequential messages in an active conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM cost control:&lt;/strong&gt; The scripted funnel handles the majority of conversations. GPT is only invoked when the intent engine genuinely can't determine the next step — keeping API costs predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding on ingest:&lt;/strong&gt; Knowledge files are embedded at upload time, not at query time. Search is a fast vector similarity lookup, not a synchronous embedding + search chain.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Extract the bot engine into a proper state machine.&lt;/strong&gt; &lt;code&gt;processMessage()&lt;/code&gt; is powerful but has grown organically. Formalizing it as an explicit state machine (with states like &lt;code&gt;COLLECTING_NAME&lt;/code&gt;, &lt;code&gt;COLLECTING_CITY&lt;/code&gt;, &lt;code&gt;COURSE_DISCOVERY&lt;/code&gt;, &lt;code&gt;SCHEDULING&lt;/code&gt;) would make the flow easier to reason about and test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Add streaming responses.&lt;/strong&gt; The current implementation waits for the complete LLM response before sending. Server-Sent Events or the Vercel AI SDK's streaming primitives would make the bot feel dramatically faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Separate the WhatsApp and web sessions more cleanly.&lt;/strong&gt; Right now, both channels share the same session store schema. As WhatsApp-specific flows diverge (templates, interactive lists, 24-hour messaging windows), having a channel-aware session type would reduce the number of &lt;code&gt;if (channel === 'whatsapp')&lt;/code&gt; checks in the engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Add a proper evaluation pipeline.&lt;/strong&gt; Testing chatbot quality is hard. A golden-dataset evaluation pipeline — where known inputs are run through the bot and outputs are checked against expected responses — would catch regressions when the LLM or prompt changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The bot handles the full admissions journey end-to-end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Course discovery&lt;/strong&gt; — carousel-driven browsing + natural language course queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead qualification&lt;/strong&gt; — multi-turn lead capture with field-level validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge search&lt;/strong&gt; — semantic search over admin-uploaded documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appointment booking&lt;/strong&gt; — slot selection and WhatsApp confirmation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staff handoff&lt;/strong&gt; — completed leads surfaced in the admin CRM with full conversation history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast campaigns&lt;/strong&gt; — AI-assisted WhatsApp template creation and dispatch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The admin team went from managing a static form to having a live CRM where every lead arrives pre-qualified with course interest, location, and contact details captured by the bot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 14&lt;/strong&gt; (App Router, Edge Runtime)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; (strict mode throughout)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma ORM&lt;/strong&gt; + &lt;strong&gt;Neon PostgreSQL&lt;/strong&gt; (WASM edge client)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI API&lt;/strong&gt; (GPT-4o for intent/generation, text-embedding-3-small for semantic search)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DoubleTick&lt;/strong&gt; (WhatsApp Business API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; (deployment, edge functions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT&lt;/strong&gt; (stateless admin auth)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; (admin UI and chat widget)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The most valuable architecture decision was the &lt;strong&gt;block-based message system&lt;/strong&gt;. By never treating bot responses as raw strings, every part of the system — the engine, the renderers, the frontend components — operates on typed, predictable data structures. Adding a new message type (say, a booking confirmation card) means adding one block builder in &lt;code&gt;llm.ts&lt;/code&gt;, one render case in each renderer, and one React component. The engine doesn't change.&lt;/p&gt;

&lt;p&gt;The second best decision was &lt;strong&gt;keeping the LLM in the hot path as little as possible&lt;/strong&gt;. GPT is powerful but introduces latency and cost. Using it for intent classification and open-ended generation, while handling the structured lead capture funnel with deterministic code, gives the best of both worlds — intelligent conversation where it matters, reliability and speed where it counts.&lt;/p&gt;

&lt;p&gt;If you're building something similar or have questions about any of the systems described here, drop them in the comments. Happy to go deeper on any piece of this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for IFDA — Indian design and technology education institute.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>webdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
