From 0d223d108ef488a0f4f3c33a133450f3a1a6eea3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 21:32:56 +0000 Subject: [PATCH 1/9] feat: scaffold Next.js WC claims agent portal Full-stack Next.js 14 app-router project for Workers' Compensation claims management with an AI assistant powered by Azure OpenAI. Key pieces: - Azure AD authentication via NextAuth (JWT sessions, roles forwarded) - /api/chat streaming SSE route using @azure/openai SDK with a detailed WC-domain system prompt (triage, medical mgmt, compensability, RTW, reserves, compliance) - AppShell sidebar layout with Dashboard, Claims list, and per-claim view - Per-claim split-panel: claim detail + streaming ChatPanel - staticwebapp.config.json + .azure/deploy.yml for Azure Static Web Apps - .env.example documents all required secrets https://claude.ai/code/session_01SyyafssZgqxsocZQzdtpwD --- wc-claims-portal/.azure/deploy.yml | 67 +++++ wc-claims-portal/.env.example | 17 ++ wc-claims-portal/.gitignore | 30 +++ wc-claims-portal/next.config.ts | 18 ++ wc-claims-portal/package.json | 36 +++ wc-claims-portal/postcss.config.js | 6 + .../src/app/api/auth/[...nextauth]/route.ts | 6 + wc-claims-portal/src/app/api/chat/route.ts | 104 ++++++++ wc-claims-portal/src/app/auth/error/page.tsx | 27 ++ wc-claims-portal/src/app/auth/signin/page.tsx | 68 +++++ wc-claims-portal/src/app/claims/[id]/page.tsx | 32 +++ wc-claims-portal/src/app/claims/page.tsx | 26 ++ wc-claims-portal/src/app/dashboard/page.tsx | 35 +++ wc-claims-portal/src/app/globals.css | 21 ++ wc-claims-portal/src/app/layout.tsx | 25 ++ wc-claims-portal/src/app/page.tsx | 11 + .../src/components/chat/ChatPanel.tsx | 232 ++++++++++++++++++ .../src/components/claims/ClaimDetail.tsx | 113 +++++++++ .../src/components/claims/ClaimsTable.tsx | 128 ++++++++++ .../src/components/claims/DashboardStats.tsx | 62 +++++ .../src/components/claims/RecentActivity.tsx | 94 +++++++ .../src/components/ui/AppShell.tsx | 121 +++++++++ .../src/components/ui/AuthProvider.tsx | 7 + wc-claims-portal/src/lib/auth.ts | 45 ++++ wc-claims-portal/src/lib/azure-openai.ts | 73 ++++++ wc-claims-portal/src/middleware.ts | 22 ++ wc-claims-portal/src/types/chat.ts | 22 ++ wc-claims-portal/src/types/next-auth.d.ts | 24 ++ wc-claims-portal/staticwebapp.config.json | 26 ++ wc-claims-portal/tailwind.config.ts | 26 ++ wc-claims-portal/tsconfig.json | 22 ++ 31 files changed, 1546 insertions(+) create mode 100644 wc-claims-portal/.azure/deploy.yml create mode 100644 wc-claims-portal/.env.example create mode 100644 wc-claims-portal/.gitignore create mode 100644 wc-claims-portal/next.config.ts create mode 100644 wc-claims-portal/package.json create mode 100644 wc-claims-portal/postcss.config.js create mode 100644 wc-claims-portal/src/app/api/auth/[...nextauth]/route.ts create mode 100644 wc-claims-portal/src/app/api/chat/route.ts create mode 100644 wc-claims-portal/src/app/auth/error/page.tsx create mode 100644 wc-claims-portal/src/app/auth/signin/page.tsx create mode 100644 wc-claims-portal/src/app/claims/[id]/page.tsx create mode 100644 wc-claims-portal/src/app/claims/page.tsx create mode 100644 wc-claims-portal/src/app/dashboard/page.tsx create mode 100644 wc-claims-portal/src/app/globals.css create mode 100644 wc-claims-portal/src/app/layout.tsx create mode 100644 wc-claims-portal/src/app/page.tsx create mode 100644 wc-claims-portal/src/components/chat/ChatPanel.tsx create mode 100644 wc-claims-portal/src/components/claims/ClaimDetail.tsx create mode 100644 wc-claims-portal/src/components/claims/ClaimsTable.tsx create mode 100644 wc-claims-portal/src/components/claims/DashboardStats.tsx create mode 100644 wc-claims-portal/src/components/claims/RecentActivity.tsx create mode 100644 wc-claims-portal/src/components/ui/AppShell.tsx create mode 100644 wc-claims-portal/src/components/ui/AuthProvider.tsx create mode 100644 wc-claims-portal/src/lib/auth.ts create mode 100644 wc-claims-portal/src/lib/azure-openai.ts create mode 100644 wc-claims-portal/src/middleware.ts create mode 100644 wc-claims-portal/src/types/chat.ts create mode 100644 wc-claims-portal/src/types/next-auth.d.ts create mode 100644 wc-claims-portal/staticwebapp.config.json create mode 100644 wc-claims-portal/tailwind.config.ts create mode 100644 wc-claims-portal/tsconfig.json diff --git a/wc-claims-portal/.azure/deploy.yml b/wc-claims-portal/.azure/deploy.yml new file mode 100644 index 0000000..2118d65 --- /dev/null +++ b/wc-claims-portal/.azure/deploy.yml @@ -0,0 +1,67 @@ +name: Deploy WC Claims Portal to Azure Static Web Apps + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - main + +jobs: + build_and_deploy: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: wc-claims-portal/package-lock.json + + - name: Install dependencies + working-directory: wc-claims-portal + run: npm ci + + - name: Build Next.js app + working-directory: wc-claims-portal + env: + NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL }} + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} + AZURE_AD_CLIENT_ID: ${{ secrets.AZURE_AD_CLIENT_ID }} + AZURE_AD_CLIENT_SECRET: ${{ secrets.AZURE_AD_CLIENT_SECRET }} + AZURE_AD_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} + NEXT_PUBLIC_APP_NAME: "WC Claims Agent Portal" + NEXT_PUBLIC_ORG_NAME: "AdaptCloud" + run: npm run build + + - name: Deploy to Azure Static Web Apps + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + action: "upload" + app_location: "wc-claims-portal" + output_location: ".next/standalone" + skip_app_build: true + + close_pull_request: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request + + steps: + - name: Close Pull Request + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} + action: "close" diff --git a/wc-claims-portal/.env.example b/wc-claims-portal/.env.example new file mode 100644 index 0000000..2e0cb09 --- /dev/null +++ b/wc-claims-portal/.env.example @@ -0,0 +1,17 @@ +# Azure AD / NextAuth +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET= + +AZURE_AD_CLIENT_ID= +AZURE_AD_CLIENT_SECRET= +AZURE_AD_TENANT_ID= + +# Azure OpenAI +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o +AZURE_OPENAI_API_VERSION=2024-10-21 + +# App +NEXT_PUBLIC_APP_NAME="WC Claims Agent Portal" +NEXT_PUBLIC_ORG_NAME="AdaptCloud" diff --git a/wc-claims-portal/.gitignore b/wc-claims-portal/.gitignore new file mode 100644 index 0000000..d2aeb22 --- /dev/null +++ b/wc-claims-portal/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Next.js build output +.next/ +out/ + +# Production +build/ + +# Env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Vercel / Azure +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts diff --git a/wc-claims-portal/next.config.ts b/wc-claims-portal/next.config.ts new file mode 100644 index 0000000..09c897a --- /dev/null +++ b/wc-claims-portal/next.config.ts @@ -0,0 +1,18 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", + experimental: { + serverComponentsExternalPackages: ["@azure/openai"], + }, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "*.microsoftonline.com", + }, + ], + }, +}; + +export default nextConfig; diff --git a/wc-claims-portal/package.json b/wc-claims-portal/package.json new file mode 100644 index 0000000..9a37937 --- /dev/null +++ b/wc-claims-portal/package.json @@ -0,0 +1,36 @@ +{ + "name": "wc-claims-portal", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@azure/openai": "^2.0.0", + "next": "14.2.29", + "next-auth": "^4.24.10", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "clsx": "^2.1.1", + "tailwind-merge": "^2.5.4", + "lucide-react": "^0.468.0", + "date-fns": "^4.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", + "eslint-config-next": "14.2.29", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2" + } +} diff --git a/wc-claims-portal/postcss.config.js b/wc-claims-portal/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/wc-claims-portal/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/wc-claims-portal/src/app/api/auth/[...nextauth]/route.ts b/wc-claims-portal/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..7b38c1b --- /dev/null +++ b/wc-claims-portal/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/wc-claims-portal/src/app/api/chat/route.ts b/wc-claims-portal/src/app/api/chat/route.ts new file mode 100644 index 0000000..a70f1eb --- /dev/null +++ b/wc-claims-portal/src/app/api/chat/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth"; +import { + getAzureOpenAIClient, + DEPLOYMENT, + WC_SYSTEM_PROMPT, +} from "@/lib/azure-openai"; +import { z } from "zod"; + +export const runtime = "nodejs"; +export const maxDuration = 60; + +const ChatRequestSchema = z.object({ + messages: z + .array( + z.object({ + role: z.enum(["user", "assistant"]), + content: z.string().min(1).max(32_000), + }) + ) + .min(1) + .max(50), + claimId: z.string().optional(), +}); + +export async function POST(req: NextRequest) { + // Require authenticated session + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + const parsed = ChatRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: "Bad request", details: parsed.error.flatten() }, + { status: 400 } + ); + } + + const { messages, claimId } = parsed.data; + + const systemContent = claimId + ? `${WC_SYSTEM_PROMPT}\n\n## Active Claim Context\nClaim ID: ${claimId}` + : WC_SYSTEM_PROMPT; + + const openai = getAzureOpenAIClient(); + + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder(); + + try { + const completion = await openai.chat.completions.create({ + model: DEPLOYMENT, + messages: [ + { role: "system", content: systemContent }, + ...messages, + ], + stream: true, + max_tokens: 2048, + temperature: 0.2, + }); + + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta?.content; + if (delta) { + // Server-Sent Events format + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ content: delta })}\n\n`) + ); + } + } + + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + } catch (err) { + const message = + err instanceof Error ? err.message : "Azure OpenAI error"; + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ error: message })}\n\n`) + ); + } finally { + controller.close(); + } + }, + }); + + return new NextResponse(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }, + }); +} diff --git a/wc-claims-portal/src/app/auth/error/page.tsx b/wc-claims-portal/src/app/auth/error/page.tsx new file mode 100644 index 0000000..e875ae5 --- /dev/null +++ b/wc-claims-portal/src/app/auth/error/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import Link from "next/link"; +import { AlertTriangle } from "lucide-react"; + +export default function AuthError() { + return ( +
+
+ +

