From c3e4f3a179f16e7da8c91e2df4828f7e167d5760 Mon Sep 17 00:00:00 2001 From: SandipM03 Date: Sun, 1 Mar 2026 16:32:38 +0530 Subject: [PATCH 1/4] feat: Add medical-assistant AgentKit --- kits/assistant/medical-assistant/.env.example | 4 + kits/assistant/medical-assistant/.gitignore | 28 + kits/assistant/medical-assistant/README.md | 129 + .../medical-assistant/actions/orchestrate.ts | 111 + .../medical-assistant/app/globals.css | 189 + .../medical-assistant/app/layout.tsx | 44 + kits/assistant/medical-assistant/app/page.tsx | 518 +++ .../medical-assistant/components.json | 21 + .../components/disclaimer.tsx | 22 + kits/assistant/medical-assistant/config.json | 22 + .../flows/medical-assistant-chat/README.md | 39 + .../flows/medical-assistant-chat/config.json | 38 + .../flows/medical-assistant-chat/inputs.json | 26 + .../flows/medical-assistant-chat/meta.json | 11 + .../medical-assistant/lib/lamatic-client.ts | 20 + kits/assistant/medical-assistant/lib/utils.ts | 6 + .../medical-assistant/next.config.mjs | 11 + .../medical-assistant/orchestrate.js | 24 + .../medical-assistant/package-lock.json | 3341 +++++++++++++++++ kits/assistant/medical-assistant/package.json | 38 + .../medical-assistant/postcss.config.cjs | 5 + .../medical-assistant/tailwind.config.ts | 15 + .../assistant/medical-assistant/tsconfig.json | 40 + 23 files changed, 4702 insertions(+) create mode 100644 kits/assistant/medical-assistant/.env.example create mode 100644 kits/assistant/medical-assistant/.gitignore create mode 100644 kits/assistant/medical-assistant/README.md create mode 100644 kits/assistant/medical-assistant/actions/orchestrate.ts create mode 100644 kits/assistant/medical-assistant/app/globals.css create mode 100644 kits/assistant/medical-assistant/app/layout.tsx create mode 100644 kits/assistant/medical-assistant/app/page.tsx create mode 100644 kits/assistant/medical-assistant/components.json create mode 100644 kits/assistant/medical-assistant/components/disclaimer.tsx create mode 100644 kits/assistant/medical-assistant/config.json create mode 100644 kits/assistant/medical-assistant/flows/medical-assistant-chat/README.md create mode 100644 kits/assistant/medical-assistant/flows/medical-assistant-chat/config.json create mode 100644 kits/assistant/medical-assistant/flows/medical-assistant-chat/inputs.json create mode 100644 kits/assistant/medical-assistant/flows/medical-assistant-chat/meta.json create mode 100644 kits/assistant/medical-assistant/lib/lamatic-client.ts create mode 100644 kits/assistant/medical-assistant/lib/utils.ts create mode 100644 kits/assistant/medical-assistant/next.config.mjs create mode 100644 kits/assistant/medical-assistant/orchestrate.js create mode 100644 kits/assistant/medical-assistant/package-lock.json create mode 100644 kits/assistant/medical-assistant/package.json create mode 100644 kits/assistant/medical-assistant/postcss.config.cjs create mode 100644 kits/assistant/medical-assistant/tailwind.config.ts create mode 100644 kits/assistant/medical-assistant/tsconfig.json diff --git a/kits/assistant/medical-assistant/.env.example b/kits/assistant/medical-assistant/.env.example new file mode 100644 index 00000000..13e38de8 --- /dev/null +++ b/kits/assistant/medical-assistant/.env.example @@ -0,0 +1,4 @@ +MEDICAL_ASSISTANT_CHAT = "MEDICAL_ASSISTANT_CHAT Flow ID" +LAMATIC_API_URL = "LAMATIC_API_URL" +LAMATIC_PROJECT_ID = "LAMATIC_PROJECT_ID" +LAMATIC_API_KEY = "LAMATIC_API_KEY" diff --git a/kits/assistant/medical-assistant/.gitignore b/kits/assistant/medical-assistant/.gitignore new file mode 100644 index 00000000..a34fbefe --- /dev/null +++ b/kits/assistant/medical-assistant/.gitignore @@ -0,0 +1,28 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/kits/assistant/medical-assistant/README.md b/kits/assistant/medical-assistant/README.md new file mode 100644 index 00000000..ef698d9f --- /dev/null +++ b/kits/assistant/medical-assistant/README.md @@ -0,0 +1,129 @@ +# Medical Assistant by Lamatic.ai + +

