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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user