diff --git a/.env.example b/.env.example index 34e9e7b..c5dda91 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,11 @@ # Database (shared PostgreSQL on CT 102) DATABASE_URL="postgresql://echos_ocr:YOUR_PASSWORD@192.168.68.102:5432/echos_ocr" -# AI Provider API Keys (set the ones you need) -OPENAI_API_KEY="" -GOOGLE_GENERATIVE_AI_API_KEY="" -ANTHROPIC_API_KEY="" +# Vercel AI Gateway (routes to OpenAI, Google, Anthropic, etc.) +AI_GATEWAY_API_KEY="" # Ollama (only needed if using Ollama as the AI provider) OLLAMA_BASE_URL="http://192.168.68.108:11434" -OLLAMA_MODEL="llava:7b" # MinIO S3 Storage (CT 105) MINIO_ENDPOINT="192.168.68.105" diff --git a/package-lock.json b/package-lock.json index 7b54f6c..79e58f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,6 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@ai-sdk/anthropic": "^3.0.58", - "@ai-sdk/google": "^3.0.43", - "@ai-sdk/openai": "^3.0.41", "@ai-sdk/openai-compatible": "^2.0.35", "@aws-sdk/client-s3": "^3.1005.0", "@aws-sdk/s3-request-presigner": "^3.1005.0", @@ -53,22 +50,6 @@ "typescript": "^5" } }, - "node_modules/@ai-sdk/anthropic": { - "version": "3.0.58", - "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.58.tgz", - "integrity": "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.19" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/gateway": { "version": "3.0.66", "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.66.tgz", @@ -86,38 +67,6 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/google": { - "version": "3.0.43", - "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.43.tgz", - "integrity": "sha512-NGCgP5g8HBxrNdxvF8Dhww+UKfqAkZAmyYBvbu9YLoBkzAmGKDBGhVptN/oXPB5Vm0jggMdoLycZ8JReQM8Zqg==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.19" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/openai": { - "version": "3.0.41", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.41.tgz", - "integrity": "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.8", - "@ai-sdk/provider-utils": "4.0.19" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/openai-compatible": { "version": "2.0.35", "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-2.0.35.tgz", diff --git a/package.json b/package.json index 65d28c9..9368509 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,6 @@ "postinstall": "npx prisma generate" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.58", - "@ai-sdk/google": "^3.0.43", - "@ai-sdk/openai": "^3.0.41", "@ai-sdk/openai-compatible": "^2.0.35", "@aws-sdk/client-s3": "^3.1005.0", "@aws-sdk/s3-request-presigner": "^3.1005.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1fa8c0b..27c4e99 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -83,6 +83,6 @@ model AppSettings { sourceRetentionDays Int @default(30) imageRetentionDays Int @default(180) - aiProvider String @default("ollama") + aiProvider String @default("gateway") aiModel String @default("") } diff --git a/src/app/api/ai-test/route.ts b/src/app/api/ai-test/route.ts index fadf538..570c677 100644 --- a/src/app/api/ai-test/route.ts +++ b/src/app/api/ai-test/route.ts @@ -1,46 +1,23 @@ import { NextRequest, NextResponse } from "next/server"; import { generateText } from "ai"; -import { openai } from "@ai-sdk/openai"; -import { google } from "@ai-sdk/google"; -import { anthropic } from "@ai-sdk/anthropic"; +import { gateway } from "@ai-sdk/gateway"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import type { LanguageModel } from "ai"; -const DEFAULT_MODELS: Record = { - openai: "gpt-4o-mini", - google: "gemini-2.5-flash", - anthropic: "claude-sonnet-4-20250514", - ollama: "llava:7b", -}; - -function getTestModel( - provider: string, - modelId: string, - ollamaUrl: string -): LanguageModel { - const resolvedModel = modelId || DEFAULT_MODELS[provider] || DEFAULT_MODELS.ollama; - - switch (provider) { - case "openai": - return openai(resolvedModel); - case "google": - return google(resolvedModel); - case "anthropic": - return anthropic(resolvedModel); - case "ollama": { - const baseURL = (ollamaUrl || process.env.OLLAMA_BASE_URL || "http://192.168.68.108:11434") + "/v1"; - const ollama = createOpenAICompatible({ name: "ollama", baseURL }); - return ollama.chatModel(resolvedModel); - } - default: - throw new Error(`Unknown provider: ${provider}`); +function getTestModel(provider: string, modelId: string, ollamaUrl: string): LanguageModel { + if (provider === "ollama") { + const baseURL = (ollamaUrl || process.env.OLLAMA_BASE_URL || "http://192.168.68.108:11434") + "/v1"; + const ollama = createOpenAICompatible({ name: "ollama", baseURL }); + return ollama.chatModel(modelId || "llava:7b"); } + + return gateway(modelId || "openai/gpt-4o-mini"); } export async function POST(request: NextRequest) { try { const body = await request.json().catch(() => ({})); - const provider = body.provider || "ollama"; + const provider = body.provider || "gateway"; const modelId = body.model || ""; const ollamaUrl = body.ollamaUrl || ""; diff --git a/src/app/cards/[id]/page.tsx b/src/app/cards/[id]/page.tsx index 71aa480..9872a42 100644 --- a/src/app/cards/[id]/page.tsx +++ b/src/app/cards/[id]/page.tsx @@ -4,7 +4,19 @@ import * as React from "react"; import { useParams, useRouter } from "next/navigation"; import Image from "next/image"; import { toast } from "sonner"; -import { ArrowLeft, ArrowRight, Check, Download, ImageIcon, ChevronDown, ChevronUp } from "lucide-react"; +import { + ArrowLeft, + ArrowRight, + Check, + Download, + ImageIcon, + ChevronDown, + ChevronUp, + ScanLine, + User, + ClipboardList, + Code, +} from "lucide-react"; import { Header } from "@/components/layout/header"; import { Button } from "@/components/ui/button"; @@ -140,10 +152,10 @@ export default function CardDetailPage() { if (loading) { return (
- -
- - + +
+ +
); @@ -152,8 +164,8 @@ export default function CardDetailPage() { if (!card) { return (
-
-
@@ -167,32 +179,33 @@ export default function CardDetailPage() { return (
-
- -
+
OCR: {ocrStatus} {reviewStatus} {confidence != null && ( = 50 && confidence <= 75 && "text-amber-600 border-amber-200", - confidence > 75 && "text-green-600 border-green-200", + "font-medium", + confidence < 50 && "text-red-600 border-red-300 dark:border-red-800", + confidence >= 50 && confidence <= 75 && "text-amber-600 border-amber-300 dark:border-amber-800", + confidence > 75 && "text-emerald-600 border-emerald-300 dark:border-emerald-800", )}> {Math.round(confidence)}% confidence @@ -200,47 +213,51 @@ export default function CardDetailPage() {
-
-
+
+
{reviewStatus !== "reviewed" && ( - )} {reviewStatus !== "exported" && ( - )} {hasEdits && ( - )}
-
- {/* Scanned Images */} - +
+ - Scanned Images + + + Scanned Images + -
+
- {/* Personal Info */} - + - Personal Information + + + Personal Information + -
+
setField("name", v)} /> setField("email", v)} /> setField("cellPhone", v)} /> @@ -250,15 +267,15 @@ export default function CardDetailPage() { setField("maritalStatus", v)} /> setField("visitType", v)} />
- -
+ +
setField("address", v)} /> setField("aptNumber", v)} /> setField("city", v)} /> setField("state", v)} /> setField("zip", v)} />
- +