+ + Live Demo + +

+ +**Medical Assistant** is an AI-powered chatbot built with [Lamatic.ai](https://lamatic.ai) that provides general medical information, symptom checks, and health guidance through a conversational interface. It uses intelligent workflows to process medical queries and return evidence-based information with markdown rendering. + +> ⚠️ **Disclaimer:** This tool provides general medical information only. It is NOT a substitute for professional medical advice, diagnosis, or treatment. Always seek the advice of a qualified healthcare provider. + +--- + +## Lamatic Setup (Pre and Post) + +Before running this project, you must build and deploy the flow in Lamatic, then wire its config into this codebase. + +### Pre: Build in Lamatic + +1. Sign in or sign up at https://lamatic.ai +2. Create a project (if you don't have one yet) +3. Click "+ New Flow" and build a medical assistant flow: + - Add an **API Trigger** with a `query` input (string) + - Add an **LLM Node** with a medical-aware system prompt + - Configure the LLM to never diagnose, always recommend professional consultation +4. Deploy the flow in Lamatic and obtain your .env keys +5. Copy the Flow ID and API credentials from your studio + +### Post: Wire into this repo + +1. Create a `.env` file and set the keys +2. Install and run locally: + - `npm install` + - `npm run dev` +3. Deploy (Vercel recommended): + - Import your repo, set the project's Root Directory to `kits/assistant/medical-assistant` + - Add env vars in Vercel (same as your `.env`) + - Deploy and test your live URL + +--- + +## 🔑 Setup + +### Required Keys and Config + +You'll need these to run this project locally: + +| Item | Purpose | Where to Get It | +| ----------- | ---------------------------------------------------- | -------------------------------- | +| `.env` Keys | Authentication for Lamatic AI APIs and Orchestration | [lamatic.ai](https://lamatic.ai) | + +### 1. Environment Variables + +Create `.env` with: + +```bash +# Lamatic +MEDICAL_ASSISTANT_CHAT=your_flow_id_here +LAMATIC_API_URL=https://api.lamatic.ai +LAMATIC_PROJECT_ID=your_project_id_here +LAMATIC_API_KEY=your_api_key_here +``` + +### 2. Install & Run + +```bash +npm install +npm run dev +# Open http://localhost:3000 +``` + +--- + +## 📂 Repo Structure + +``` +/actions + └── orchestrate.ts # Lamatic workflow orchestration for medical queries +/app + ├── globals.css # Teal-themed design system + ├── layout.tsx # Root layout with SEO metadata + └── page.tsx # Chat-style medical assistant UI +/components + ├── header.tsx # Header with medical branding + └── disclaimer.tsx # Medical disclaimer components +/lib + ├── lamatic-client.ts # Lamatic SDK client + └── utils.ts # Tailwind class merge utility +/flows + └── medical-assistant-chat/ + ├── config.json # Flow configuration + ├── inputs.json # Input schema + ├── meta.json # Flow metadata + └── README.md # Flow documentation +``` + +--- + +## 🏥 Features + +- **Conversational Interface** — Chat-style Q&A with message history +- **Symptom Guidance** — Describe symptoms to get relevant medical information +- **Suggested Prompts** — Quick-start chips for common health questions +- **Markdown Rendering** — Rich formatted responses with headers, lists, and emphasis +- **Medical Disclaimers** — Persistent disclaimer banner + per-response reminders +- **Copy to Clipboard** — Easy sharing of responses +- **Emergency Detection** — Advises calling emergency services when appropriate + +--- + +## 🔒 Privacy & Data Handling + +- No user data is stored persistently — chat history exists only in the browser session +- Queries are sent to the Lamatic flow for processing and are not logged client-side +- No personal health information (PHI) is collected or stored +- Review your Lamatic project's data handling policies for server-side processing details + +--- + +## 🤝 Contributing + +We welcome contributions! Please see [CONTRIBUTING.md](../../../CONTRIBUTING.md) for guidelines. + +--- + +## 📜 License + +MIT License — see [LICENSE](../../../LICENSE). diff --git a/kits/assistant/medical-assistant/actions/orchestrate.ts b/kits/assistant/medical-assistant/actions/orchestrate.ts new file mode 100644 index 00000000..1b7da0ae --- /dev/null +++ b/kits/assistant/medical-assistant/actions/orchestrate.ts @@ -0,0 +1,111 @@ +"use server" + +import { lamaticClient } from "@/lib/lamatic-client" +import {config} from "../orchestrate.js" + +export async function sendMedicalQuery( + query: string, +): Promise<{ + success: boolean + data?: any + error?: string +}> { + try { + console.log("[medical-assistant] Processing query, length:", query.length) + + // Get the first workflow from the config + const flows = config.flows + const firstFlowKey = Object.keys(flows)[0] + + if (!firstFlowKey) { + throw new Error("No workflows found in configuration") + } + + const flow = flows[firstFlowKey as keyof typeof flows] as (typeof flows)[keyof typeof flows]; + console.log("[medical-assistant] Using workflow:", flow.name, flow.workflowId); + + // Prepare inputs based on the flow's input schema + const inputs: Record = { + query, + } + + // Map to schema if needed + for (const inputKey of Object.keys(flow.inputSchema || {})) { + if (inputKey === "query" || inputKey === "question" || inputKey === "message") { + inputs[inputKey] = query + } + } + + console.log("[medical-assistant] Sending inputs for workflow:", flow.workflowId) + + if(!flow.workflowId){ + throw new Error("Workflow not found in config.") + } + let resData = await lamaticClient.executeFlow(flow.workflowId, inputs) + console.log("[medical-assistant] Response received, status:", resData?.status) + + // Check for API-level errors first + if (resData?.status === "error") { + const apiError = resData?.message || "Unknown workflow error" + throw new Error(`Lamatic workflow error: ${apiError}. Please check your workflow configuration on the Lamatic dashboard.`) + } + + // Handle async response - if we get a requestId, poll for the result + if (resData?.result?.requestId && !resData?.result?.answer) { + const requestId = resData.result.requestId + console.log("[medical-assistant] Async response, polling with requestId:", requestId) + + const asyncResult = await lamaticClient.checkStatus(requestId, 2, 60) + console.log("[medical-assistant] Async poll result:", asyncResult) + + if (asyncResult?.status === "error") { + throw new Error(`Workflow execution failed: ${asyncResult?.message || "Unknown error"}`) + } + + resData = asyncResult + } + + // Parse the answer - handle multiple response structures + const rawAnswer = resData?.result?.answer + || (resData as any)?.data?.output?.result?.answer + || resData?.result?.output?.answer + || (typeof resData?.result === "string" ? resData.result : null) + + // If the answer is an object (LLM output), extract the generatedResponse text + let answer: string | null = null + if (typeof rawAnswer === "object" && rawAnswer !== null) { + answer = rawAnswer.generatedResponse || rawAnswer.text || rawAnswer.content || JSON.stringify(rawAnswer) + } else if (typeof rawAnswer === "string" && rawAnswer.length > 0) { + answer = rawAnswer + } + + console.log("[medical-assistant] Parsed answer:", answer ? `[${answer.length} chars]` : "null") + + if (!answer) { + throw new Error("No answer found in response. Check workflow output configuration.") + } + + return { + success: true, + data: answer, + } + } catch (error) { + console.error("[medical-assistant] Query error:", error) + + let errorMessage = "Unknown error occurred" + if (error instanceof Error) { + errorMessage = error.message + if (error.message.includes("fetch failed")) { + errorMessage = + "Network error: Unable to connect to the service. Please check your internet connection and try again." + } else if (error.message.includes("API key")) { + errorMessage = "Authentication error: Please check your API configuration." + } + } + + return { + success: false, + error: errorMessage, + } + } +} diff --git a/kits/assistant/medical-assistant/app/globals.css b/kits/assistant/medical-assistant/app/globals.css new file mode 100644 index 00000000..5441ce08 --- /dev/null +++ b/kits/assistant/medical-assistant/app/globals.css @@ -0,0 +1,189 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@plugin "@tailwindcss/typography"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(0.985 0.002 155); + --foreground: oklch(0.145 0.01 155); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0.01 155); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0.01 155); + --primary: oklch(0.55 0.15 168); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.96 0.01 155); + --secondary-foreground: oklch(0.25 0.03 155); + --muted: oklch(0.96 0.005 155); + --muted-foreground: oklch(0.5 0.02 155); + --accent: oklch(0.96 0.01 155); + --accent-foreground: oklch(0.25 0.03 155); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.91 0.01 155); + --input: oklch(0.91 0.01 155); + --ring: oklch(0.55 0.15 168); + --chart-1: oklch(0.55 0.15 168); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.55 0.15 168); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.55 0.15 168); +} + +.dark { + --background: oklch(0.145 0.01 155); + --foreground: oklch(0.985 0 0); + --card: oklch(0.18 0.01 155); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.18 0.01 155); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.7 0.17 168); + --primary-foreground: oklch(0.145 0.01 155); + --secondary: oklch(0.269 0.01 155); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0.01 155); + --muted-foreground: oklch(0.708 0.02 155); + --accent: oklch(0.269 0.01 155); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.3 0.01 155); + --input: oklch(0.3 0.01 155); + --ring: oklch(0.7 0.17 168); + --chart-1: oklch(0.7 0.17 168); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.7 0.17 168); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.3 0.01 155); + --sidebar-ring: oklch(0.7 0.17 168); +} + +@theme inline { + --font-sans: "Geist", "Geist Fallback"; + --font-mono: "Geist Mono", "Geist Mono Fallback"; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* ===== Chat Message Animations ===== */ +@keyframes chatMessageEnter { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.chat-message-enter { + animation: chatMessageEnter 0.25s cubic-bezier(0.2, 0, 0, 1) forwards; +} + +/* ===== Markdown prose overrides in chat ===== */ +.prose p:first-child { + margin-top: 0; +} + +.prose p:last-child { + margin-bottom: 0; +} + +.prose ul, +.prose ol { + padding-left: 1.25rem; +} + +.prose code { + font-size: 0.8125rem; +} + +.prose pre { + border-radius: 0.5rem; + margin: 0.75rem 0; +} + +/* Minimalist scrollbar */ +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(15, 23, 42, 0.15); + border-radius: 9999px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgba(15, 23, 42, 0.25); +} +.dark ::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.15); +} +.dark ::-webkit-scrollbar-thumb:hover { + background-color: rgba(255, 255, 255, 0.25); +} diff --git a/kits/assistant/medical-assistant/app/layout.tsx b/kits/assistant/medical-assistant/app/layout.tsx new file mode 100644 index 00000000..4b0bc9b4 --- /dev/null +++ b/kits/assistant/medical-assistant/app/layout.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const geist = Geist({ subsets: ["latin"], variable: "--font-geist-sans" }); +const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" }); + +export const metadata: Metadata = { + title: 'Medical Assistant — Lamatic AgentKit', + description: 'AI-powered medical assistant chatbot providing general health information, symptom guidance, and wellness tips. Built with Lamatic.ai.', + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/kits/assistant/medical-assistant/app/page.tsx b/kits/assistant/medical-assistant/app/page.tsx new file mode 100644 index 00000000..53f1a54d --- /dev/null +++ b/kits/assistant/medical-assistant/app/page.tsx @@ -0,0 +1,518 @@ +"use client" + +import type React from "react" +import { useState, useRef, useEffect } from "react" +import { + Loader2, Send, Stethoscope, Copy, Check, User, Bot, + Plus, MessageSquare, PanelLeftClose, PanelLeft, FileText, Github +} from "lucide-react" +import { sendMedicalQuery } from "@/actions/orchestrate" +import ReactMarkdown from "react-markdown" +import { Disclaimer, MiniDisclaimer } from "@/components/disclaimer" +import Link from "next/link" + +interface ChatMessage { + id: string + role: "user" | "assistant" + content: string + timestamp: Date +} + +interface Session { + id: string + title: string + messages: ChatMessage[] + createdAt: Date +} + +export default function MedicalAssistantPage() { + const [sessions, setSessions] = useState([]) + const [activeSessionId, setActiveSessionId] = useState(null) + const [input, setInput] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [copiedId, setCopiedId] = useState(null) + const [sidebarOpen, setSidebarOpen] = useState(true) + const messagesEndRef = useRef(null) + const inputRef = useRef(null) + + const activeSession = sessions.find((s) => s.id === activeSessionId) + const messages = activeSession?.messages || [] + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + } + + useEffect(() => { + scrollToBottom() + }, [messages, isLoading]) + + const createNewSession = () => { + const newSession: Session = { + id: `session-${Date.now()}`, + title: "New Chat", + messages: [], + createdAt: new Date(), + } + setSessions((prev) => [newSession, ...prev]) + setActiveSessionId(newSession.id) + setInput("") + } + + const handleSend = async () => { + const query = input.trim() + if (!query || isLoading) return + + let currentSessionId = activeSessionId + if (!currentSessionId) { + const newSession: Session = { + id: `session-${Date.now()}`, + title: query.length > 40 ? query.slice(0, 40) + "..." : query, + messages: [], + createdAt: new Date(), + } + setSessions((prev) => [newSession, ...prev]) + setActiveSessionId(newSession.id) + currentSessionId = newSession.id + } + + const userMessage: ChatMessage = { + id: `user-${Date.now()}`, + role: "user", + content: query, + timestamp: new Date(), + } + + setSessions((prev) => + prev.map((s) => { + if (s.id === currentSessionId) { + const isFirst = s.messages.length === 0 + return { + ...s, + title: isFirst + ? query.length > 40 + ? query.slice(0, 40) + "..." + : query + : s.title, + messages: [...s.messages, userMessage], + } + } + return s + }) + ) + + setInput("") + setIsLoading(true) + + try { + const response = await sendMedicalQuery(query) + const assistantMessage: ChatMessage = { + id: `assistant-${Date.now()}`, + role: "assistant", + content: response.success + ? response.data + : `I'm sorry, I encountered an error: ${response.error || "Unknown error"}. Please try again.`, + timestamp: new Date(), + } + setSessions((prev) => + prev.map((s) => { + if (s.id === currentSessionId) { + return { ...s, messages: [...s.messages, assistantMessage] } + } + return s + }) + ) + } catch { + const errorMessage: ChatMessage = { + id: `assistant-${Date.now()}`, + role: "assistant", + content: "I'm sorry, something went wrong. Please try again later.", + timestamp: new Date(), + } + setSessions((prev) => + prev.map((s) => { + if (s.id === currentSessionId) { + return { ...s, messages: [...s.messages, errorMessage] } + } + return s + }) + ) + } finally { + setIsLoading(false) + inputRef.current?.focus() + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + const handleCopy = async (id: string, content: string) => { + try { + await navigator.clipboard.writeText(content) + setCopiedId(id) + setTimeout(() => setCopiedId(null), 2000) + } catch (err) { + console.error("Failed to copy:", err) + } + } + + const formatTime = (date: Date) => { + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + } + + return ( +
+ {/* ── Sidebar ── */} + + + {/* ── Main Area ── */} +
+ {/* Top bar */} +
+ + {activeSession && ( +

+ {activeSession.title} +

+ )} +
+ + {messages.length === 0 ? ( + /* ── Welcome State ── */ +
+
+
+
+ ✦ NEXT-GEN HEALTH COMPANION +
+ +

+ Understand your health. +
+ Instantly. +

+
+
+ + {/* Welcome Input Card */} +
+
+
+ +