+ Authentication Error +

+

+ There was a problem signing you in. Your account may not be authorized + to access this portal. +

+ + Try Again + +
+
+ ); +} diff --git a/wc-claims-portal/src/app/auth/signin/page.tsx b/wc-claims-portal/src/app/auth/signin/page.tsx new file mode 100644 index 0000000..51d0767 --- /dev/null +++ b/wc-claims-portal/src/app/auth/signin/page.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useSearchParams } from "next/navigation"; +import { Suspense } from "react"; +import { ShieldCheck } from "lucide-react"; + +function SignInContent() { + const params = useSearchParams(); + const callbackUrl = params.get("callbackUrl") ?? "/dashboard"; + const error = params.get("error"); + + return ( +
+
+
+
+ +
+

+ {process.env.NEXT_PUBLIC_APP_NAME ?? "WC Claims Agent Portal"} +

+

+ {process.env.NEXT_PUBLIC_ORG_NAME ?? "AdaptCloud"} +

+
+ + {error && ( +
+ Authentication failed. Please try again or contact your + administrator. +
+ )} + + + +

+ Access restricted to authorized claims personnel only. +

+
+
+ ); +} + +function MicrosoftIcon() { + return ( + + ); +} + +export default function SignInPage() { + return ( + + + + ); +} diff --git a/wc-claims-portal/src/app/claims/[id]/page.tsx b/wc-claims-portal/src/app/claims/[id]/page.tsx new file mode 100644 index 0000000..d461a2f --- /dev/null +++ b/wc-claims-portal/src/app/claims/[id]/page.tsx @@ -0,0 +1,32 @@ +import { getServerSession } from "next-auth/next"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/lib/auth"; +import { AppShell } from "@/components/ui/AppShell"; +import { ClaimDetail } from "@/components/claims/ClaimDetail"; +import { ChatPanel } from "@/components/chat/ChatPanel"; + +interface Props { + params: Promise<{ id: string }>; +} + +export default async function ClaimPage({ params }: Props) { + const session = await getServerSession(authOptions); + if (!session) redirect("/auth/signin"); + + const { id } = await params; + + return ( + +
+
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/wc-claims-portal/src/app/claims/page.tsx b/wc-claims-portal/src/app/claims/page.tsx new file mode 100644 index 0000000..27dc035 --- /dev/null +++ b/wc-claims-portal/src/app/claims/page.tsx @@ -0,0 +1,26 @@ +import { getServerSession } from "next-auth/next"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/lib/auth"; +import { AppShell } from "@/components/ui/AppShell"; +import { ClaimsTable } from "@/components/claims/ClaimsTable"; + +export default async function ClaimsPage() { + const session = await getServerSession(authOptions); + if (!session) redirect("/auth/signin"); + + return ( + +
+
+
+

