feat: add internal AI chat assistant and AI Input Agent page

- Add Gemini-powered internal staff chatbot (free-text input in the
  upper-right bot panel): type "check MARIA GONZALES" to search patient
  and pre-fill eligibility, or "open claims" to navigate directly
- Add /api/ai/internal-chat endpoint with LangGraph + Google Gemini
  classifier (intent: check_eligibility, find_patient, navigate_*)
- Add Users AI Chat settings section in Settings > Advanced > AI Chat
  to configure a custom system prompt for the internal assistant
- Store internal chat system prompt in existing twilioSettings JSON
  blob (no DB migration needed)
- Add AI Input Agent sidebar entry and placeholder page describing
  planned keyboard-automation typing into Open Dental / Eaglesoft

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-02 00:44:05 -04:00
parent 3ac185b0ec
commit be90966f6e
8 changed files with 707 additions and 101 deletions

View File

@@ -29,6 +29,7 @@ const ReportsPage = lazy(() => import("./pages/reports-page"));
const CloudStoragePage = lazy(() => import("./pages/cloud-storage-page"));
const JobMonitorPage = lazy(() => import("./pages/job-monitor-page"));
const ChartPage = lazy(() => import("./pages/chart-page"));
const AiInputAgentPage = lazy(() => import("./pages/ai-input-agent-page"));
const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page"));
const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page"));
const ActivationPage = lazy(() => import("./pages/activation-page"));
@@ -64,6 +65,7 @@ function Router() {
/>
<ProtectedRoute path="/reports" component={() => <ReportsPage />} />
<ProtectedRoute path="/cloud-storage" component={() => <CloudStoragePage />} />
<ProtectedRoute path="/ai-input-agent" component={() => <AiInputAgentPage />} />
<ProtectedRoute path="/dental-shopping/search-tag" component={() => <DentalShoppingSearchTagPage />} />
<ProtectedRoute path="/dental-shopping/login-info" component={() => <DentalShoppingLoginInfoPage />} />
<ProtectedRoute path="/activation" component={() => <ActivationPage />} adminOnly />

View File

@@ -7,29 +7,47 @@ import {
Calendar,
FileText,
MessageSquare,
Send,
Loader2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useLocation } from "wouter";
import { cn } from "@/lib/utils";
import { apiRequest } from "@/lib/queryClient";
type Step = "menu" | "eligibility-input" | "eligibility-confirm";
type Step =
| "menu"
| "eligibility-input"
| "eligibility-confirm"
| "ai-loading"
| "patient-found";
interface Message {
id: number;
role: "bot" | "user";
text: string;
isLoading?: boolean;
}
interface PatientResult {
id: number;
firstName: string;
lastName: string;
insuranceId: string | null;
insuranceProvider: string | null;
dateOfBirth: string | null;
}
interface EligibilityData {
memberId: string;
dob: string; // MM/DD/YYYY display format
dobISO: string; // YYYY-MM-DD for storage
dob: string;
dobISO: string;
}
let msgCounter = 0;
function makeMsg(role: "bot" | "user", text: string): Message {
return { id: ++msgCounter, role, text };
function makeMsg(role: "bot" | "user", text: string, isLoading = false): Message {
return { id: ++msgCounter, role, text, isLoading };
}
function getAutoCheck(dobISO: string): "mh" | "cmsp" {
@@ -44,7 +62,6 @@ function getAutoCheck(dobISO: string): "mh" | "cmsp" {
function parseEligibilityInput(
raw: string
): { memberId: string; display: string; iso: string } | null {
// Find a date anywhere in the text (MM/DD/YYYY or MM-DD-YYYY)
const dateMatch = raw.match(/\b(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})\b/);
if (!dateMatch) return null;
const m = dateMatch[1];
@@ -56,7 +73,6 @@ function parseEligibilityInput(
const year = parseInt(y, 10);
if (month < 1 || month > 12 || day < 1 || day > 31 || year < 1900) return null;
// Remove the matched date, then collect remaining alphanumeric chars as member ID
const withoutDate = raw.replace(dateMatch[0], "");
const memberId = (withoutDate.match(/[a-zA-Z0-9]/g) ?? []).join("");
if (!memberId) return null;
@@ -78,11 +94,13 @@ export function ChatbotButton() {
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
const [pasteInput, setPasteInput] = useState("");
const [parseError, setParseError] = useState("");
const [eligibilityData, setEligibilityData] =
useState<EligibilityData | null>(null);
const [eligibilityData, setEligibilityData] = useState<EligibilityData | null>(null);
const [freeTextInput, setFreeTextInput] = useState("");
const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
const [, setLocation] = useLocation();
const messagesEndRef = useRef<HTMLDivElement>(null);
const pasteRef = useRef<HTMLTextAreaElement>(null);
const freeTextRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -92,10 +110,21 @@ export function ChatbotButton() {
if (step === "eligibility-input") {
setTimeout(() => pasteRef.current?.focus(), 50);
}
if (step === "menu") {
setTimeout(() => freeTextRef.current?.focus(), 50);
}
}, [step]);
const addMsg = (role: "bot" | "user", text: string) =>
setMessages((prev) => [...prev, makeMsg(role, text)]);
const addMsg = (role: "bot" | "user", text: string, isLoading = false) =>
setMessages((prev) => [...prev, makeMsg(role, text, isLoading)]);
const replaceLastMsg = (text: string) =>
setMessages((prev) => {
const next = [...prev];
const last = next[next.length - 1];
if (last) next[next.length - 1] = { ...last, text, isLoading: false };
return next;
});
const reset = () => {
setStep("menu");
@@ -103,6 +132,8 @@ export function ChatbotButton() {
setPasteInput("");
setParseError("");
setEligibilityData(null);
setFreeTextInput("");
setPatientResult(null);
};
const handleClose = () => {
@@ -110,31 +141,15 @@ export function ChatbotButton() {
reset();
};
const handleOptionSelect = (
option: "eligibility" | "schedule" | "claims" | "chat"
) => {
const handleOptionSelect = (option: "eligibility" | "schedule" | "claims") => {
if (option === "schedule") {
addMsg("user", "Schedule an appointment");
addMsg("bot", "Opening the appointments page...");
setTimeout(() => {
setLocation("/appointments");
setOpen(false);
reset();
}, 600);
setTimeout(() => { setLocation("/appointments"); setOpen(false); reset(); }, 600);
} else if (option === "claims") {
addMsg("user", "View claims");
addMsg("bot", "Opening the claims page...");
setTimeout(() => {
setLocation("/claims");
setOpen(false);
reset();
}, 600);
} else if (option === "chat") {
addMsg("user", "General chat");
addMsg(
"bot",
"This feature is coming soon! For now, use the quick options below to navigate."
);
setTimeout(() => { setLocation("/claims"); setOpen(false); reset(); }, 600);
} else if (option === "eligibility") {
addMsg("user", "Check Eligibility");
addMsg("bot", "Please enter the patient's Member ID and Date of Birth:");
@@ -145,25 +160,14 @@ export function ChatbotButton() {
const handleEligibilitySubmit = () => {
const parsed = parseEligibilityInput(pasteInput);
if (!parsed) {
setParseError(
"Couldn't find both a Member ID and a date. Try: 123456789 01/15/1985"
);
setParseError("Couldn't find both a Member ID and a date. Try: 123456789 01/15/1985");
return;
}
setParseError("");
const data: EligibilityData = {
memberId: parsed.memberId,
dob: parsed.display,
dobISO: parsed.iso,
};
const data: EligibilityData = { memberId: parsed.memberId, dob: parsed.display, dobISO: parsed.iso };
setEligibilityData(data);
addMsg("user", pasteInput.trim());
addMsg(
"bot",
`Ready to check MassHealth eligibility for:\n• Member ID: ${parsed.memberId}\n• Date of Birth: ${parsed.display}\n\nShall I proceed?`
);
addMsg("bot", `Ready to check MassHealth eligibility for:\n• Member ID: ${parsed.memberId}\n• Date of Birth: ${parsed.display}\n\nShall I proceed?`);
setStep("eligibility-confirm");
};
@@ -171,22 +175,74 @@ export function ChatbotButton() {
if (!eligibilityData) return;
addMsg("user", "Yes, check now");
addMsg("bot", "Opening the eligibility check page with this patient...");
sessionStorage.setItem(
"chatbot_eligibility",
JSON.stringify({
memberId: eligibilityData.memberId,
dob: eligibilityData.dobISO,
autoCheck: getAutoCheck(eligibilityData.dobISO),
})
);
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
memberId: eligibilityData.memberId,
dob: eligibilityData.dobISO,
autoCheck: getAutoCheck(eligibilityData.dobISO),
}));
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
setTimeout(() => {
setLocation("/insurance-status");
setOpen(false);
reset();
}, 600);
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
};
const handleEligibilityFromPatient = () => {
if (!patientResult) return;
addMsg("user", "Check eligibility now");
addMsg("bot", "Opening the eligibility check page...");
if (patientResult.insuranceId && patientResult.dateOfBirth) {
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
memberId: patientResult.insuranceId,
dob: patientResult.dateOfBirth,
autoCheck: getAutoCheck(patientResult.dateOfBirth),
}));
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
}
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
};
const handleFreeTextSubmit = async () => {
const text = freeTextInput.trim();
if (!text || step === "ai-loading") return;
setFreeTextInput("");
addMsg("user", text);
addMsg("bot", "Thinking...", true);
setStep("ai-loading");
try {
const res = await apiRequest("POST", "/api/ai/internal-chat", { message: text });
const data = await res.json();
replaceLastMsg(data.reply ?? "Sorry, I couldn't process that.");
if (data.action === "navigate" && data.actionData?.url) {
setTimeout(() => { setLocation(data.actionData.url); setOpen(false); reset(); }, 800);
return;
}
if (
(data.action === "check_eligibility_prefill" || data.action === "show_patient") &&
data.actionData?.patient
) {
setPatientResult(data.actionData.patient);
setStep("patient-found");
return;
}
setStep("menu");
} catch {
replaceLastMsg("Sorry, something went wrong. Please try again.");
setStep("menu");
}
};
const handleFreeTextKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleFreeTextSubmit();
}
};
const showFreeTextInput = step === "menu" || step === "ai-loading";
return (
<>
<Button
@@ -202,11 +258,7 @@ export function ChatbotButton() {
{open && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={handleClose}
aria-hidden
/>
<div className="fixed inset-0 z-40" onClick={handleClose} aria-hidden />
{/* Chat panel */}
<div className="fixed top-16 right-0 z-50 w-80 h-[calc(100vh-4rem)] bg-white border-l shadow-2xl flex flex-col">
@@ -225,15 +277,12 @@ export function ChatbotButton() {
</button>
</div>
{/* Messages + controls */}
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.map((msg) => (
<div
key={msg.id}
className={cn(
"flex",
msg.role === "user" ? "justify-end" : "justify-start"
)}
className={cn("flex", msg.role === "user" ? "justify-end" : "justify-start")}
>
<div
className={cn(
@@ -243,12 +292,19 @@ export function ChatbotButton() {
: "bg-gray-100 text-gray-800 rounded-tl-sm"
)}
>
{msg.text}
{msg.isLoading ? (
<span className="flex items-center gap-1.5">
<Loader2 className="h-3 w-3 animate-spin" />
Thinking...
</span>
) : (
msg.text
)}
</div>
</div>
))}
{/* Step: menu */}
{/* Quick-action buttons (menu step) */}
{step === "menu" && (
<div className="grid grid-cols-2 gap-2 pt-1">
<button
@@ -267,44 +323,30 @@ export function ChatbotButton() {
</button>
<button
onClick={() => handleOptionSelect("claims")}
className="flex items-center gap-2 p-3 rounded-xl border border-orange-300 hover:bg-orange-50 text-sm text-left transition-colors"
className="flex items-center gap-2 p-3 rounded-xl border border-orange-300 hover:bg-orange-50 text-sm text-left transition-colors col-span-2"
>
<FileText className="h-4 w-4 text-orange-500 shrink-0" />
<span>Claims</span>
</button>
<button
onClick={() => handleOptionSelect("chat")}
className="flex items-center gap-2 p-3 rounded-xl border border-gray-300 hover:bg-gray-50 text-sm text-left transition-colors"
>
<MessageSquare className="h-4 w-4 text-gray-500 shrink-0" />
<span>Chat</span>
</button>
</div>
)}
{/* Step: eligibility input */}
{/* Eligibility manual input */}
{step === "eligibility-input" && (
<div className="space-y-2 bg-gray-50 rounded-xl p-3 border">
<Label className="text-xs text-gray-600">
Paste Member ID and Date of Birth
</Label>
<Label className="text-xs text-gray-600">Paste Member ID and Date of Birth</Label>
<textarea
ref={pasteRef}
rows={3}
placeholder={"e.g. 123456789 01/15/1985\nor 01/15/1985 123456789"}
value={pasteInput}
onChange={(e) => {
setPasteInput(e.target.value);
setParseError("");
}}
onChange={(e) => { setPasteInput(e.target.value); setParseError(""); }}
className={cn(
"w-full rounded-md border bg-white px-3 py-2 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring",
parseError && "border-red-400"
)}
/>
{parseError && (
<p className="text-xs text-red-500">{parseError}</p>
)}
{parseError && <p className="text-xs text-red-500">{parseError}</p>}
<div className="flex gap-2">
<Button
size="sm"
@@ -314,26 +356,17 @@ export function ChatbotButton() {
>
Continue <ChevronRight className="h-3 w-3 ml-1" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 text-xs"
onClick={reset}
>
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}>
Cancel
</Button>
</div>
</div>
)}
{/* Step: confirm */}
{/* Eligibility confirm */}
{step === "eligibility-confirm" && (
<div className="flex gap-2">
<Button
size="sm"
className="flex-1 h-8 text-xs"
onClick={handleConfirm}
>
<Button size="sm" className="flex-1 h-8 text-xs" onClick={handleConfirm}>
Yes, Check Now
</Button>
<Button
@@ -350,8 +383,72 @@ export function ChatbotButton() {
</div>
)}
{/* AI-found patient card */}
{step === "patient-found" && patientResult && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 space-y-2">
<p className="text-xs font-semibold text-blue-800">
{patientResult.firstName} {patientResult.lastName}
</p>
{patientResult.insuranceProvider && (
<p className="text-xs text-blue-600">{patientResult.insuranceProvider}</p>
)}
{patientResult.insuranceId && (
<p className="text-xs text-gray-500">ID: {patientResult.insuranceId}</p>
)}
{patientResult.dateOfBirth && (
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
)}
<div className="flex gap-2 pt-1">
{patientResult.insuranceId && (
<Button
size="sm"
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90"
onClick={handleEligibilityFromPatient}
>
<Stethoscope className="h-3 w-3 mr-1" />
Check Eligibility
</Button>
)}
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}>
Done
</Button>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Persistent free-text input */}
{showFreeTextInput && (
<div className="shrink-0 border-t bg-white px-3 py-2">
<div className="flex items-end gap-2">
<textarea
ref={freeTextRef}
rows={2}
placeholder='e.g. "check MARIA GONZALES" or "open claims"'
value={freeTextInput}
onChange={(e) => setFreeTextInput(e.target.value)}
onKeyDown={handleFreeTextKeyDown}
disabled={step === "ai-loading"}
className="flex-1 resize-none rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
/>
<Button
size="sm"
className="h-9 w-9 p-0 shrink-0"
onClick={handleFreeTextSubmit}
disabled={!freeTextInput.trim() || step === "ai-loading"}
>
{step === "ai-loading" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-[10px] text-gray-400 mt-1 pl-1">Enter to send · Shift+Enter for new line</p>
</div>
)}
</div>
</>
)}

View File

@@ -169,6 +169,11 @@ export function Sidebar() {
path: "/cloud-storage",
icon: <Cloud className="h-5 w-5 text-sky-500" />,
},
{
name: "AI Input Agent",
path: "/ai-input-agent",
icon: <Bot className="h-5 w-5 text-violet-500" />,
},
{
name: "AI Dental Shopping",
path: "/dental-shopping",

View File

@@ -6,7 +6,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap } from "lucide-react";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap, SlidersHorizontal } from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -609,6 +609,133 @@ function NewPatientFlow() {
);
}
// ─── Users AI Chat (internal staff assistant) config ─────────────────────────
const INTERNAL_CHAT_PLACEHOLDER =
`Examples of custom context you can add:
• "This office primarily treats MassHealth patients — always prefer MassHealth eligibility checks."
• "When a patient name is typed, also show their upcoming appointment if available."
• "Default provider is Mary Scannell."`;
function InternalChatSettingsCard() {
const { toast } = useToast();
const [systemPrompt, setSystemPrompt] = useState("");
const initialized = useRef(false);
const { data, isLoading } = useQuery<{ systemPrompt: string }>({
queryKey: ["/api/ai/internal-chat-settings"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/ai/internal-chat-settings");
if (!res.ok) return { systemPrompt: "" };
return res.json();
},
staleTime: Infinity,
refetchOnWindowFocus: false,
});
useEffect(() => {
if (data && !initialized.current) {
initialized.current = true;
setSystemPrompt(data.systemPrompt ?? "");
}
}, [data]);
const saveMutation = useMutation({
mutationFn: async (prompt: string) => {
const res = await apiRequest("PUT", "/api/ai/internal-chat-settings", { systemPrompt: prompt });
if (!res.ok) throw new Error("Failed to save");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/ai/internal-chat-settings"] });
toast({ title: "Users AI Chat settings saved" });
},
onError: () => {
toast({ title: "Error", description: "Failed to save settings.", variant: "destructive" });
},
});
return (
<Card>
<CardContent className="py-6 space-y-5">
<div className="flex items-center gap-2">
<SlidersHorizontal className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Users AI Chat</h3>
</div>
<p className="text-sm text-muted-foreground">
Configure the AI assistant available to staff via the{" "}
<span className="font-medium text-foreground">bot icon</span> in the top-right corner.
Staff can type natural language commands such as{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">"check MARIA GONZALES"</code>{" "}
to search for a patient and pre-fill their eligibility check, or{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">"open claims"</code>{" "}
to navigate directly to a page.
</p>
{/* Capability summary */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
{[
{ icon: "🔍", label: "Patient search", desc: 'e.g. "find GONZALES"' },
{ icon: "🏥", label: "Eligibility prefill", desc: 'e.g. "check MARIA DE LA CRUZ"' },
{ icon: "🗺️", label: "Navigation", desc: 'e.g. "open claims", "schedule"' },
].map((c) => (
<div key={c.label} className="flex items-start gap-2 rounded-lg border bg-muted/30 p-3">
<span className="text-base leading-none">{c.icon}</span>
<div>
<p className="font-medium text-foreground">{c.label}</p>
<p className="text-muted-foreground mt-0.5">{c.desc}</p>
</div>
</div>
))}
</div>
{/* Additional context / system prompt */}
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<Bot className="h-4 w-4 text-primary" />
<span className="text-sm font-medium">Additional AI Context</span>
</div>
<p className="text-xs text-muted-foreground">
Optional instructions injected into the AI's system prompt. Use this to describe your
practice's preferences, common shortcuts, or patient demographics.
</p>
{isLoading ? (
<p className="text-sm text-muted-foreground">Loading...</p>
) : (
<Textarea
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
placeholder={INTERNAL_CHAT_PLACEHOLDER}
rows={5}
className="text-sm resize-none"
/>
)}
</div>
<div className="flex items-center gap-3 pt-1">
<Button
disabled={saveMutation.isPending || isLoading}
className="bg-teal-600 hover:bg-teal-700 text-white"
onClick={() => saveMutation.mutate(systemPrompt)}
>
{saveMutation.isPending ? "Saving..." : "Save"}
</Button>
{systemPrompt && (
<Button
variant="ghost"
className="text-xs text-muted-foreground"
onClick={() => setSystemPrompt("")}
>
Clear
</Button>
)}
</div>
</CardContent>
</Card>
);
}
// ─── Main component ───────────────────────────────────────────────────────────
export function AiChatSettingsCard() {
@@ -1068,6 +1195,9 @@ export function AiChatSettingsCard() {
</CardContent>
</Card>
{/* ── Section 3: Users AI Chat ─────────────────────────────── */}
<InternalChatSettingsCard />
</div>
);
}

View File

@@ -0,0 +1,137 @@
import { Bot, Keyboard, FileCheck, CreditCard, Shield, Zap, ArrowRight } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
const SOFTWARE_TARGETS = [
{ name: "Open Dental", color: "bg-blue-100 text-blue-700 border-blue-200" },
{ name: "Eaglesoft", color: "bg-emerald-100 text-emerald-700 border-emerald-200" },
{ name: "Dentrix", color: "bg-violet-100 text-violet-700 border-violet-200" },
{ name: "Curve Dental", color: "bg-orange-100 text-orange-700 border-orange-200" },
];
const CAPABILITIES = [
{
icon: <Shield className="h-5 w-5 text-teal-600" />,
title: "Eligibility Results",
description:
"Automatically type insurance eligibility information — coverage status, plan details, deductibles, and co-pays — directly into the patient chart in your dental software.",
},
{
icon: <FileCheck className="h-5 w-5 text-blue-600" />,
title: "Claim Information",
description:
"Transfer claim numbers, claim status updates, and denial reasons from the insurance portal results into your claims module without manual re-entry.",
},
{
icon: <CreditCard className="h-5 w-5 text-indigo-600" />,
title: "Insurance Payments",
description:
"Post ERA / EOB payment details — amounts, adjustments, patient responsibility — directly into your payment ledger by typing them into the active software window.",
},
];
export default function AiInputAgentPage() {
return (
<div className="max-w-3xl mx-auto px-4 py-10 space-y-10">
{/* Hero */}
<div className="text-center space-y-3">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-violet-100 mb-2">
<Bot className="h-8 w-8 text-violet-600" />
</div>
<h1 className="text-3xl font-bold tracking-tight">AI Input Agent</h1>
<p className="text-muted-foreground text-base max-w-xl mx-auto">
An AI-powered agent that reads data from this app and types it directly into your
current dental management software no copy-paste, no manual re-entry.
</p>
<span className="inline-block bg-amber-100 text-amber-700 text-xs font-semibold px-3 py-1 rounded-full border border-amber-200">
Coming Soon
</span>
</div>
{/* How it works */}
<Card>
<CardContent className="py-6 space-y-4">
<div className="flex items-center gap-2">
<Keyboard className="h-5 w-5 text-violet-500" />
<h2 className="text-base font-semibold">How it works</h2>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 text-sm">
{[
"Retrieve data in this app\n(eligibility, claim, payment)",
"Agent identifies the active\nfield in your dental software",
"Types the value directly\nvia keyboard automation",
].map((step, i) => (
<div key={i} className="flex items-center gap-3 flex-1">
<div className="flex-shrink-0 w-7 h-7 rounded-full bg-violet-100 text-violet-700 text-xs font-bold flex items-center justify-center">
{i + 1}
</div>
<p className="whitespace-pre-line text-muted-foreground leading-snug">{step}</p>
{i < 2 && <ArrowRight className="h-4 w-4 text-muted-foreground/40 hidden sm:block flex-shrink-0" />}
</div>
))}
</div>
<p className="text-xs text-muted-foreground pt-1 border-t">
The agent runs as a lightweight local process on your workstation. It receives
instructions from this app and uses keyboard automation to type data into whatever
window is currently focused in your dental software no clipboard involved.
</p>
</CardContent>
</Card>
{/* What it will type */}
<div className="space-y-3">
<h2 className="text-base font-semibold">What it will type</h2>
<div className="grid gap-4 sm:grid-cols-3">
{CAPABILITIES.map((cap) => (
<Card key={cap.title}>
<CardContent className="py-5 space-y-2">
<div className="flex items-center gap-2">
{cap.icon}
<span className="text-sm font-medium">{cap.title}</span>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
{cap.description}
</p>
</CardContent>
</Card>
))}
</div>
</div>
{/* Supported software */}
<div className="space-y-3">
<h2 className="text-base font-semibold">Planned software support</h2>
<div className="flex flex-wrap gap-2">
{SOFTWARE_TARGETS.map((s) => (
<span
key={s.name}
className={`text-sm font-medium px-3 py-1.5 rounded-lg border ${s.color}`}
>
{s.name}
</span>
))}
</div>
<p className="text-xs text-muted-foreground">
Each software has its own field layout. The agent will include pre-built templates
for common workflows in each platform.
</p>
</div>
{/* Why */}
<Card className="border-violet-200 bg-violet-50/50">
<CardContent className="py-5 flex items-start gap-3">
<Zap className="h-5 w-5 text-violet-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm font-medium text-violet-900">Why this matters</p>
<p className="text-sm text-violet-700 leading-relaxed">
Staff currently check eligibility or look up a claim here, then manually retype
every value into Open Dental or Eaglesoft. This agent eliminates that step entirely
one click sends the data straight into the right field.
</p>
</div>
</CardContent>
</Card>
</div>
);
}