From 58696c600d49192d58d2c4ce990cf4992d3ee22e Mon Sep 17 00:00:00 2001 From: athivaratz Date: Sun, 5 Jul 2026 19:04:16 +0700 Subject: [PATCH 01/21] feat: enhance AI agent configuration and UI components - Added new settings for agent provider and model configuration in AppSettings, allowing users to select between Gemini and OpenRouter. - Introduced a test button for agent providers in the Admin AI Models page to validate configurations. - Updated the layout to include the AppModeProvider for managing application modes. - Enhanced the bottom navigation and sidebar with a mode switcher for improved user experience. - Updated dependencies in package.json and bun.lock for better compatibility with new features. --- .env.example | 6 +- .gitignore | 1 + app/(app)/assistant/page.tsx | 8 + app/admin/ai/models/page.tsx | 186 ++++++++++ app/admin/ai/page.tsx | 19 + app/agent-globals.css | 97 ++++++ app/api/agent/chat/route.ts | 92 +++++ app/api/agent/test-providers/route.ts | 78 +++++ app/layout.tsx | 3 + bun.lock | 62 +++- components/agent/agent-chat-shell.tsx | 182 ++++++++++ components/agent/agent-composer.tsx | 83 +++++ components/agent/agent-empty-state.tsx | 48 +++ components/agent/agent-message-bubble.tsx | 161 +++++++++ components/agent/agent-message-list.tsx | 82 +++++ components/agent/agent-thinking-log.tsx | 96 +++++ components/agent/agent-top-bar.tsx | 54 +++ components/agent/agent-typing-indicator.tsx | 24 ++ components/agent/classic-quick-links.tsx | 73 ++++ components/agent/item-result-card.tsx | 101 ++++++ components/agent/match-result-card.tsx | 47 +++ components/agent/mode-switcher.tsx | 65 ++++ components/agent/ner-result-card.tsx | 40 +++ .../agent/traditional-fallback-panel.tsx | 56 +++ components/agent/voice-sphere-overlay.tsx | 107 ++++++ components/layout/bottom-nav.tsx | 3 + components/layout/sidebar.tsx | 5 + components/layout/student-app-shell.tsx | 14 + contexts/app-mode-context.tsx | 136 ++++++++ hooks/use-voice-input.ts | 151 ++++++++ lib/agent/context-pruner.ts | 17 + lib/agent/create-agent.ts | 30 ++ lib/agent/fallback.ts | 40 +++ lib/agent/item-actions-server.ts | 249 +++++++++++++ lib/agent/item-queries-server.ts | 133 +++++++ lib/agent/provider-router.ts | 123 +++++++ lib/agent/row-mappers.ts | 115 ++++++ lib/agent/system-prompt.ts | 15 + lib/agent/tools/index.ts | 329 ++++++++++++++++++ lib/agent/validations/agent-tools.ts | 87 +++++ lib/copy/thai-student.ts | 45 +++ lib/menu.ts | 10 +- lib/ner-fallback.ts | 77 ++++ lib/ner.ts | 71 ++-- lib/types.ts | 18 + package.json | 4 + .../20250702000000_agent_search_indexes.sql | 8 + 47 files changed, 3413 insertions(+), 38 deletions(-) create mode 100644 app/(app)/assistant/page.tsx create mode 100644 app/agent-globals.css create mode 100644 app/api/agent/chat/route.ts create mode 100644 app/api/agent/test-providers/route.ts create mode 100644 components/agent/agent-chat-shell.tsx create mode 100644 components/agent/agent-composer.tsx create mode 100644 components/agent/agent-empty-state.tsx create mode 100644 components/agent/agent-message-bubble.tsx create mode 100644 components/agent/agent-message-list.tsx create mode 100644 components/agent/agent-thinking-log.tsx create mode 100644 components/agent/agent-top-bar.tsx create mode 100644 components/agent/agent-typing-indicator.tsx create mode 100644 components/agent/classic-quick-links.tsx create mode 100644 components/agent/item-result-card.tsx create mode 100644 components/agent/match-result-card.tsx create mode 100644 components/agent/mode-switcher.tsx create mode 100644 components/agent/ner-result-card.tsx create mode 100644 components/agent/traditional-fallback-panel.tsx create mode 100644 components/agent/voice-sphere-overlay.tsx create mode 100644 contexts/app-mode-context.tsx create mode 100644 hooks/use-voice-input.ts create mode 100644 lib/agent/context-pruner.ts create mode 100644 lib/agent/create-agent.ts create mode 100644 lib/agent/fallback.ts create mode 100644 lib/agent/item-actions-server.ts create mode 100644 lib/agent/item-queries-server.ts create mode 100644 lib/agent/provider-router.ts create mode 100644 lib/agent/row-mappers.ts create mode 100644 lib/agent/system-prompt.ts create mode 100644 lib/agent/tools/index.ts create mode 100644 lib/agent/validations/agent-tools.ts create mode 100644 lib/copy/thai-student.ts create mode 100644 lib/ner-fallback.ts create mode 100644 supabase/migrations/20250702000000_agent_search_indexes.sql diff --git a/.env.example b/.env.example index e189117..5ec6e9e 100644 --- a/.env.example +++ b/.env.example @@ -22,4 +22,8 @@ SCHOOL_AUTH_DOMAIN=your.domain.com NEXT_PUBLIC_APP_URL=https://your.domain.com # Gemini API KEY -GEMMA_API_KEY=YOUR_GEMMA_API_KEY \ No newline at end of file +GEMMA_API_KEY=YOUR_GEMMA_API_KEY + +# OpenRouter (Agent fallback / alternate provider) +OPENROUTER_API_KEY= +OPENROUTER_MODEL=google/gemini-2.0-flash-exp:free \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9a02c60..d075097 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ service-account*.json *.tsbuildinfo next-env.d.ts +# development docs/ .vscode/ .cursor/ diff --git a/app/(app)/assistant/page.tsx b/app/(app)/assistant/page.tsx new file mode 100644 index 0000000..4ea71fe --- /dev/null +++ b/app/(app)/assistant/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +import { AgentChatShell } from "@/components/agent/agent-chat-shell"; +import "@/app/agent-globals.css"; + +export default function AssistantPage() { + return ; +} diff --git a/app/admin/ai/models/page.tsx b/app/admin/ai/models/page.tsx index e40024a..6383e2f 100644 --- a/app/admin/ai/models/page.tsx +++ b/app/admin/ai/models/page.tsx @@ -14,6 +14,7 @@ import { CheckCircle2, AlertTriangle, Search, + Activity, } from "lucide-react"; import { useAuth } from "@/contexts/auth-context"; import { getAppSettings, updateAppSettings } from "@/lib/database"; @@ -36,6 +37,58 @@ function parseNumber(value: string) { return Number.isNaN(parsed) ? undefined : parsed; } +function AgentProviderTestButton({ settings }: { settings: AppSettings }) { + const [testing, setTesting] = useState(false); + const [result, setResult] = useState(null); + + const runTest = async () => { + setTesting(true); + setResult(null); + try { + const res = await fetch("/api/agent/test-providers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ settings }), + }); + const data = await res.json(); + const lines = Object.entries(data.providers || {}).map( + ([name, info]) => { + const p = info as { + configured: boolean; + ok: boolean; + model?: string; + error?: string; + }; + const modelSuffix = p.model ? ` [${p.model}]` : ""; + if (p.ok) return `${name}${modelSuffix}: OK`; + if (!p.configured) return `${name}: no key`; + return `${name}${modelSuffix}: ${p.error || "fail"}`; + } + ); + setResult(lines.join(" · ")); + } catch { + setResult("ทดสอบไม่สำเร็จ"); + } finally { + setTesting(false); + } + }; + + return ( +
+ + {result ? {result} : null} +
+ ); +} + export default function AdminAIModelsPage() { const { user } = useAuth(); const [settings, setSettings] = useState(DEFAULT_APP_SETTINGS); @@ -126,6 +179,13 @@ export default function AdminAIModelsPage() { ); }, [generateContentModels, settings.aiVisionModel]); + const agentModelValid = useMemo(() => { + if (!settings.agentModel) return false; + return generateContentModels.some( + (model) => normalizeModelName(model.name) === normalizeModelName(settings.agentModel || "") + ); + }, [generateContentModels, settings.agentModel]); + const handleSave = async () => { if (!user?.uid) return; @@ -145,6 +205,14 @@ export default function AdminAIModelsPage() { aiVisionTemperature: settings.aiVisionTemperature, aiVisionTopP: settings.aiVisionTopP, aiVisionMaxOutputTokens: settings.aiVisionMaxOutputTokens, + agentProvider: settings.agentProvider, + agentFallbackProvider: settings.agentFallbackProvider, + agentModel: settings.agentModel, + agentOpenRouterModel: settings.agentOpenRouterModel, + agentMaxSteps: settings.agentMaxSteps, + agentMaxOutputTokens: settings.agentMaxOutputTokens, + agentTemperature: settings.agentTemperature, + agentContextMaxMessages: settings.agentContextMaxMessages, }, user.uid ); @@ -465,6 +533,116 @@ export default function AdminAIModelsPage() { +
+

+ Agentic AI (ผู้ช่วย /assistant) +

+

+ ตั้งค่า provider, โมเดล, และขีดจำกัด agent loop +

+
+
+ + +
+
+ + +
+
+
+ + {!agentModelValid && settings.agentModel && ( + + + ไม่อยู่ในรายการ + + )} +
+ + setSettings((prev) => ({ ...prev, agentModel: e.target.value })) + } + placeholder="models/gemini-2.5-flash" + className="mt-1 w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white" + /> +
+
+ + + setSettings((prev) => ({ + ...prev, + agentOpenRouterModel: e.target.value, + })) + } + className="mt-1 w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm" + /> +
+
+ + + setSettings((prev) => ({ + ...prev, + agentMaxSteps: parseNumber(e.target.value), + })) + } + className="mt-1 w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm" + /> +
+
+ + + setSettings((prev) => ({ + ...prev, + agentContextMaxMessages: parseNumber(e.target.value), + })) + } + className="mt-1 w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm" + /> +
+
+ +
+
@@ -564,6 +742,14 @@ export default function AdminAIModelsPage() { > ใช้กับ Vision +
)) diff --git a/app/admin/ai/page.tsx b/app/admin/ai/page.tsx index 1c9d87c..d3d634d 100644 --- a/app/admin/ai/page.tsx +++ b/app/admin/ai/page.tsx @@ -123,7 +123,26 @@ export default function AdminAIPage() { )}
+
+
Agent Provider
+
+ {loading ? ( + + + กำลังโหลด + + ) : ( + settings.agentProvider || "auto" + )} +
+
+ + เปิดหน้าผู้ช่วย AI → + ); diff --git a/app/agent-globals.css b/app/agent-globals.css new file mode 100644 index 0000000..0cb3da0 --- /dev/null +++ b/app/agent-globals.css @@ -0,0 +1,97 @@ +@import "./globals.css"; + +/* Agent workspace utilities */ +.agent-mesh-bg { + background: + radial-gradient(ellipse 80% 50% at 50% -20%, rgba(6, 199, 85, 0.12), transparent), + radial-gradient(ellipse 60% 40% at 100% 0%, rgba(59, 130, 246, 0.06), transparent), + var(--bg-secondary); +} + +.dark .agent-mesh-bg { + background: + radial-gradient(ellipse 80% 50% at 50% -20%, rgba(6, 199, 85, 0.08), transparent), + radial-gradient(ellipse 60% 40% at 100% 0%, rgba(59, 130, 246, 0.04), transparent), + var(--bg-secondary); +} + +.agent-stream-cursor { + display: inline-block; + width: 2px; + height: 1em; + margin-left: 2px; + vertical-align: text-bottom; + background: var(--line-green); + animation: agent-cursor-blink 1s step-end infinite; +} + +@keyframes agent-cursor-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +.agent-glass { + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} + +.agent-composer-shadow { + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.08), + 0 0 0 1px rgba(6, 199, 85, 0.08); +} + +.dark .agent-composer-shadow { + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(255, 255, 255, 0.06); +} + +.agent-thinking-panel { + background: var(--agent-thinking-bg, rgba(0, 0, 0, 0.03)); +} + +.dark .agent-thinking-panel { + --agent-thinking-bg: rgba(255, 255, 255, 0.04); +} + +@media (prefers-reduced-motion: reduce) { + .agent-stream-cursor { + animation: none; + } + + .agent-typing-dot { + animation: none; + opacity: 0.6; + } +} + +.agent-typing-dot { + width: 8px; + height: 8px; + border-radius: 9999px; + background: var(--text-tertiary); + animation: agent-typing-bounce 1.2s ease-in-out infinite; +} + +.dark .agent-typing-dot { + background: rgba(255, 255, 255, 0.45); +} + +@keyframes agent-typing-bounce { + 0%, + 60%, + 100% { + transform: translateY(0); + opacity: 0.45; + } + 30% { + transform: translateY(-5px); + opacity: 1; + } +} diff --git a/app/api/agent/chat/route.ts b/app/api/agent/chat/route.ts new file mode 100644 index 0000000..f12e96b --- /dev/null +++ b/app/api/agent/chat/route.ts @@ -0,0 +1,92 @@ +import { NextRequest } from "next/server"; +import { createAgentUIStreamResponse, type UIMessage } from "ai"; +import { createClient } from "@/lib/supabase/server"; +import { + checkAndRecordRateLimitAtomic, + getAppSettingsAdmin, +} from "@/lib/ai-rate-limit"; +import { pruneUiMessages } from "@/lib/agent/context-pruner"; +import { createFoundUAgent } from "@/lib/agent/create-agent"; +import { + buildFallbackPayload, + isProviderError, +} from "@/lib/agent/fallback"; +import { withProviderFallback } from "@/lib/agent/provider-router"; +import { thaiCopy } from "@/lib/copy/thai-student"; +import { DEFAULT_APP_SETTINGS } from "@/lib/types"; + +export const maxDuration = 30; + +export async function POST(request: NextRequest) { + try { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + const messages = (body.messages || []) as UIMessage[]; + + const settings = await getAppSettingsAdmin(); + const mergedSettings = { ...DEFAULT_APP_SETTINGS, ...settings }; + const pruned = pruneUiMessages( + messages, + mergedSettings.agentContextMaxMessages ?? 8 + ); + + const rateLimit = await checkAndRecordRateLimitAtomic( + user.id, + mergedSettings, + "agent-chat" + ); + + if (!rateLimit.allowed) { + return Response.json( + buildFallbackPayload( + "rate_limit", + mergedSettings.aiRateLimitMessage || thaiCopy.agent.rateLimit + ), + { status: 429 } + ); + } + + const { result: streamResponse } = await withProviderFallback( + mergedSettings, + async (provider, model) => { + const agent = createFoundUAgent({ + model, + settings: mergedSettings, + userId: user.id, + }); + + return createAgentUIStreamResponse({ + agent, + uiMessages: pruned, + headers: { + "X-Agent-Provider": provider, + }, + }); + } + ); + + return streamResponse; + } catch (error) { + console.error("[agent/chat] error:", error); + + if (isProviderError(error)) { + return Response.json( + buildFallbackPayload("provider_error", thaiCopy.agent.aiDown), + { status: 503 } + ); + } + + return Response.json( + buildFallbackPayload("unknown", thaiCopy.agent.aiBusy), + { status: 500 } + ); + } +} diff --git a/app/api/agent/test-providers/route.ts b/app/api/agent/test-providers/route.ts new file mode 100644 index 0000000..c7c36fb --- /dev/null +++ b/app/api/agent/test-providers/route.ts @@ -0,0 +1,78 @@ +import { NextResponse } from "next/server"; +import { generateText } from "ai"; +import { getAppSettingsAdmin } from "@/lib/ai-rate-limit"; +import { + getAgentModel, + isProviderConfigured, + type AgentProviderName, +} from "@/lib/agent/provider-router"; +import { DEFAULT_APP_SETTINGS } from "@/lib/types"; + +function resolveModelLabel( + provider: AgentProviderName, + settings: typeof DEFAULT_APP_SETTINGS +) { + if (provider === "openrouter") { + return ( + settings.agentOpenRouterModel || + process.env.OPENROUTER_MODEL || + "google/gemini-2.0-flash-exp:free" + ); + } + return settings.agentModel || "gemini-2.0-flash"; +} + +export async function POST(request: Request) { + let mergedSettings = { ...DEFAULT_APP_SETTINGS, ...(await getAppSettingsAdmin()) }; + try { + const body = await request.json(); + if (body?.settings && typeof body.settings === "object") { + mergedSettings = { ...mergedSettings, ...body.settings }; + } + } catch { + // use database settings only + } + return runProviderTests(mergedSettings); +} + +export async function GET() { + const mergedSettings = { ...DEFAULT_APP_SETTINGS, ...(await getAppSettingsAdmin()) }; + return runProviderTests(mergedSettings); +} + +async function runProviderTests(mergedSettings: typeof DEFAULT_APP_SETTINGS) { + const results: Record< + string, + { configured: boolean; ok: boolean; model?: string; error?: string } + > = { + gemini: { configured: isProviderConfigured("gemini"), ok: false }, + openrouter: { configured: isProviderConfigured("openrouter"), ok: false }, + }; + + for (const provider of ["gemini", "openrouter"] as const) { + const modelLabel = resolveModelLabel(provider, mergedSettings); + results[provider].model = modelLabel; + + if (!results[provider].configured) { + results[provider].error = "API key not configured"; + continue; + } + try { + const model = getAgentModel(provider, mergedSettings); + await generateText({ + model, + prompt: "Reply with OK only.", + maxOutputTokens: 8, + }); + results[provider].ok = true; + } catch (error) { + results[provider].error = + error instanceof Error ? error.message : "Connection failed"; + } + } + + return NextResponse.json({ + providers: results, + settingsSource: "database", + }); +} diff --git a/app/layout.tsx b/app/layout.tsx index 39ba145..e477635 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next"; import { Kanit } from "next/font/google"; import "./globals.css"; import { AuthProvider } from "@/contexts/auth-context"; +import { AppModeProvider } from "@/contexts/app-mode-context"; import { DataProvider } from "@/contexts/DataContext"; import { ThemeProvider } from "@/components/theme-provider"; import { ErrorBoundary } from "@/components/ui/error-boundary"; @@ -54,6 +55,7 @@ export default function RootLayout({ storageKey="theme" > + @@ -69,6 +71,7 @@ export default function RootLayout({ + diff --git a/bun.lock b/bun.lock index 3a21fe5..2d4329b 100644 --- a/bun.lock +++ b/bun.lock @@ -4,12 +4,16 @@ "": { "name": "scfondue", "dependencies": { + "@ai-sdk/google": "^4.0.6", + "@ai-sdk/openai": "^4.0.5", + "@ai-sdk/react": "^4.0.12", "@aws-sdk/client-s3": "^3.879.0", "@aws-sdk/s3-request-presigner": "^3.879.0", "@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/server": "^13.3.0", "@supabase/ssr": "^0.12.0", - "@supabase/supabase-js": "2.105.0", + "@supabase/supabase-js": "^2.108.0", + "ai": "^7.0.11", "browser-image-compression": "^2.0.2", "bun": "^1.3.8", "clsx": "^2.1.1", @@ -43,6 +47,20 @@ }, }, "packages": { + "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.8", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.3", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-esHzojMp+LllqOnEu6sYa6NKOmPcE5UObPV61LE6L2QAzCH6WHMgTS+Q/HF5HU3lVUWsHtkDzN7TNqMfv/ecCg=="], + + "@ai-sdk/google": ["@ai-sdk/google@4.0.6", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.3" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YJnWlHVqG6Lq3NY5Z0hs1+8+P4GFTBLPkmnuFIGdsYi19qB7C36IpK2JOzdkyE47waVJuu6tq/d0rFyHZzY2MQ=="], + + "@ai-sdk/mcp": ["@ai-sdk/mcp@2.0.5", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.3", "pkce-challenge": "^5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0FW3pcvfGEZ9VPSLSsDHxFN9+tMsUVZurVryCnsBWayzsueUPD6RYKkI9vYoqWAIwWFDEemCwZHeMzdHF/wsZQ=="], + + "@ai-sdk/openai": ["@ai-sdk/openai@4.0.5", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.3" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-a9cYQNAKAbkMTt6bHAvDuLv3klwtJT9m0A6x7s8EZlf1ebcxadg/1CONaYh1GA80ZRA9UQHJUS2wHoOphkQQkg=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@4.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6p3C/vGqVIjcptBu1DnVd/BZJ2wWmV9TUv9192vT6ZvT9KNED8EwRTqyqFpoQZKgSbMDSvBSq3dqR524Nt/Crw=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.3", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-El0JOXXGcHFe6+owJ1UV0VgB9XtdivP8vcZni+rUIt6lt+cBHCnUdddaA6LE2avJGJ5AD0uXSY+uAvRL2tSvgw=="], + + "@ai-sdk/react": ["@ai-sdk/react@4.0.12", "", { "dependencies": { "@ai-sdk/mcp": "2.0.5", "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.3", "ai": "7.0.11", "swr": "^2.4.1", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-uQmGQb1VEihU8cZ6mihgxfdILD35tx8QBkD9ni+HqBL9Lbdx6wOcgoMZUiYS/jATVqUXmmcZlhsplfj5EBc2bA=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -343,21 +361,23 @@ "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@supabase/auth-js": ["@supabase/auth-js@2.105.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-cwNB9M4gClqOVJrlX+p2oPgqgRHiUm6hOQSRjgntplB/9XLP78/6MtvkhWdGeWpkP6npZxiLZ+VwNgeigk1wiw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@supabase/auth-js": ["@supabase/auth-js@2.110.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-Mi288WCTp6wxMFCOu/UgzgHEXODjdl2uVTLqK11eanzGZaldU3RyP8Am+ZbNuVzFP+5+iOvppxzv7N5Ym84xTg=="], - "@supabase/functions-js": ["@supabase/functions-js@2.105.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-Q58EDZPb/3KM0Ksp4pUYPrShIAjoC12BRMIKlMOxcpVBYMQRZCDqr5ohRp1pKiCCvRbDD/bhiLIutdBmU5Nu6Q=="], + "@supabase/functions-js": ["@supabase/functions-js@2.110.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-Fde5wlY8ZZy+9yqrWlQHo8MacSyUBArBEtN2boB4thJQigPnQD/cc61qZN0n3I1L0gwhWtHYwIMnOBKxSvF6Hw=="], - "@supabase/phoenix": ["@supabase/phoenix@0.4.2", "", {}, "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A=="], + "@supabase/phoenix": ["@supabase/phoenix@0.4.4", "", {}, "sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ=="], - "@supabase/postgrest-js": ["@supabase/postgrest-js@2.105.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-+M8mHTNEGlWXNvDEU14oL0aGQxAwGra19PO49/Gqco9iHKzgKL2xceE5CiqGOLQ547KMB/1uSFsETIKj8WQYmg=="], + "@supabase/postgrest-js": ["@supabase/postgrest-js@2.110.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ZbC1QZL3jcvBUfVKjJbgRM27G4Mg3Zzqdm44m5pJafe1e52Cli793EOnwQucomBAGEUDd03Nzaf7XV3ji/XexQ=="], - "@supabase/realtime-js": ["@supabase/realtime-js@2.105.0", "", { "dependencies": { "@supabase/phoenix": "^0.4.0", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-sU3bhcZnIT8rny4ZAR257JMjh6tBZVLvhTfczDXDKHaFZVje9Qaaqbl4O9UuuZmPsGWRfOfI1kUJ15uPeL0KhA=="], + "@supabase/realtime-js": ["@supabase/realtime-js@2.110.0", "", { "dependencies": { "@supabase/phoenix": "0.4.4", "tslib": "2.8.1" } }, "sha512-Wn2AWpneZuDFTkp/65tqctvoh+3JvyTjMam8sTMqVWy5BgkU8zAvFwilPYPPPhkINeKF8NAJKP7FclJ2iGCUMw=="], "@supabase/ssr": ["@supabase/ssr@0.12.0", "", { "dependencies": { "cookie": "^1.0.2" }, "peerDependencies": { "@supabase/supabase-js": "^2.108.0" } }, "sha512-d9XV5XzJvzzZbeAIM7fWTCUYxQJZ2Ru6ny3dJHmHGp/LIrJ+o9FpD7N9Rf/UhhWEvHXSoDe8SI32Z2ouOdMjBg=="], - "@supabase/storage-js": ["@supabase/storage-js@2.105.0", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-advo1qhRjeNLPYciUMpGeJTVFqaidPJq/6h4FoPF3XSo2SfecBUYQg/axcy26uon7y58QZoJxxguSmRZhuiRQA=="], + "@supabase/storage-js": ["@supabase/storage-js@2.110.0", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-71+gU3HrhiylAhftY6FmO5PPdcsScnVcS766CVD+vTYK9qTDLbrx8FhgBYbqGm3iV/wkTfzrNJfjGsMeFRkJRQ=="], - "@supabase/supabase-js": ["@supabase/supabase-js@2.105.0", "", { "dependencies": { "@supabase/auth-js": "2.105.0", "@supabase/functions-js": "2.105.0", "@supabase/postgrest-js": "2.105.0", "@supabase/realtime-js": "2.105.0", "@supabase/storage-js": "2.105.0" } }, "sha512-UUmh6KpStf2RdKpRUmzj0cPl6OXlo1hkRTNHdFHozbiJv2MIxR/7eWGKHAO8OgnaZt0gv52k7NL/bZXgPQbw/A=="], + "@supabase/supabase-js": ["@supabase/supabase-js@2.110.0", "", { "dependencies": { "@supabase/auth-js": "2.110.0", "@supabase/functions-js": "2.110.0", "@supabase/postgrest-js": "2.110.0", "@supabase/realtime-js": "2.110.0", "@supabase/storage-js": "2.110.0" } }, "sha512-8yI84VJiEVW4zxZpLUmxXmjzQ7O2St9X/ymzlBETDHTURPWG3LmvbSiibq+7dqAJmyoUfxZnSfXeM4HCM8s4XQ=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -411,8 +431,6 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/type-utils": "8.50.1", "@typescript-eslint/utils": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg=="], @@ -471,10 +489,16 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + + "@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="], + "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "ai": ["ai@7.0.11", "", { "dependencies": { "@ai-sdk/gateway": "4.0.8", "@ai-sdk/provider": "4.0.1", "@ai-sdk/provider-utils": "5.0.3" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ecCtume1NKJG/8oIkV8G26KL9jmFJRx6+Gjx2260FcDZqIUGPiF1uBoNeGy4S1vxbHmFxUul4D7axqiXZZ1XuA=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -581,6 +605,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], @@ -653,6 +679,8 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], @@ -815,6 +843,8 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], @@ -937,6 +967,8 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -1049,12 +1081,16 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "swr": ["swr@2.4.2", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1091,6 +1127,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "uzip": ["uzip@0.20201231.0", "", {}, "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1109,8 +1147,6 @@ "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], @@ -1135,8 +1171,6 @@ "@types/qrcode/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], - "@types/ws/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], diff --git a/components/agent/agent-chat-shell.tsx b/components/agent/agent-chat-shell.tsx new file mode 100644 index 0000000..6ae9e81 --- /dev/null +++ b/components/agent/agent-chat-shell.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport, type UIMessage } from "ai"; +import { useAuth } from "@/contexts/auth-context"; +import { AGENT_MESSAGES_KEY } from "@/contexts/app-mode-context"; +import { AgentTopBar } from "@/components/agent/agent-top-bar"; +import { AgentEmptyState } from "@/components/agent/agent-empty-state"; +import { AgentMessageList } from "@/components/agent/agent-message-list"; +import { AgentComposer } from "@/components/agent/agent-composer"; +import { ClassicQuickLinks } from "@/components/agent/classic-quick-links"; +import { TraditionalFallbackPanel } from "@/components/agent/traditional-fallback-panel"; +import { VoiceSphereOverlay } from "@/components/agent/voice-sphere-overlay"; +import type { AgentFallbackPayload } from "@/lib/agent/fallback"; +import { thaiCopy } from "@/lib/copy/thai-student"; +import Link from "next/link"; +import { AUTH_ROUTES } from "@/lib/auth-routes"; + +function loadStoredMessages(): UIMessage[] { + if (typeof window === "undefined") return []; + try { + const raw = sessionStorage.getItem(AGENT_MESSAGES_KEY); + if (!raw) return []; + return JSON.parse(raw) as UIMessage[]; + } catch { + return []; + } +} + +export function AgentChatShell() { + const { user, loading: authLoading } = useAuth(); + const [input, setInput] = useState(""); + const [fallback, setFallback] = useState(null); + const [voiceOpen, setVoiceOpen] = useState(false); + const [initialMessages] = useState(() => loadStoredMessages()); + + const { messages, sendMessage, setMessages, status, error } = useChat({ + transport: new DefaultChatTransport({ + api: "/api/agent/chat", + fetch: async (input, init) => { + const res = await fetch(input, init); + if (!res.ok) { + try { + const data = (await res.clone().json()) as AgentFallbackPayload; + if (data.fallback) setFallback(data); + } catch { + // ignore parse errors + } + } + return res; + }, + }), + messages: initialMessages, + }); + + useEffect(() => { + if (messages.length > 0) { + sessionStorage.setItem(AGENT_MESSAGES_KEY, JSON.stringify(messages)); + } + }, [messages]); + + useEffect(() => { + if (!error) return; + const tryParseFallback = async () => { + if (error.message) { + try { + const parsed = JSON.parse(error.message) as AgentFallbackPayload; + if (parsed.fallback) { + setFallback(parsed); + return; + } + } catch { + // not json + } + } + setFallback({ + fallback: true, + reason: "unknown", + message: thaiCopy.agent.aiBusy, + suggestedRoutes: [ + { href: "/list", labelKey: "list" }, + { href: "/tracking", labelKey: "tracking" }, + { href: "/lost", labelKey: "lost" }, + { href: "/found", labelKey: "found" }, + ], + }); + }; + void tryParseFallback(); + }, [error]); + + const handleSubmit = useCallback(() => { + const text = input.trim(); + if (!text || !user) return; + setFallback(null); + sendMessage({ text }); + setInput(""); + }, [input, user, sendMessage]); + + const handleNewChat = () => { + setMessages([]); + sessionStorage.removeItem(AGENT_MESSAGES_KEY); + setFallback(null); + setInput(""); + }; + + const isThinking = status === "streaming" || status === "submitted"; + const composerDisabled = !user || isThinking; + + if (authLoading) { + return ( +
+
+
+ ); + } + + if (!user) { + return ( +
+ +
+

{thaiCopy.agent.loginRequired}

+ + เข้าสู่ระบบ + + +
+
+ ); + } + + return ( +
+ + + {messages.length === 0 && !fallback ? ( + { + setInput(prompt); + sendMessage({ text: prompt }); + }} + /> + ) : ( + + )} + + {fallback ? : null} + + { + if (!user || isThinking) return; + setFallback(null); + setInput(prompt); + sendMessage({ text: prompt }); + }} + /> + + setVoiceOpen(true)} + disabled={composerDisabled} + /> + + setVoiceOpen(false)} + onTranscript={(text) => { + setInput(text); + sendMessage({ text }); + setVoiceOpen(false); + }} + /> +
+ ); +} diff --git a/components/agent/agent-composer.tsx b/components/agent/agent-composer.tsx new file mode 100644 index 0000000..0c3fe49 --- /dev/null +++ b/components/agent/agent-composer.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useRef, useEffect, KeyboardEvent } from "react"; +import { Send, Mic } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { thaiCopy } from "@/lib/copy/thai-student"; + +type AgentComposerProps = { + value: string; + onChange: (value: string) => void; + onSubmit: () => void; + onVoiceClick?: () => void; + disabled?: boolean; + className?: string; +}; + +export function AgentComposer({ + value, + onChange, + onSubmit, + onVoiceClick, + disabled, + className, +}: AgentComposerProps) { + const textareaRef = useRef(null); + + useEffect(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = `${Math.min(el.scrollHeight, 120)}px`; + }, [value]); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (value.trim() && !disabled) onSubmit(); + } + }; + + return ( +
+
+