Claims

+

+ Manage and review workers' compensation claims +

+
+
+ +
+
+ ); +} diff --git a/wc-claims-portal/src/app/dashboard/page.tsx b/wc-claims-portal/src/app/dashboard/page.tsx new file mode 100644 index 0000000..4abf573 --- /dev/null +++ b/wc-claims-portal/src/app/dashboard/page.tsx @@ -0,0 +1,35 @@ +import { getServerSession } from "next-auth/next"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/lib/auth"; +import { AppShell } from "@/components/ui/AppShell"; +import { DashboardStats } from "@/components/claims/DashboardStats"; +import { RecentActivity } from "@/components/claims/RecentActivity"; + +export default async function DashboardPage() { + const session = await getServerSession(authOptions); + if (!session) redirect("/auth/signin"); + + return ( + +
+
+

+ Welcome back, {session.user.name?.split(" ")[0]} +

+

+ WC Claims Dashboard — {new Date().toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} +

+
+ +
+ +
+
+
+ ); +} diff --git a/wc-claims-portal/src/app/globals.css b/wc-claims-portal/src/app/globals.css new file mode 100644 index 0000000..3660754 --- /dev/null +++ b/wc-claims-portal/src/app/globals.css @@ -0,0 +1,21 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --bg: 248 250 252; + --surface: 255 255 255; + } + + body { + @apply bg-slate-50 text-slate-900 antialiased; + } +} + +@layer utilities { + .scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: theme(colors.slate.300) transparent; + } +} diff --git a/wc-claims-portal/src/app/layout.tsx b/wc-claims-portal/src/app/layout.tsx new file mode 100644 index 0000000..4718b0f --- /dev/null +++ b/wc-claims-portal/src/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { AuthProvider } from "@/components/ui/AuthProvider"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: process.env.NEXT_PUBLIC_APP_NAME ?? "WC Claims Agent Portal", + description: "Workers' Compensation claims management powered by AI", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/wc-claims-portal/src/app/page.tsx b/wc-claims-portal/src/app/page.tsx new file mode 100644 index 0000000..775821e --- /dev/null +++ b/wc-claims-portal/src/app/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth"; + +export default async function Home() { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/auth/signin"); + } + redirect("/dashboard"); +} diff --git a/wc-claims-portal/src/components/chat/ChatPanel.tsx b/wc-claims-portal/src/components/chat/ChatPanel.tsx new file mode 100644 index 0000000..6addb3d --- /dev/null +++ b/wc-claims-portal/src/components/chat/ChatPanel.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useState, useRef, useEffect, useCallback } from "react"; +import { Send, Bot, User, Loader2 } from "lucide-react"; +import ReactMarkdown from "react-markdown"; +import { clsx } from "clsx"; +import type { Message } from "@/types/chat"; + +interface Props { + claimId?: string; +} + +let _msgId = 0; +const uid = () => `msg-${++_msgId}-${Date.now()}`; + +export function ChatPanel({ claimId }: Props) { + const [messages, setMessages] = useState([ + { + id: uid(), + role: "assistant", + content: claimId + ? `I'm your WC claims assistant. I have context for claim **${claimId}**. How can I help you with this claim today?` + : "I'm your WC claims assistant. Ask me about any claim, coverage analysis, return-to-work planning, reserves, or jurisdiction requirements.", + createdAt: new Date(), + }, + ]); + const [input, setInput] = useState(""); + const [streaming, setStreaming] = useState(false); + const bottomRef = useRef(null); + const textareaRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + const send = useCallback(async () => { + const content = input.trim(); + if (!content || streaming) return; + + const userMsg: Message = { + id: uid(), + role: "user", + content, + createdAt: new Date(), + }; + + setMessages((prev) => [...prev, userMsg]); + setInput(""); + setStreaming(true); + + const assistantId = uid(); + setMessages((prev) => [ + ...prev, + { id: assistantId, role: "assistant", content: "", createdAt: new Date() }, + ]); + + try { + const history = [...messages, userMsg] + .filter((m) => m.role !== "system") + .map(({ role, content }) => ({ role, content })); + + const res = await fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ messages: history, claimId }), + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + + const reader = res.body?.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + if (!reader) throw new Error("No response body"); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + if (!line.startsWith("data: ")) continue; + const data = line.slice(6); + if (data === "[DONE]") break; + + try { + const parsed = JSON.parse(data) as { + content?: string; + error?: string; + }; + if (parsed.error) throw new Error(parsed.error); + if (parsed.content) { + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { ...m, content: m.content + parsed.content! } + : m + ) + ); + } + } catch { + // ignore malformed chunks + } + } + } + } catch (err) { + const errorText = + err instanceof Error ? err.message : "Something went wrong"; + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + content: `_Error: ${errorText}. Please try again._`, + } + : m + ) + ); + } finally { + setStreaming(false); + } + }, [input, streaming, messages, claimId]); + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + send(); + } + }; + + return ( +
+
+ + + WC Claims Assistant + + {claimId && ( + + {claimId} + + )} +
+ +
+ {messages.map((msg) => ( + + ))} +
+
+ +
+
+