-
-
-
-
+
+
+
+
+
+ {/* Upload Section */}
+
+
+
+
+
+
+
Upload Contract
-
- Add PDF contracts and let the AI pipeline extract summary,
- key points, and legal-business insights.
+
+ PDF documents supported
-
-
+
-
-
-
- Your Contracts
-
-
- Review contract lifecycle, trigger analysis, and ask AI
- questions per file.
-
+
+
+
+
+
+
+ New Document
+
+
+ Our AI pipeline will automatically extract summaries,
+ key clauses, risk factors, and generate actionable
+ business insights.
+
+
+
+
+
+
+
+ AI Ready
+
+
+
+
+
+
+
+ {/* Contracts List Section */}
+
+
+
+
+
+
+
+
+ Your Contracts
+
+
+ Manage, analyze, and query your document library
+
+
- {showContracts ? (
-
- ) : (
-
+ {showContracts && (
+
setRefreshTrigger((prev) => prev + 1)}
+ className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors px-3 py-1.5 rounded-lg hover:bg-muted/50"
+ >
+
+ Refresh
+
)}
-
-
+
+
+
+
+
+
+ {showContracts ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {/* Bottom Info Cards */}
+
+ {[
+ {
+ icon: ,
+ title: "Secure Processing",
+ desc: "Documents are encrypted in transit and at rest. Your data never leaves your infrastructure.",
+ color: "emerald",
+ },
+ {
+ icon: ,
+ title: "AI Extraction",
+ desc: "Advanced NLP models identify parties, obligations, risks, and key dates automatically.",
+ color: "primary",
+ },
+ {
+ icon: ,
+ title: "Instant Insights",
+ desc: "Get executive summaries and red-flag alerts within seconds of upload completion.",
+ color: "violet",
+ },
+ ].map((feature, i) => (
+
+
+ {feature.icon}
+
+
+ {feature.title}
+
+
+ {feature.desc}
+
+
+
+ ))}
+
-
-
- >
+
+
+
);
}
diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx
index cc7108e..9edb8d3 100644
--- a/app/(dashboard)/dashboard/page.tsx
+++ b/app/(dashboard)/dashboard/page.tsx
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
-import { motion } from "motion/react";
+import { motion, AnimatePresence } from "motion/react";
import {
Activity,
AlertTriangle,
@@ -16,6 +16,11 @@ import {
RefreshCw,
Sparkles,
TrendingUp,
+ Zap,
+ Shield,
+ Fingerprint,
+ ChevronRight,
+ Tag,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
@@ -24,9 +29,42 @@ import { checkDeadlineNotifications } from "@/features/notifications/api/notific
import dynamic from "next/dynamic";
// Dynamically import heavy charting libraries to dramatically improve initial load and rendering time
-const ContractStatusChart = dynamic(() => import("@/features/analytics/components/charts").then(mod => mod.ContractStatusChart), { ssr: false, loading: () =>
});
-const ContractTypeChart = dynamic(() => import("@/features/analytics/components/charts").then(mod => mod.ContractTypeChart), { ssr: false, loading: () =>
});
-const TrendChart = dynamic(() => import("@/features/analytics/components/charts").then(mod => mod.TrendChart), { ssr: false, loading: () =>
});
+const ContractStatusChart = dynamic(
+ () =>
+ import("@/features/analytics/components/charts").then(
+ (mod) => mod.ContractStatusChart,
+ ),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+const ContractTypeChart = dynamic(
+ () =>
+ import("@/features/analytics/components/charts").then(
+ (mod) => mod.ContractTypeChart,
+ ),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+const TrendChart = dynamic(
+ () =>
+ import("@/features/analytics/components/charts").then(
+ (mod) => mod.TrendChart,
+ ),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
interface DashboardStats {
totalContracts: number;
@@ -228,93 +266,110 @@ export default function DashboardPage() {
label: "Uploaded",
value: stats.uploadedContracts,
colorClass: "bg-amber-500",
+ icon:
,
},
{
label: "Processing",
value: stats.processingContracts,
- colorClass: "bg-primary",
+ colorClass: "bg-blue-500",
+ icon:
,
},
{
label: "Analyzed",
value: stats.analyzedContracts,
colorClass: "bg-emerald-500",
+ icon:
,
},
{
label: "Failed",
value: stats.failedContracts,
- colorClass: "bg-destructive",
+ colorClass: "bg-red-500",
+ icon:
,
},
];
if (isLoading) {
return (
-
-
-
-
-
- Building your analytics workspace...
-
-
+
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
);
}
return (
-
-
-
-
-
-
-
-
-
-
-
+
+ {/* Ambient Background */}
+
+
+ {/* Hero Section */}
+
+
+
+
-
-
+
+
Performance Overview
-
-
+
+
+
Financial Contracts Analytics
-
+
+
A reliable command center for uploaded documents, AI analysis
throughput, and portfolio quality across your BFSI workflow.
-
-
-
+
+
+
+
+
+
Live metrics
-
-
+
+
{isRefreshing
? "Syncing..."
: `Updated ${formatLastUpdated(lastUpdated)}`}
@@ -322,338 +377,415 @@ export default function DashboardPage() {
-
-
-
-
- Pipeline Snapshot
-
-
- {numberFormatter.format(stats.totalContracts)} files
-
-
-
-
-
-
+ {/* Pipeline Snapshot Card */}
+
+
+
+
-
-
-
-
-
Analyzed
-
- {numberFormatter.format(stats.analyzedContracts)}
-
+
+
+
+ Pipeline Snapshot
+
+
+ {numberFormatter.format(stats.totalContracts)}{" "}
+
+ files
+
+
+
+
+
+
-
-
Pending
-
- {numberFormatter.format(pendingContracts)}
-
-
-
-
-
void loadStats()}
- className="w-full rounded-xl"
- >
-
- Refresh
-
-
-
- Manage
-
-
-
-
-
+
+
+
+
+
+ Analyzed
+
+
+ {numberFormatter.format(stats.analyzedContracts)}
+
+
+
+
+ Pending
+
+
+ {numberFormatter.format(pendingContracts)}
+
+
+
+
+
+
void loadStats()}
+ className="w-full rounded-xl border-border/60 bg-background/40 backdrop-blur-xl hover:bg-background/60"
+ >
+
+ Refresh
+
+
+
+ Manage
+
+
+
+
+
+
-
-
-
-
-
- {numberFormatter.format(stats.totalContracts)}
-
-
- Uploaded into your workspace
-
-
+
+ {/* Bento Stats Grid */}
+
+ }
+ label="Total Files"
+ value={numberFormatter.format(stats.totalContracts)}
+ subtitle="Uploaded into your workspace"
+ gradient="from-primary/20 to-primary/5"
+ border="border-primary/20"
+ iconColor="text-primary"
+ delay={0}
+ />
+ }
+ label="Analyzed"
+ value={numberFormatter.format(stats.analyzedContracts)}
+ subtitle="Completed by AI pipeline"
+ gradient="from-emerald-500/20 to-emerald-500/5"
+ border="border-emerald-500/20"
+ iconColor="text-emerald-500"
+ progress={analyzedPercent}
+ progressColor="bg-emerald-500"
+ delay={0.05}
+ />
+ }
+ label="Pending Queue"
+ value={numberFormatter.format(pendingContracts)}
+ subtitle="Uploaded and processing files"
+ gradient="from-amber-500/20 to-amber-500/5"
+ border="border-amber-500/20"
+ iconColor="text-amber-500"
+ progress={pendingPercent}
+ progressColor="bg-amber-500"
+ delay={0.1}
+ />
+ }
+ label="Failed"
+ value={numberFormatter.format(stats.failedContracts)}
+ subtitle="Items needing re-analysis"
+ gradient="from-red-500/20 to-red-500/5"
+ border="border-red-500/20"
+ iconColor="text-red-500"
+ progress={failedPercent}
+ progressColor="bg-red-500"
+ delay={0.15}
+ />
+
-
-
-
- {numberFormatter.format(stats.analyzedContracts)}
-
-
- Completed by AI pipeline
-
-
-
+ {/* Pipeline Pulse + Premium Info */}
+
+
+
+
-
-
-
- {numberFormatter.format(pendingContracts)}
-
-
- Uploaded and processing files
-
-
-
+
+
+
+
+
+
+
+ Pipeline Pulse
+
+
+
+ {statusRows.map((row) => {
+ const rowPercent =
+ stats.totalContracts > 0
+ ? clampPercent((row.value / stats.totalContracts) * 100)
+ : 0;
-
-
-
- {numberFormatter.format(stats.failedContracts)}
-
-
- Items needing re-analysis
-
-
-
-
-
-
-
-
-
-
-
Pipeline Pulse
-
-
- {statusRows.map((row) => {
- const rowPercent =
- stats.totalContracts > 0
- ? clampPercent((row.value / stats.totalContracts) * 100)
- : 0;
-
- return (
-
-
-
{row.label}
-
- {numberFormatter.format(row.value)}
-
+ return (
+
+
+
+
+ {row.icon}
+
+ {row.label}
+
+
+ {numberFormatter.format(row.value)}
+
+
+
+
+
-
-
- );
- })}
+ );
+ })}
+
+
+
+
+
+
+ Success Rate
+
+
+ {stats.analysisRate}%
+
+
+ Completed vs total files
+
+
+
+
+ Avg Premium
+
+
+ {currencyFormatter.format(premiumInfo.averagePremium)}
+
+
+ Across {numberFormatter.format(premiumInfo.count)} analyzed
+ files
+
+
+
+
+ Total Premium
+
+
+ {currencyFormatter.format(premiumInfo.totalPremium)}
+
+
+ Portfolio value captured by AI
+
+
+
+
-
-
-
Success Rate
-
- {stats.analysisRate}%
-
-
- Completed vs total files
-
+ {/* AI Learning Telemetry */}
+
+
+
+
+
+
+
+
+
+
+
+ AI Learning Telemetry
+
-
-
Avg Premium
-
- {currencyFormatter.format(premiumInfo.averagePremium)}
-
-
- Across {numberFormatter.format(premiumInfo.count)} analyzed
- files
-
+
+
+ Score {aiLearningTelemetry.learningScore}/100
+
+
+
+
+ {[
+ {
+ label: "Completed Samples",
+ value: numberFormatter.format(
+ aiLearningTelemetry.completedSamples,
+ ),
+ sub: `${numberFormatter.format(aiLearningTelemetry.completedLast7Days)} in last 7 days`,
+ icon:
,
+ color: "emerald",
+ },
+ {
+ label: "Avg Summary Length",
+ value: numberFormatter.format(
+ aiLearningTelemetry.avgSummaryLength,
+ ),
+ sub: "characters",
+ icon:
,
+ color: "primary",
+ },
+ {
+ label: "Avg Extracted Text",
+ value: numberFormatter.format(
+ aiLearningTelemetry.avgExtractedTextLength,
+ ),
+ sub: "characters",
+ icon:
,
+ color: "blue",
+ },
+ {
+ label: "Avg Key Points",
+ value: aiLearningTelemetry.avgKeyPointsPerContract.toFixed(1),
+ sub: "items per analysis",
+ icon:
,
+ color: "violet",
+ },
+ ].map((item) => (
+
+
+
+ {item.icon}
+
+
+ {item.label}
+
+
+
+ {item.value}
+
+
+ {item.sub}
+
+
+ ))}
+
+
+
+
+
+ Learning quality index
+
+
+ {aiLearningTelemetry.learningScore}%
+
-
-
Total Premium
-
- {currencyFormatter.format(premiumInfo.totalPremium)}
-
-
- Portfolio value captured by AI
-
+
+
-
-
-
-
-
-
-
-
-
AI Learning Telemetry
-
-
-
- Score {aiLearningTelemetry.learningScore}/100
-
-
-
-
-
-
Completed Samples
-
- {numberFormatter.format(aiLearningTelemetry.completedSamples)}
-
-
- {numberFormatter.format(aiLearningTelemetry.completedLast7Days)}{" "}
- in last 7 days
+
+ {aiLearningTelemetry.improvementHint}
-
-
-
- Avg Summary Length
-
-
- {numberFormatter.format(aiLearningTelemetry.avgSummaryLength)}
-
-
characters
-
-
-
-
- Avg Extracted Text
-
-
- {numberFormatter.format(
- aiLearningTelemetry.avgExtractedTextLength,
- )}
-
-
characters
-
-
-
-
Avg Key Points
-
- {aiLearningTelemetry.avgKeyPointsPerContract.toFixed(1)}
-
-
- items per analysis
-
-
-
-
-
-
- Learning quality index
- {aiLearningTelemetry.learningScore}%
-
-
-
- {aiLearningTelemetry.improvementHint}
-
-
-
+
+
{hasChartData ? (
-
+
{chartData && chartData.trends.length > 0 && (
-
-
-
-
+
+
+
+
+
+
+
Upload Trend (30 days)
@@ -664,10 +796,15 @@ export default function DashboardPage() {
)}
{chartData && chartData.byStatus.length > 0 && (
-
-
-
-
Processing Status
+
+
+
+
+
+
+
+ Processing Status
+
0 && (
-
-
-
-
+
+
+
+
+
+
+
Contract Type Distribution
@@ -694,47 +834,68 @@ export default function DashboardPage() {
)}
-
-
-
-
Recent Analyses
+
+
+
+
+
+
+
+ Recent Analyses
+
{recentContracts.length > 0 ? (
- {recentContracts.map((contract) => (
-
-
- {contract.title || "Untitled contract"}
-
-
- {contract.type || "Unknown type"}
-
- {new Date(contract.createdAt).toLocaleDateString(
- "en-US",
- {
- month: "short",
- day: "numeric",
- },
- )}
-
-
-
- Premium:{" "}
- {contract.premium !== null
- ? currencyFormatter.format(contract.premium)
- : "Not detected"}
-
-
- ))}
+
+ {recentContracts.map((contract, idx) => (
+
+
+
+ {contract.title || "Untitled contract"}
+
+
+
+
+
+
+ {contract.type || "Unknown type"}
+
+
+
+ {new Date(contract.createdAt).toLocaleDateString(
+ "en-US",
+ {
+ month: "short",
+ day: "numeric",
+ },
+ )}
+
+
+
+ Premium:{" "}
+ {contract.premium !== null
+ ? currencyFormatter.format(contract.premium)
+ : "Not detected"}
+
+
+ ))}
+
) : (
-
+
-
+
+
No recent analyses yet
@@ -744,72 +905,109 @@ export default function DashboardPage() {
)}
-
+
) : (
-
-
-
-
-
-
-
-
-
-
- Your analytics will appear here
-
-
- Upload and analyze contracts to unlock trend and distribution
- charts.
-
-
-
- Upload first contract
-
-
-
-
-
+
+
+
+
+
+ Your analytics will appear here
+
+
+ Upload and analyze contracts to unlock trend and distribution
+ charts.
+
+
+
+ Upload first contract
+
+
+
+
+
+
)}
);
}
+
+// ─────────────────────────────────────────────────
+// Bento Stat Sub-Component
+// ─────────────────────────────────────────────────
+
+function BentoStat({
+ icon,
+ label,
+ value,
+ subtitle,
+ gradient,
+ border,
+ iconColor,
+ progress,
+ progressColor,
+ delay,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+ subtitle: string;
+ gradient: string;
+ border: string;
+ iconColor: string;
+ progress?: number;
+ progressColor?: string;
+ delay: number;
+}) {
+ return (
+
+
+
+
+
+
+
+
+ {label}
+
+
+ {value}
+
+
{subtitle}
+
+ {progress !== undefined && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/app/(dashboard)/not-found.tsx b/app/(dashboard)/not-found.tsx
new file mode 100644
index 0000000..b634768
--- /dev/null
+++ b/app/(dashboard)/not-found.tsx
@@ -0,0 +1,5 @@
+import { InvalidRouteScreen } from "@/components/layout/invalid-route-screen";
+
+export default function NotFound() {
+ return ;
+}
\ No newline at end of file
diff --git a/app/api/contracts/[id]/download/route.ts b/app/api/contracts/[id]/download/route.ts
new file mode 100644
index 0000000..c8ef667
--- /dev/null
+++ b/app/api/contracts/[id]/download/route.ts
@@ -0,0 +1,53 @@
+import { auth } from "@clerk/nextjs/server";
+import { ContractService } from "@/lib/services/contract.service";
+
+const sanitizeFilename = (fileName: string): string => {
+ const cleaned = fileName.replace(/[\\/:*?"<>|]/g, "_").trim();
+ return cleaned || "contract";
+};
+
+export async function GET(
+ _: Request,
+ { params }: { params: Promise<{ id: string }> },
+) {
+ try {
+ const { userId: clerkId } = await auth();
+ if (!clerkId) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+
+ const { id } = await params;
+ if (!id) {
+ return new Response("Missing contract ID", { status: 400 });
+ }
+
+ const contract = await ContractService.getById(id);
+
+ const upstream = await fetch(contract.fileUrl);
+ if (!upstream.ok) {
+ return new Response("Unable to fetch source file", { status: 502 });
+ }
+
+ const bytes = await upstream.arrayBuffer();
+ const contentType =
+ contract.mimeType ||
+ upstream.headers.get("content-type") ||
+ "application/octet-stream";
+ const fileName = sanitizeFilename(contract.fileName);
+ const encodedFileName = encodeURIComponent(fileName);
+
+ return new Response(bytes, {
+ status: 200,
+ headers: {
+ "Content-Type": contentType,
+ "Content-Disposition": `attachment; filename="${fileName}"; filename*=UTF-8''${encodedFileName}`,
+ "Cache-Control": "private, no-store, no-cache, must-revalidate",
+ Pragma: "no-cache",
+ Expires: "0",
+ },
+ });
+ } catch (error) {
+ console.error("Contract download error:", error);
+ return new Response("Failed to download contract", { status: 500 });
+ }
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 0000000..b634768
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,5 @@
+import { InvalidRouteScreen } from "@/components/layout/invalid-route-screen";
+
+export default function NotFound() {
+ return ;
+}
\ No newline at end of file
diff --git a/components/layout/invalid-route-screen.tsx b/components/layout/invalid-route-screen.tsx
new file mode 100644
index 0000000..913a961
--- /dev/null
+++ b/components/layout/invalid-route-screen.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import Link from "next/link";
+import { Home, Compass } from "lucide-react";
+import { motion } from "motion/react";
+
+import { Button } from "@/components/ui/button";
+
+export function InvalidRouteScreen() {
+ return (
+
+ {/* Ambient Background */}
+
+
+
+ {/* 404 Code */}
+
+
+
+ 404
+
+
+
+ {/* Content */}
+
+
+
+
+ Page not found
+
+
+
+ This page doesn't exist
+
+
+
+ The URL you entered doesn't match any known route. Double-check
+ the address or return to the homepage.
+
+
+
+ {/* Action */}
+
+
+
+
+ Back to Home
+
+
+
+
+ {/* Decorative footer */}
+
+
+
+ LexiChain
+
+
+
+
+
+
+ );
+}
diff --git a/docs/blockchain-module.md b/docs/blockchain-module.md
index cf9f1c7..73b57bc 100644
--- a/docs/blockchain-module.md
+++ b/docs/blockchain-module.md
@@ -31,6 +31,7 @@
### The Problem (in simple terms)
When a client sends an insurance claim or uploads a contract, they need **proof** that they submitted it on a specific date. Without proof:
+
- The insurance company could claim "we never received it"
- Deadlines could be disputed
- There's no transparency
@@ -40,6 +41,7 @@ When a client sends an insurance claim or uploads a contract, they need **proof*
A **blockchain** is like a public, tamper-proof notebook. Once you write something in it, **nobody can erase or modify it** — not even the person who wrote it.
We use the blockchain as a **digital notary**:
+
1. We take the uploaded contract PDF
2. We create a unique **fingerprint** (hash) of that file
3. We write that fingerprint into the blockchain with a timestamp
@@ -50,11 +52,13 @@ We use the blockchain as a **digital notary**:
### What is a Smart Contract?
A **smart contract** is a program that runs on the blockchain. Think of it as a vending machine:
+
- You put in a coin (send a transaction)
- The machine executes its programmed logic
- The result is permanent and visible to everyone
Our smart contract (`DocumentRegistry.sol`) has two main functions:
+
- **Register**: Store a document fingerprint with a timestamp
- **Verify**: Check if a fingerprint exists and when it was stored
@@ -64,14 +68,14 @@ Our smart contract (`DocumentRegistry.sol`) has two main functions:
### Features Implemented
-| Feature | Description |
-|---------|-------------|
-| **Auto-Registration** | After AI analyzes a contract, its hash is automatically registered on-chain |
-| **Manual Registration** | Users can register unregistered contracts via the Blockchain Explorer |
-| **Document Verification** | Paste any document hash to check if it exists on-chain |
-| **Transaction Explorer** | View all blockchain transactions with details |
-| **Network Stats** | Live stats: verified documents, latest block, network status |
-| **Proof Badges** | Contract list shows which contracts are blockchain-verified |
+| Feature | Description |
+| ------------------------- | --------------------------------------------------------------------------- |
+| **Auto-Registration** | After AI analyzes a contract, its hash is automatically registered on-chain |
+| **Manual Registration** | Users can register unregistered contracts via the Blockchain Explorer |
+| **Document Verification** | Paste any document hash to check if it exists on-chain |
+| **Transaction Explorer** | View all blockchain transactions with details |
+| **Network Stats** | Live stats: verified documents, latest block, network status |
+| **Proof Badges** | Contract list shows which contracts are blockchain-verified |
### What Happens When a User Uploads a Contract?
@@ -80,6 +84,7 @@ User uploads PDF → AI analyzes it → Blockchain registers the hash
```
The entire flow is automatic. The user doesn't need:
+
- ❌ MetaMask or any wallet
- ❌ Cryptocurrency knowledge
- ❌ To pay anything
@@ -129,10 +134,10 @@ flowchart TD
### Network Modes
-| Mode | When | URL | Cost |
-|------|------|-----|------|
-| **Hardhat** | Development | `http://127.0.0.1:8545` | Free (local) |
-| **Sepolia** | Demo/Presentation | Via Alchemy/Infura RPC | Free (testnet) |
+| Mode | When | URL | Cost |
+| ----------- | ----------------- | ----------------------- | -------------- |
+| **Hardhat** | Development | `http://127.0.0.1:8545` | Free (local) |
+| **Sepolia** | Demo/Presentation | Via Alchemy/Infura RPC | Free (testnet) |
The mode is controlled by a single env variable: `BLOCKCHAIN_NETWORK`.
@@ -178,12 +183,14 @@ flowchart LR
```
#### `registerDocument(bytes32 _docHash)`
+
- **Purpose**: Store a document hash on-chain
- **Access**: Only the contract owner (our server wallet)
- **Guard**: Prevents duplicate registration (same hash can't be registered twice)
- **Event**: Emits `DocumentRegistered` for off-chain indexing
#### `verifyDocument(bytes32 _docHash)`
+
- **Purpose**: Check if a hash exists and get its details
- **Cost**: Free (read-only, no gas)
- **Returns**: `(exists, timestamp, depositor)`
@@ -231,6 +238,7 @@ flowchart LR
### Why Server-Side?
Most blockchain dApps require users to install MetaMask and sign transactions. This is bad UX for a BFSI enterprise platform because:
+
- Users shouldn't need crypto knowledge
- The platform manages documents, not individual users
- Server-side signing is more reliable
@@ -260,14 +268,14 @@ sequenceDiagram
### Key Methods
-| Method | Purpose | Gas Cost |
-|--------|---------|----------|
-| `hashDocument(fileUrl)` | Download file + compute SHA-256 | None (off-chain) |
-| `registerOnChain(hash, fileName)` | Send tx to smart contract | ~50,000 gas |
-| `verifyOnChain(hash)` | Read-only check | Free |
-| `hashAndRegister(fileUrl, fileName)` | Combined: hash + register | ~50,000 gas |
-| `getNetworkStats()` | Get block number, total docs | Free |
-| `isConfigured()` | Check if env vars are set | None |
+| Method | Purpose | Gas Cost |
+| ------------------------------------ | ------------------------------- | ---------------- |
+| `hashDocument(fileUrl)` | Download file + compute SHA-256 | None (off-chain) |
+| `registerOnChain(hash, fileName)` | Send tx to smart contract | ~50,000 gas |
+| `verifyOnChain(hash)` | Read-only check | Free |
+| `hashAndRegister(fileUrl, fileName)` | Combined: hash + register | ~50,000 gas |
+| `getNetworkStats()` | Get block number, total docs | Free |
+| `isConfigured()` | Check if env vars are set | None |
### Graceful Degradation
@@ -424,6 +432,7 @@ sequenceDiagram
participant SA as Server Action
participant AI as AI Service
participant BS as BlockchainService
+ participant ES as EmailService
participant SC as Smart Contract
participant DB as PostgreSQL
@@ -446,6 +455,8 @@ sequenceDiagram
SA->>DB: Save txHash, blockNumber, etc.
SA->>DB: Create BlockchainTransaction
+ SA->>ES: Send analysis + blockchain proof email
+ ES-->>U: Email received (or Ethereal preview in dev)
SA-->>UI: Success!
Note over U,UI: User visits /blockchain
@@ -470,6 +481,7 @@ sequenceDiagram
## 10. How to Run Locally
### Prerequisites
+
- Node.js installed
- The Next.js app running (`npm run dev`)
@@ -562,6 +574,7 @@ npx hardhat run scripts/deploy.ts --network sepolia
### Step 5: Verify on Etherscan
After deploying, transactions will have real Etherscan links:
+
```
https://sepolia.etherscan.io/tx/0x...
```
@@ -570,19 +583,20 @@ https://sepolia.etherscan.io/tx/0x...
## 12. Technology Choices & Rationale
-| Technology | Why We Chose It |
-|-----------|----------------|
-| **Solidity 0.8.24** | Latest stable version with built-in overflow protection |
-| **Hardhat** | Industry standard for Solidity development, free local blockchain |
-| **ethers.js v6** | Modern, lightweight, TypeScript-native Ethereum library |
-| **SHA-256** | Standard cryptographic hash, deterministic, collision-resistant |
-| **Server-side wallet** | Users don't need MetaMask; enterprise-grade UX |
-| **Sepolia testnet** | Official Ethereum testnet, free, has Etherscan explorer |
-| **Graceful degradation** | Blockchain is optional; app works perfectly without it |
+| Technology | Why We Chose It |
+| ------------------------ | ----------------------------------------------------------------- |
+| **Solidity 0.8.24** | Latest stable version with built-in overflow protection |
+| **Hardhat** | Industry standard for Solidity development, free local blockchain |
+| **ethers.js v6** | Modern, lightweight, TypeScript-native Ethereum library |
+| **SHA-256** | Standard cryptographic hash, deterministic, collision-resistant |
+| **Server-side wallet** | Users don't need MetaMask; enterprise-grade UX |
+| **Sepolia testnet** | Official Ethereum testnet, free, has Etherscan explorer |
+| **Graceful degradation** | Blockchain is optional; app works perfectly without it |
### Why NOT Web3j / Java?
The original project spec suggested Web3j (Java library). We chose ethers.js instead because:
+
1. Our backend is **Next.js/TypeScript**, not Spring Boot
2. ethers.js has **better TypeScript support** and is more actively maintained
3. Both libraries do the same job — interact with Ethereum — but ethers.js is native to our stack
@@ -592,64 +606,70 @@ The original project spec suggested Web3j (Java library). We chose ethers.js ins
## 13. File Reference
### Smart Contract Layer
-| File | Purpose |
-|------|---------|
+
+| File | Purpose |
+| ------------------------------------------- | ----------------------- |
| `blockchain/contracts/DocumentRegistry.sol` | Solidity smart contract |
-| `blockchain/test/DocumentRegistry.test.ts` | 14 comprehensive tests |
-| `blockchain/scripts/deploy.ts` | Deployment script |
-| `blockchain/hardhat.config.ts` | Hardhat configuration |
-| `blockchain/package.json` | Hardhat dependencies |
+| `blockchain/test/DocumentRegistry.test.ts` | 14 comprehensive tests |
+| `blockchain/scripts/deploy.ts` | Deployment script |
+| `blockchain/hardhat.config.ts` | Hardhat configuration |
+| `blockchain/package.json` | Hardhat dependencies |
### Service Layer
-| File | Purpose |
-|------|---------|
+
+| File | Purpose |
+| ------------------------------------ | ---------------------------- |
| `lib/services/blockchain.service.ts` | Core blockchain interactions |
-| `lib/services/blockchain.types.ts` | TypeScript type definitions |
+| `lib/services/blockchain.types.ts` | TypeScript type definitions |
### Server Actions
-| File | Purpose |
-|------|---------|
-| `features/blockchain/api/blockchain.action.ts` | Blockchain server actions |
-| `features/contracts/api/contract.action.ts` | Updated with auto-registration |
+
+| File | Purpose |
+| ---------------------------------------------- | ------------------------------ |
+| `features/blockchain/api/blockchain.action.ts` | Blockchain server actions |
+| `features/contracts/api/contract.action.ts` | Updated with auto-registration |
### Frontend
-| File | Purpose |
-|------|---------|
-| `app/(dashboard)/blockchain/page.tsx` | Blockchain Explorer page |
-| `app/(dashboard)/blockchain/layout.tsx` | Page metadata |
-| `components/layout/navigation.tsx` | Updated with blockchain link |
+
+| File | Purpose |
+| --------------------------------------- | ---------------------------- |
+| `app/(dashboard)/blockchain/page.tsx` | Blockchain Explorer page |
+| `app/(dashboard)/blockchain/layout.tsx` | Page metadata |
+| `components/layout/navigation.tsx` | Updated with blockchain link |
### Database
-| File | Purpose |
-|------|---------|
+
+| File | Purpose |
+| ---------------------- | ------------------------------ |
| `prisma/schema.prisma` | Updated with blockchain fields |
### Configuration
-| File | Purpose |
-|------|---------|
-| `.env` | Blockchain env vars |
-| `.env.example` | Template for new developers |
-| `.gitignore` | Blockchain artifacts excluded |
+
+| File | Purpose |
+| -------------- | ----------------------------- |
+| `.env` | Blockchain env vars |
+| `.env.example` | Template for new developers |
+| `.gitignore` | Blockchain artifacts excluded |
---
## Glossary
-| Term | Definition |
-|------|-----------|
-| **Hash** | A fixed-size fingerprint of data. Same input → same output. |
-| **SHA-256** | A specific hash algorithm producing 256-bit (32-byte) outputs |
-| **Smart Contract** | A program stored on the blockchain that executes automatically |
-| **Gas** | The fee for executing operations on Ethereum (free on testnet) |
-| **Block** | A batch of transactions grouped together on the blockchain |
-| **Transaction (Tx)** | A single operation on the blockchain (e.g., registering a hash) |
-| **Tx Hash** | A unique identifier for a transaction (like a receipt number) |
-| **Block Number** | The sequential number of the block containing a transaction |
-| **Block Timestamp** | The time the block was created (proof of when the tx happened) |
-| **Private Key** | Secret key used to sign transactions (like a password) |
-| **Address** | Public identifier derived from the private key (like a username) |
-| **ABI** | Application Binary Interface — the "API spec" of a smart contract |
-| **Hardhat** | Development tool for writing, testing, and deploying smart contracts |
-| **Sepolia** | Ethereum test network for free experimentation |
-| **ethers.js** | JavaScript library for interacting with the Ethereum blockchain |
-| **Faucet** | A service that gives free test ETH for development |
+| Term | Definition |
+| -------------------- | -------------------------------------------------------------------- |
+| **Hash** | A fixed-size fingerprint of data. Same input → same output. |
+| **SHA-256** | A specific hash algorithm producing 256-bit (32-byte) outputs |
+| **Smart Contract** | A program stored on the blockchain that executes automatically |
+| **Gas** | The fee for executing operations on Ethereum (free on testnet) |
+| **Block** | A batch of transactions grouped together on the blockchain |
+| **Transaction (Tx)** | A single operation on the blockchain (e.g., registering a hash) |
+| **Tx Hash** | A unique identifier for a transaction (like a receipt number) |
+| **Block Number** | The sequential number of the block containing a transaction |
+| **Block Timestamp** | The time the block was created (proof of when the tx happened) |
+| **Private Key** | Secret key used to sign transactions (like a password) |
+| **Address** | Public identifier derived from the private key (like a username) |
+| **ABI** | Application Binary Interface — the "API spec" of a smart contract |
+| **Hardhat** | Development tool for writing, testing, and deploying smart contracts |
+| **Sepolia** | Ethereum test network for free experimentation |
+| **ethers.js** | JavaScript library for interacting with the Ethereum blockchain |
+| **Faucet** | A service that gives free test ETH for development |
diff --git a/docs/lexichain-full-system.md b/docs/lexichain-full-system.md
new file mode 100644
index 0000000..14a060e
--- /dev/null
+++ b/docs/lexichain-full-system.md
@@ -0,0 +1,80 @@
+# LexiChain: Intelligent BFSI Contract Management System
+
+## 🌟 The Platform Vision
+**LexiChain** is a state-of-the-art enterprise platform designed specifically for the **BFSI** (Banking, Financial Services, and Insurance) sector. The core objective is to solve the historical problem of "Information Silos" and "Trust Gaps" in contract management.
+
+In the traditional world, insurance policies and bank loans are long, complex, and opaque. LexiChain uses **Generative AI** to make these documents "conversational" and **Blockchain Technology** to make them "tamper-proof."
+
+---
+
+## 🏗️ System Architecture
+The platform follows a **Modular Full-Stack Architecture** designed for scalability, security, and high performance. It is divided into three distinct layers:
+
+### 1. The Presentation Layer (Frontend)
+Built with **React and Next.js**, the interface provides a "Premium Executive" experience. It is fully responsive, theme-aware, and designed for high-density information display. It uses **Server Components** for fast loading and **Client Components** for interactive elements like the AI Chat and Blockchain Explorer.
+
+### 2. The Intelligence & Processing Layer (Backend)
+This is the "Brain" of LexiChain. It handles:
+* **Authentication**: Managed by **Clerk**, providing enterprise-grade security and multi-factor authentication.
+* **File Orchestration**: Securely handling document uploads and cloud storage.
+* **AI Pipeline**: Converting raw PDF data into structured knowledge.
+* **Blockchain Bridge**: Acting as a middleware between the web app and the decentralized network.
+
+### 3. The Persistence Layer (Database)
+We use a **PostgreSQL** database managed by **Prisma ORM**. This stores all user metadata, contract details, and the historical "Audit Trail" of blockchain transactions.
+
+---
+
+## 🤖 Core Pillar 1: AI & Retrieval-Augmented Generation (RAG)
+LexiChain doesn't just "read" your contracts; it "understands" them. We implement a pattern called **RAG (Retrieval-Augmented Generation)**.
+
+### How it works:
+1. **Ingestion & Parsing**: When a contract is uploaded, our AI service (powered by **Google Gemini**) breaks the document down into small "semantic chunks."
+2. **Vector Indexing**: These chunks are indexed based on their meaning.
+3. **Contextual Retrieval**: When you ask a question like *"Does this policy cover water damage?"*, the system doesn't search for keywords. It searches for **Concepts**.
+4. **Informed Response**: The AI retrieves the relevant sections of your contract and uses them as "facts" to generate a precise, grounded answer. This eliminates "hallucinations" and ensures 100% accuracy based on your actual document.
+
+---
+
+## 🔗 Core Pillar 2: The Blockchain Trust Layer
+In the BFSI industry, "when" and "what" was signed is everything. LexiChain uses an **Ethereum-based Smart Contract** to establish absolute trust.
+
+### The Problem it Solves:
+If a user and a bank have a dispute, the bank could theoretically change the digital contract in their database. LexiChain prevents this through **Immutable Proof-of-Deposit**.
+
+### Key Concepts Implemented:
+* **Cryptographic Fingerprinting (Hashing)**: We generate a unique SHA-256 hash of the contract. This fingerprint is mathematically tied to every single character in the document.
+* **Smart Contract Execution**: The platform automatically sends this fingerprint to a **Solidity Smart Contract** on the blockchain.
+* **Immutable Timestamping**: Once the transaction is "mined," it is given a permanent timestamp by the network. This provides an **indisputable proof** that the document existed in that exact state on that specific date.
+* **Decentralized Verification**: Anyone with the file can verify it against the blockchain record. If even one comma is changed in the PDF, the verification will fail.
+
+---
+
+## 🔄 The Integrated Workflow (The App Journey)
+1. **Upload**: The user securely uploads a contract (Insurance policy, Loan agreement, etc.).
+2. **AI Extraction**: The AI immediately extracts key data points (Expiration date, Total value, Involved parties) to populate the dashboard.
+3. **Semantic Indexing**: The document is prepared for the RAG-based Chat interface.
+4. **On-Chain Registration**: Simultaneously, the system computes the document's hash and registers it on the blockchain.
+5. **Interaction**: The user can now "Chat" with their document or verify its "Blockchain Status" via the Explorer.
+
+---
+
+## 🛠️ The Technology Stack
+* **Frontend/Backend Framework**: Next.js 15+ (App Router).
+* **Styling**: TailwindCSS with Custom Framer Motion animations.
+* **Database**: PostgreSQL with Prisma ORM.
+* **AI Engine**: Google Gemini Pro (Vision & Text).
+* **Blockchain Environment**: Hardhat (Local) & Sepolia (Public Testnet).
+* **Smart Contract Language**: Solidity 0.8.24.
+* **Blockchain Integration**: Ethers.js v6.
+* **File Storage**: UploadThing.
+* **Security/Auth**: Clerk Auth.
+
+---
+
+## 📈 Software Engineering Principles Used
+* **Separation of Concerns**: The AI, Blockchain, and Core Business logic are kept in separate services to prevent "God Objects."
+* **Idempotency**: Blockchain registrations are designed to be idempotent (you can't register the same hash twice).
+* **Graceful Degradation**: If the blockchain network is down, the AI and Core App features continue to work normally.
+* **Data Integrity**: Using SHA-256 ensures that the data being audited is exactly the data that was signed.
+* **Scalability**: The RAG architecture allows the system to handle thousands of documents without slowing down the AI responses.
diff --git a/docs/project-technical-overview.md b/docs/project-technical-overview.md
new file mode 100644
index 0000000..e501038
--- /dev/null
+++ b/docs/project-technical-overview.md
@@ -0,0 +1,85 @@
+# LexiChain — Technical Platform Overview
+
+## 1. Executive Summary: What is LexiChain?
+**LexiChain** is an advanced intelligence platform specifically designed for the **BFSI** (Banking, Financial Services, and Insurance) sector. It transforms complex, opaque legal documents into interactive, actionable data using a combination of **Generative AI** and **Blockchain Technology**.
+
+The core mission of LexiChain is to solve the "Black Box" problem in contracts: where clients and institutions often sign long documents without fully understanding the hidden risks, obligations, or deadlines.
+
+---
+
+## 2. The Core Problem & Solution
+### The Problem
+* **Cognitive Overload**: Insurance and banking contracts are filled with "Legalese"—dense, technical language that is difficult for non-experts to parse.
+* **Lack of Trust**: There is no easy way to prove that a document hasn't been modified after signing.
+* **Static Data**: Traditional PDFs are "dead" files. You cannot ask a PDF a question like *"What happens if I miss a payment by 3 days?"*
+
+### The LexiChain Solution
+LexiChain creates a **"Living Document"** environment. It uses AI to extract meaning and Blockchain to guarantee integrity, allowing users to converse with their contracts in natural language.
+
+---
+
+## 3. System Architecture
+LexiChain is built using a modern **Distributed Architecture** composed of four primary layers:
+
+### A. The Client Layer (Frontend)
+Built with **Next.js 15** and **Tailwind CSS**. It focuses on **User Experience (UX)**, providing a dashboard that works seamlessly on both desktop and mobile. It handles the secure transmission of files to the backend.
+
+### B. The Application Layer (Backend)
+This is the "Brain" of the system, powered by **Next.js Server Actions**. It coordinates the flow of data between the user, the database, the AI models, and the blockchain network. It manages authentication, file storage, and the processing pipeline.
+
+### C. The Intelligence Layer (AI & RAG)
+This layer uses **Gemini 1.5 Pro** and **Mistral AI** for high-speed analysis. Instead of just "reading" text, it uses a **Vector Database** to perform Retrieval-Augmented Generation (RAG), ensuring the AI answers only based on the specific facts found in the uploaded contract.
+
+### D. The Trust Layer (Blockchain)
+A decentralized layer powered by **Ethereum/Hardhat**. It creates a unique cryptographic "fingerprint" (hash) for every contract. Once recorded, this fingerprint becomes an immutable proof of the document's existence and original state.
+
+---
+
+## 4. How the Application Works (The Pipeline)
+
+1. **Intake**: The user uploads a contract (PDF/Image).
+2. **OCR & Parsing**: The system converts the document into machine-readable text.
+3. **Semantic Chunking**: The text is broken down into small "concepts" or chunks.
+4. **AI Analysis**: The AI extracts key metadata (Dates, Parties, Obligations, Risks).
+5. **Blockchain Certification**: The document hash is sent to a Smart Contract to lock in the "Proof of Deposit."
+6. **RAG Indexing**: The chunks are stored in a specialized index for the Chat interface.
+7. **Interaction**: The user can now ask questions, view the blockchain proof, or check their dashboard for upcoming contract deadlines.
+
+---
+
+## 5. Deep Dive: RAG (Retrieval-Augmented Generation)
+### What is it?
+In simple terms, RAG is like giving the AI a **"Open Book Exam."**
+
+Most AI models rely on what they learned during training (which might be old or generic). With RAG, when you ask a question, the system first **searches** your specific contract for the relevant paragraphs, **retrieves** them, and then **gives** them to the AI to summarize.
+
+### Why use it in BFSI?
+* **Zero Hallucination**: The AI is forbidden from "guessing." If the answer isn't in your contract, it says "I don't know."
+* **Contextual Accuracy**: It understands the difference between a "Home Loan" in 2010 vs. a "Car Insurance" in 2024 because it only looks at the specific context of your file.
+
+---
+
+## 6. Deep Dive: Blockchain & Trust
+### The Digital Notary
+In the BFSI world, dates and integrity are everything. If a claim is denied because of a "deadline," the user needs proof that they held the document on time.
+
+### How it works technically:
+1. **Hashing**: We turn your PDF into a 64-character string called a "Hash." Even changing a single comma in the PDF would result in a completely different hash.
+2. **Immutability**: Once this hash is written into our **Solidity Smart Contract**, it can never be deleted or changed by anyone—not even the platform administrators.
+3. **Verification**: At any time, a user can "Verify" their document. The system re-hashes the file and compares it to the blockchain. If they match, the document is **Genuine**.
+
+---
+
+## 7. The Technology Stack (Summary)
+
+* **Frontend**: Next.js (React), Tailwind CSS, Lucide Icons, Framer Motion.
+* **Backend**: TypeScript, Prisma ORM, Server Actions.
+* **Database**: PostgreSQL (Neon) for metadata, Vector Storage for AI.
+* **AI**: Google Gemini (Large Language Model), Mistral AI (Fallback with Pixtral Vision).
+* **Blockchain**: Solidity (Smart Contracts), Hardhat (Local Node), Ethers.js (Integration).
+* **Storage**: UploadThing (Secure File Hosting).
+
+---
+
+## 8. Conclusion
+LexiChain is not just a document viewer; it is a **Decision Support System**. By combining the analytical power of AI with the structural trust of Blockchain, it bridges the gap between complex legal documents and clear, verifiable human understanding.
diff --git a/features/analytics/components/charts.tsx b/features/analytics/components/charts.tsx
index 157cb95..954b379 100644
--- a/features/analytics/components/charts.tsx
+++ b/features/analytics/components/charts.tsx
@@ -23,23 +23,57 @@ type StatusData = Array<{ name: string; count: number }>;
const PIE_COLORS: Record = {
Uploaded: "hsl(38 92% 50%)",
- Processing: "hsl(var(--primary))",
+ Processing: "hsl(217 91% 60%)",
Analyzed: "hsl(160 84% 39%)",
- Failed: "hsl(var(--destructive))",
+ Failed: "hsl(0 84% 60%)",
};
const FALLBACK_COLORS = [
- "hsl(var(--primary))",
- "hsl(var(--secondary))",
- "hsl(var(--accent))",
- "hsl(var(--destructive))",
+ "hsl(217 91% 60%)",
+ "hsl(260 89% 65%)",
+ "hsl(190 85% 50%)",
+ "hsl(340 82% 52%)",
];
const tooltipStyle = {
- backgroundColor: "hsl(var(--background))",
- border: "1px solid hsl(var(--border))",
- borderRadius: "12px",
+ backgroundColor: "hsl(var(--background) / 0.95)",
+ border: "1px solid hsl(var(--border) / 0.6)",
+ borderRadius: "16px",
color: "hsl(var(--foreground))",
+ backdropFilter: "blur(12px)",
+ boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
+ padding: "12px 16px",
+ fontSize: "13px",
+};
+
+const CustomTooltip = ({ active, payload, label }: any) => {
+ if (!active || !payload?.length) return null;
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+ {payload.map((entry: any, index: number) => (
+
+
+
+ {entry.name}
+
+
+ {typeof entry.value === "number"
+ ? entry.value.toLocaleString()
+ : entry.value}
+
+
+ ))}
+
+ );
};
export function TrendChart({ data }: { data: TrendData }) {
@@ -72,64 +106,82 @@ export function TrendChart({ data }: { data: TrendData }) {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {
- const numericValue = Number(value ?? 0);
- if (name === "movingAverage") {
- return [numericValue.toFixed(1), "7-day avg"];
- }
-
- return [numericValue, "Uploads"];
- }}
- />
+ } />
@@ -152,43 +204,65 @@ export function ContractTypeChart({ data }: { data: TypeData }) {
layout="vertical"
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
[
- Number(value ?? 0),
- "Files",
- ]}
+ content={ }
+ cursor={{ fill: "hsl(var(--muted) / 0.15)", radius: 8 }}
/>
-
+
{sortedData.map((item, index) => {
- const opacity = Math.max(0.35, 0.95 - index * 0.12);
+ const gradients = [
+ "url(#barGradient)",
+ "url(#barGradient2)",
+ "url(#barGradient3)",
+ "url(#barGradient4)",
+ ];
return (
|
);
})}
@@ -210,16 +284,26 @@ export function ContractStatusChart({ data }: { data: StatusData }) {
+
+
+
+
+
+
+
+
+
{data.map((entry, index) => (
|
))}
{total > 0 && (
- {total}
+ {total.toLocaleString()}
Files
)}
- [
- Number(value ?? 0),
- "Files",
- ]}
- />
+ } />
-
+
{data.map((item, index) => {
const color =
PIE_COLORS[item.name] ??
@@ -274,16 +354,16 @@ export function ContractStatusChart({ data }: { data: StatusData }) {
return (
-
+
{item.name}
-
+
{item.count}
diff --git a/features/contracts/api/contract.action.ts b/features/contracts/api/contract.action.ts
index 91185ce..f844176 100644
--- a/features/contracts/api/contract.action.ts
+++ b/features/contracts/api/contract.action.ts
@@ -20,6 +20,7 @@
"use server";
import { auth } from "@clerk/nextjs/server";
+import { clerkClient } from "@clerk/nextjs/server";
import { revalidatePath } from "next/cache";
import {
ContractService,
@@ -29,6 +30,7 @@ import { AIService } from "@/lib/services/ai.service";
import { RAGService } from "@/lib/services/rag.service";
import { NotificationService } from "@/lib/services/notification.service";
import { BlockchainService } from "@/lib/services/blockchain.service";
+import { EmailService } from "@/lib/services/email.service";
import { prisma } from "@/lib/db/prisma";
import type { NormalizedAnalysis } from "@/lib/services/ai/analysis.types";
@@ -209,7 +211,9 @@ export async function getContracts(filters?: Record
) {
documentHash: contract.documentHash || null,
txHash: contract.txHash || null,
blockNumber: contract.blockNumber || null,
- blockTimestamp: contract.blockTimestamp ? contract.blockTimestamp.toISOString() : null,
+ blockTimestamp: contract.blockTimestamp
+ ? contract.blockTimestamp.toISOString()
+ : null,
blockchainNetwork: contract.blockchainNetwork || null,
contractAddress: contract.contractAddress || null,
}));
@@ -517,6 +521,16 @@ export async function analyzeContractAction(id: string) {
keyPoints: keyPointsWithLearning,
});
+ let blockchainEmailData: {
+ documentHash: string;
+ txHash: string;
+ blockNumber: number;
+ blockTimestamp: Date;
+ network: string;
+ contractAddress: string;
+ explorerUrl: string | null;
+ } | null = null;
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// BLOCKCHAIN: Auto-register document on-chain
// This is non-blocking — if blockchain fails, analysis still succeeds
@@ -525,7 +539,7 @@ export async function analyzeContractAction(id: string) {
if (BlockchainService.isConfigured()) {
const proof = await BlockchainService.hashAndRegister(
contract.fileUrl,
- contract.fileName
+ contract.fileName,
);
// Save blockchain proof to the contract record
@@ -556,7 +570,19 @@ export async function analyzeContractAction(id: string) {
},
});
- console.log(`🔗 Blockchain proof stored: ${proof.txHash.slice(0, 16)}...`);
+ blockchainEmailData = {
+ documentHash: proof.documentHash,
+ txHash: proof.txHash,
+ blockNumber: proof.blockNumber,
+ blockTimestamp: proof.blockTimestamp,
+ network: proof.network,
+ contractAddress: proof.contractAddress,
+ explorerUrl: proof.explorerUrl,
+ };
+
+ console.log(
+ `🔗 Blockchain proof stored: ${proof.txHash.slice(0, 16)}...`,
+ );
}
} catch (blockchainError) {
// Blockchain failure should NOT fail the analysis
@@ -581,6 +607,71 @@ export async function analyzeContractAction(id: string) {
expiresIn: 7 * 24 * 60 * 60 * 1000, // 7 days
});
+ // Email summary + blockchain proof (non-blocking)
+ try {
+ let recipientEmail = user.email;
+
+ if (!recipientEmail) {
+ const clerk = await clerkClient();
+ const clerkUser = await clerk.users.getUser(clerkId);
+ recipientEmail =
+ clerkUser.emailAddresses.find(
+ (address) => address.id === clerkUser.primaryEmailAddressId,
+ )?.emailAddress ??
+ clerkUser.emailAddresses[0]?.emailAddress ??
+ "";
+ }
+
+ if (recipientEmail) {
+ const premiumValue =
+ aiResults.premium === null || aiResults.premium === undefined
+ ? null
+ : aiResults.premium;
+
+ const keyPointsRecord =
+ typeof keyPointsWithLearning === "object" &&
+ keyPointsWithLearning !== null
+ ? (keyPointsWithLearning as Record)
+ : null;
+
+ await EmailService.sendContractAnalysisCompletedEmail({
+ to: recipientEmail,
+ userDisplayName:
+ `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || null,
+ contractId: id,
+ contractFileName: contract.fileName,
+ contractTitle: aiResults.title,
+ blueprint: {
+ type: aiResults.type,
+ provider: aiResults.provider ?? null,
+ policyNumber: aiResults.policyNumber ?? null,
+ startDate: aiResults.startDate ?? null,
+ endDate: aiResults.endDate ?? null,
+ premium: premiumValue,
+ premiumCurrency:
+ aiAnalysis.premiumCurrency ??
+ (keyPointsRecord?.aiMeta &&
+ typeof keyPointsRecord.aiMeta === "object" &&
+ keyPointsRecord.aiMeta !== null &&
+ "premiumCurrency" in keyPointsRecord.aiMeta
+ ? String(
+ (keyPointsRecord.aiMeta as Record)
+ .premiumCurrency ?? "",
+ ) || null
+ : null),
+ summary: aiResults.summary,
+ },
+ blockchain: blockchainEmailData,
+ });
+ } else {
+ console.warn(
+ `⚠️ Contract analysis email skipped: no recipient email found for user ${user.id}`,
+ );
+ }
+ } catch (emailError) {
+ console.warn("⚠️ Contract analysis email skipped:", emailError);
+ }
+
revalidatePath("/contacts");
revalidatePath("/dashboard");
diff --git a/features/contracts/components/list/contracts-list.tsx b/features/contracts/components/list/contracts-list.tsx
index cf6946f..61b5140 100644
--- a/features/contracts/components/list/contracts-list.tsx
+++ b/features/contracts/components/list/contracts-list.tsx
@@ -17,6 +17,14 @@ import {
Search,
Info,
Network,
+ Shield,
+ Sparkles,
+ FileIcon,
+ ChevronRight,
+ Calendar,
+ HardDrive,
+ Tag,
+ FileType,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
@@ -57,6 +65,7 @@ import {
exportToCSV,
exportToPDF,
} from "@/features/contracts/utils/export.utils";
+import { motion, AnimatePresence } from "motion/react";
interface Contract {
id: string;
@@ -918,37 +927,77 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
const getFileIcon = (mimeType: string) => {
if (mimeType.startsWith("image/")) {
- return "🖼️";
+ return ;
}
if (mimeType === "application/pdf") {
- return "📄";
+ return ;
}
- return "📋";
+ return ;
};
- const getStatusColor = (status: string) => {
+ const getFileIconBg = (mimeType: string) => {
+ if (mimeType.startsWith("image/")) {
+ return "bg-violet-500/10 border-violet-500/20";
+ }
+ if (mimeType === "application/pdf") {
+ return "bg-red-500/10 border-red-500/20";
+ }
+ return "bg-blue-500/10 border-blue-500/20";
+ };
+
+ const getStatusConfig = (status: string) => {
switch (status) {
case "COMPLETED":
- return "text-green-500 dark:text-green-400 bg-green-50 dark:bg-green-950/30";
+ return {
+ dot: "bg-emerald-500",
+ bg: "bg-emerald-500/10 border-emerald-500/20 text-emerald-700 dark:text-emerald-300",
+ label: "Completed",
+ };
case "PROCESSING":
- return "text-blue-500 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/30";
+ return {
+ dot: "bg-blue-500",
+ bg: "bg-blue-500/10 border-blue-500/20 text-blue-700 dark:text-blue-300",
+ label: "Processing",
+ };
case "UPLOADED":
- return "text-amber-500 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30";
+ return {
+ dot: "bg-amber-500",
+ bg: "bg-amber-500/10 border-amber-500/20 text-amber-700 dark:text-amber-300",
+ label: "Uploaded",
+ };
case "FAILED":
- return "text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-950/30";
+ return {
+ dot: "bg-red-500",
+ bg: "bg-red-500/10 border-red-500/20 text-red-700 dark:text-red-300",
+ label: "Failed",
+ };
default:
- return "text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-950/30";
+ return {
+ dot: "bg-gray-500",
+ bg: "bg-gray-500/10 border-gray-500/20 text-gray-700 dark:text-gray-300",
+ label: status,
+ };
}
};
if (isLoading) {
return (
-
-
-
- Loading contracts...
-
-
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
);
}
@@ -959,15 +1008,21 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
return (
<>
{invalidContractReason && (
-
-
-
-
+
+
+
+
Invalid contract upload detected
-
+
{invalidContractFileName
? `${invalidContractFileName}: `
: ""}
@@ -978,7 +1033,7 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
{
setInvalidContractReason("");
setInvalidContractFileName("");
@@ -987,32 +1042,37 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
-
+
)}
-
-
-
-
setSearchQuery(event.target.value)}
- placeholder="Search by contract title or provider..."
- className="pl-9"
- />
+ {/* Toolbar */}
+
+
+
+
+
+ setSearchQuery(event.target.value)}
+ placeholder="Search by contract title or provider..."
+ className="pl-10 pr-4 h-11 rounded-xl border-border/60 bg-background/60 backdrop-blur-xl focus:bg-background/80 focus:ring-2 focus:ring-primary/20 transition-all"
+ />
+
-
+
{debouncedSearchQuery && (
-
- Showing results for: "{debouncedSearchQuery}"
+
+ {contracts.length} result{contracts.length !== 1 ? "s" : ""} for
+ "{debouncedSearchQuery}"
)}
setDeleteAllDialogOpen(true)}
- className="gap-2"
+ className="gap-2 rounded-xl border-border/60 hover:border-red-500/30 hover:bg-red-500/5 hover:text-red-600 transition-all"
>
{isDeletingAll ? (
@@ -1024,162 +1084,201 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
-
-
- {contracts.map((contract) => (
-
-
-
- {getFileIcon(contract.mimeType)}
-
+ {/* Contract Cards */}
+
+
+ {contracts.map((contract, idx) => {
+ const status = getStatusConfig(contract.status);
+ return (
+
+
-
-
-
- {contract.fileName}
-
-
+
+
- {contract.status}
-
- {contract.isRagged && (
-
-
- RAG {contract.ragChunkCount ?? 0}
-
- )}
+ {getFileIcon(contract.mimeType)}
+
+
+
+
+
+ {contract.fileName}
+
+
+
+ {status.label}
+
+ {contract.isRagged && (
+
+
+ RAG {contract.ragChunkCount ?? 0}
+
+ )}
+
+
+
+
+
+ {formatFileSize(contract.fileSize)}
+
+
+
+
+ {formatDate(contract.createdAt)}
+
+
+
-
- {formatFileSize(contract.fileSize)}
- •
- {formatDate(contract.createdAt)}
-
-
-
-
-
-
{
- if (contract.fileUrl) {
- window.open(contract.fileUrl, "_blank");
- }
- }}
- >
-
-
-
-
{
- if (contract.fileUrl) {
- const link = document.createElement("a");
- link.href = contract.fileUrl;
- link.download =
- contract.fileUrl.split("/").pop() || "contract";
- link.target = "_blank";
- link.rel = "noopener noreferrer";
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- }
- }}
- >
-
-
-
-
-
+
{
+ if (contract.fileUrl) {
+ window.open(contract.fileUrl, "_blank");
+ }
+ }}
>
-
+
-
-
- handleOpenAsk(contract)}
- className="cursor-pointer"
- >
-
- Ask about this file
-
- handleOpenDetails(contract)}
- className="cursor-pointer"
- >
-
- Details
-
- exportToPDF(contract as any)}
- className="cursor-pointer"
- >
-
- Export Analysis (PDF)
-
- exportToCSV(contract as any)}
- className="cursor-pointer"
- >
-
- Export Analysis (CSV)
-
- requestDeleteContract(contract)}
- disabled={deletingId === contract.id}
- className="text-destructive focus:text-destructive cursor-pointer"
- >
- {deletingId === contract.id ? (
- <>
-
- Deleting...
- >
- ) : (
- <>
-
- Delete
- >
- )}
-
-
-
-
-
- ))}
- {contracts.length === 0 && debouncedSearchQuery && (
-
-
- No contracts found
-
-
- Try different keywords from the title or provider name.
-
+
{
+ if (contract.id) {
+ const link = document.createElement("a");
+ link.href = `/api/contracts/${contract.id}/download`;
+ link.setAttribute(
+ "download",
+ contract.fileName || "contract",
+ );
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ handleOpenAsk(contract)}
+ className="cursor-pointer rounded-lg focus:bg-primary/10"
+ >
+
+ Ask about this file
+
+ handleOpenDetails(contract)}
+ className="cursor-pointer rounded-lg focus:bg-primary/10"
+ >
+
+ Details
+
+ exportToPDF(contract as any)}
+ className="cursor-pointer rounded-lg focus:bg-primary/10"
+ >
+
+ Export Analysis (PDF)
+
+ exportToCSV(contract as any)}
+ className="cursor-pointer rounded-lg focus:bg-primary/10"
+ >
+
+ Export Analysis (CSV)
+
+ requestDeleteContract(contract)}
+ disabled={deletingId === contract.id}
+ className="text-destructive focus:text-destructive cursor-pointer rounded-lg focus:bg-destructive/10"
+ >
+ {deletingId === contract.id ? (
+ <>
+
+ Deleting...
+ >
+ ) : (
+ <>
+
+ Delete
+ >
+ )}
+
+
+
+
+
+
+ );
+ })}
+
+
+ {contracts.length === 0 && debouncedSearchQuery && (
+
+
- )}
-
-
+
+ No contracts found
+
+
+ Try different keywords from the title or provider name.
+
+
+ )}
+
{/* Details Modal */}
-
-
-
-
+
+
+
+
+
+
+
+
Contract Details
@@ -1187,210 +1286,158 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
{selectedContract && (
-
-
+ {/* Document Profile */}
+
+
+
+
-
+
Document Profile
-
+
{selectedContract.fileName}
+
{selectedContract.status}
-
-
-
- File Size
-
-
- {formatFileSize(selectedContract.fileSize)}
-
-
-
-
- Mime Type
-
-
- {selectedContract.mimeType}
-
-
-
-
- Uploaded
-
-
- {formatDate(selectedContract.createdAt)}
-
-
-
-
- Category
-
-
- {selectedContract.type || "Pending analysis"}
-
-
+
+ {[
+ {
+ label: "File Size",
+ value: formatFileSize(selectedContract.fileSize),
+ icon:
,
+ },
+ {
+ label: "Mime Type",
+ value: selectedContract.mimeType,
+ icon:
,
+ },
+ {
+ label: "Uploaded",
+ value: formatDate(selectedContract.createdAt),
+ icon:
,
+ },
+ {
+ label: "Category",
+ value: selectedContract.type || "Pending analysis",
+ icon:
,
+ },
+ ].map((item) => (
+
+
+
{item.icon}
+
+ {item.label}
+
+
+
+ {item.value}
+
+
+ ))}
{/* AI Analysis Results */}
{selectedContract.status === "COMPLETED" && (
<>
-
-
- Extracted Contract Information
-
+
+
+
+
+ Extracted Contract Information
+
+
-
-
-
- Title
-
-
- handleOpenFieldProof("title", "Title")
- }
- className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
- aria-label="Show title proof"
- title="Show proof"
- >
-
-
-
-
- {stripMarkdown(selectedContract.title) || "N/A"}
-
-
-
-
-
- Provider
-
-
- handleOpenFieldProof("provider", "Provider")
- }
- className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
- aria-label="Show provider proof"
- title="Show proof"
- >
-
-
-
-
- {stripMarkdown(selectedContract.provider) || "N/A"}
-
-
-
-
-
- Policy Number
-
-
- handleOpenFieldProof(
- "policyNumber",
- "Policy Number",
- )
- }
- className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
- aria-label="Show policy number proof"
- title="Show proof"
- >
-
-
-
-
- {stripMarkdown(selectedContract.policyNumber) ||
- "N/A"}
-
-
-
-
-
- Start Date
-
-
- handleOpenFieldProof("startDate", "Start Date")
- }
- className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
- aria-label="Show start date proof"
- title="Show proof"
- >
-
-
-
-
- {selectedContract.startDate
+ {[
+ {
+ key: "title",
+ label: "Title",
+ value: stripMarkdown(selectedContract.title) || "N/A",
+ },
+ {
+ key: "provider",
+ label: "Provider",
+ value:
+ stripMarkdown(selectedContract.provider) || "N/A",
+ },
+ {
+ key: "policyNumber",
+ label: "Policy Number",
+ value:
+ stripMarkdown(selectedContract.policyNumber) ||
+ "N/A",
+ },
+ {
+ key: "startDate",
+ label: "Start Date",
+ value: selectedContract.startDate
? formatDate(selectedContract.startDate)
- : "N/A"}
-
-
-
-
-
- End Date
-
-
- handleOpenFieldProof("endDate", "End Date")
- }
- className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
- aria-label="Show end date proof"
- title="Show proof"
- >
-
-
-
-
- {selectedContract.endDate
+ : "N/A",
+ },
+ {
+ key: "endDate",
+ label: "End Date",
+ value: selectedContract.endDate
? formatDate(selectedContract.endDate)
- : "N/A"}
-
-
-
-
-
- Premium
+ : "N/A",
+ },
+ {
+ key: "premium",
+ label: "Premium",
+ value:
+ formatPremiumWithSourceCurrency(selectedContract),
+ },
+ ].map((field) => (
+
+
+
+ {field.label}
+
+
+ handleOpenFieldProof(field.key, field.label)
+ }
+ className="rounded-lg border border-transparent p-1.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-all hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
+ aria-label={`Show ${field.label.toLowerCase()} proof`}
+ title="Show proof"
+ >
+
+
+
+
+ {field.value}
-
- handleOpenFieldProof("premium", "Premium")
- }
- className="rounded-lg border border-transparent p-1 text-muted-foreground transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary"
- aria-label="Show premium proof"
- title="Show proof"
- >
-
-
-
- {formatPremiumWithSourceCurrency(selectedContract)}
-
-
+ ))}
{selectedContract.summary && (
-
-
- Summary
-
-
+
+
+
+
+ Summary
+
+
+
{renderRichParagraphs(
selectedContract.summary,
`summary-${selectedContract.id}`,
@@ -1400,35 +1447,38 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
)}
{selectedContract.keyPoints && (
-
-
- Key Points
-
-
+
+
+
+
+ Key Points
+
+
+
{isContractKeyPoints(selectedContract.keyPoints) &&
selectedContract.keyPoints.guarantees &&
Array.isArray(
selectedContract.keyPoints.guarantees,
) && (
-
- Guarantees:
+
+ Guarantees
-
+
{(
selectedContract.keyPoints.guarantees ?? []
).map((guarantee, idx: number) => (
-
{renderRichParagraphs(
guarantee,
`guarantee-${selectedContract.id}-${idx}`,
)}
-
+
))}
-
+
)}
{isContractKeyPoints(selectedContract.keyPoints) &&
@@ -1437,33 +1487,33 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
selectedContract.keyPoints.exclusions,
) && (
-
- Exclusions:
+
+ Exclusions
-
+
{(
selectedContract.keyPoints.exclusions ?? []
).map((exclusion, idx: number) => (
-
{renderRichParagraphs(
exclusion,
`exclusion-${selectedContract.id}-${idx}`,
)}
-
+
))}
-
+
)}
{isContractKeyPoints(selectedContract.keyPoints) &&
selectedContract.keyPoints.franchise && (
-
- Deductible:
+
+ Deductible
-
+
{renderRichParagraphs(
String(selectedContract.keyPoints.franchise),
`franchise-${selectedContract.id}`,
@@ -1478,29 +1528,46 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
)}
{selectedContract.status === "PROCESSING" && (
-
-
-
- AI analysis is in progress...
-
+
+
+
+
+
+
+ AI analysis is in progress
+
+
+ Extracting entities, clauses, and generating insights...
+
+
)}
{selectedContract.status === "UPLOADED" && (
-
-
-
- Contract uploaded. AI analysis will start automatically.
-
+
+
+
+
+
+
+ Contract uploaded successfully
+
+
+ AI analysis will begin automatically momentarily
+
+
)}
{selectedContract.status === "FAILED" && (
-
-
- Analysis failed
-
-
+
+
+
+
+ Analysis failed
+
+
+
{selectedContract.summary ||
"The uploaded file could not be processed as a valid contract. Please upload a clearer contract document and try again."}
@@ -1525,13 +1592,22 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
/>
-
+
- Delete this contract?
-
+
+
+ Delete this contract?
+
+
This action permanently removes the selected contract and its
associated file.
- {contractToDelete ? `\n\nFile: ${contractToDelete.fileName}` : ""}
+ {contractToDelete ? (
+
+ {contractToDelete.fileName}
+
+ ) : (
+ ""
+ )}
@@ -1539,12 +1615,13 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
onClick={() => {
setContractToDelete(null);
}}
+ className="rounded-xl"
>
Cancel
void confirmDeleteContract()}
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-xl"
>
{contractToDelete && deletingId === contractToDelete.id
? "Deleting..."
@@ -1558,19 +1635,22 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
open={deleteAllDialogOpen}
onOpenChange={setDeleteAllDialogOpen}
>
-
+
- Delete all contracts?
+
+
+ Delete all contracts?
+
This action permanently removes all contracts and related files
for your account. This cannot be undone.
- Cancel
+ Cancel
void handleDeleteAll()}
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-xl"
>
{isDeletingAll ? "Deleting..." : "Delete All"}
@@ -1582,7 +1662,7 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
open={invalidContractDialogOpen}
onOpenChange={setInvalidContractDialogOpen}
>
-
+
@@ -1594,9 +1674,11 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
The AI could not validate this file as a real contract.
-
-
Reason
-
+
+
+ Reason
+
+
{invalidContractReason ||
"This uploaded file does not appear to be a valid contract."}
@@ -1604,12 +1686,12 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
{invalidContractFileName && (
File:{" "}
-
+
{invalidContractFileName}
)}
-
+
Please upload a contract or policy document with readable legal
terms and agreement details.
diff --git a/lib/services/ai.service.ts b/lib/services/ai.service.ts
index 5553613..feb735f 100644
--- a/lib/services/ai.service.ts
+++ b/lib/services/ai.service.ts
@@ -20,14 +20,18 @@ import { keyManager } from "@/lib/services/ai/key-manager";
const PRIMARY_ANALYSIS_MODEL =
process.env.AI_MODEL_PRIMARY || "gemini-3.1-flash-lite-preview";
const GEMINI_SECONDARY_ANALYSIS_MODEL =
- process.env.AI_MODEL_SECONDARY_GEMINI || "";
+ process.env.AI_MODEL_SECONDARY_GEMINI || process.env.AI_MODEL_SECONDARY || "";
const FALLBACK_ANALYSIS_MODEL =
- process.env.AI_MODEL_FALLBACK || "llama-3.3-70b-versatile";
+ process.env.AI_MODEL_FALLBACK || "mistral-large-latest";
const FALLBACK_REPAIR_MODEL =
- process.env.AI_MODEL_FALLBACK_REPAIR || "llama-3.3-70b-versatile";
-const GROQ_API_KEY =
- process.env.GROQ_API_KEY?.trim() || process.env.AI_GROQ_API_KEY?.trim() || "";
-const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
+ process.env.AI_MODEL_FALLBACK_REPAIR || "mistral-large-latest";
+const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY?.trim() || "";
+const MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions";
+const MISTRAL_OCR_API_URL = "https://api.mistral.ai/v1/ocr";
+const MISTRAL_VISION_MODEL =
+ process.env.AI_MODEL_MISTRAL_VISION || "pixtral-large-latest";
+const MISTRAL_OCR_MODEL =
+ process.env.AI_MODEL_MISTRAL_OCR || "mistral-ocr-latest";
const GEMINI_ANALYSIS_MODELS = Array.from(
new Set(
@@ -36,7 +40,7 @@ const GEMINI_ANALYSIS_MODELS = Array.from(
);
const ANALYSIS_MODELS = Array.from(
- new Set([...GEMINI_ANALYSIS_MODELS, `groq:${FALLBACK_ANALYSIS_MODEL}`]),
+ new Set([...GEMINI_ANALYSIS_MODELS, `mistral:${FALLBACK_ANALYSIS_MODEL}`]),
);
const FORCE_FALLBACK_TEST =
@@ -89,7 +93,7 @@ const isAdaptiveKeyPoints = (
};
export class AIService {
- private static isTransientGeminiError(message: string): boolean {
+ private static isTransientAIError(message: string): boolean {
const normalized = message.toLowerCase();
return (
normalized.includes("503") ||
@@ -282,7 +286,7 @@ export class AIService {
// Better error messages
if (errorMessage.includes("API key")) {
throw new Error(
- "Invalid or missing AI API key. Check AI_API_KEY1/2/3 for Gemini and GROQ_API_KEY for Groq fallback.",
+ "Invalid or missing AI API key. Check AI_API_KEY1/2/3 for Gemini and MISTRAL_API_KEY for Mistral fallback.",
);
} else if (errorMessage.includes("INVALID_CONTRACT:")) {
const reason = String(errorMessage)
@@ -291,9 +295,9 @@ export class AIService {
throw new Error(
reason || "Uploaded file is not recognized as a valid contract.",
);
- } else if (this.isTransientGeminiError(errorMessage)) {
+ } else if (this.isTransientAIError(errorMessage)) {
throw new Error(
- `Gemini is temporarily overloaded for the configured analysis models (${ANALYSIS_MODELS.join(", ")}). The app retried automatically, but both models are still busy. Please try again in a few minutes.`,
+ `The AI providers (Gemini/Mistral) are temporarily overloaded for the configured analysis models (${ANALYSIS_MODELS.join(", ")}). The app retried automatically, but both providers are still busy. Please try again in a few minutes.`,
);
} else if (
errorMessage.includes("not found") ||
@@ -337,7 +341,7 @@ export class AIService {
}
} else if (errorMessage.includes("quota")) {
throw new Error(
- "Limit exceeded. Gemini or Groq quota may be exhausted. Check your provider dashboards for usage and limits.",
+ "Limit exceeded. Gemini or Mistral quota may be exhausted. Check your provider dashboards for usage and limits.",
);
} else {
throw new Error(`Error analyzing contract: ${errorMessage}`);
@@ -389,11 +393,11 @@ export class AIService {
return parseAiJsonResponse(text);
}
- private static isGroqConfigured(): boolean {
- return GROQ_API_KEY.length > 0;
+ private static isMistralConfigured(): boolean {
+ return MISTRAL_API_KEY.length > 0;
}
- private static async generateWithGroq(input: {
+ private static async generateWithMistral(input: {
model?: string;
prompt: string;
systemPrompt?: string;
@@ -402,9 +406,9 @@ export class AIService {
temperature?: number;
topP?: number;
}): Promise
{
- if (!this.isGroqConfigured()) {
+ if (!this.isMistralConfigured()) {
throw new Error(
- "Groq fallback is not configured. Set GROQ_API_KEY (or AI_GROQ_API_KEY).",
+ "Mistral fallback is not configured. Set MISTRAL_API_KEY.",
);
}
@@ -418,23 +422,25 @@ export class AIService {
messages.push({ role: "user", content: input.prompt });
// Use json_object mode (compatible with all models)
- const responseFormat: Record | undefined = input.responseAsJson
- ? { type: "json_object" as const }
- : undefined;
+ const responseFormat: Record | undefined =
+ input.responseAsJson ? { type: "json_object" as const } : undefined;
+
+ const temperature = input.temperature ?? 0;
+ const top_p = temperature === 0 ? 1 : (input.topP ?? 0.95);
const body: Record = {
model: modelName,
- temperature: input.temperature ?? 0,
- top_p: input.topP ?? 0.95,
+ temperature,
+ top_p,
max_tokens: input.maxOutputTokens,
response_format: responseFormat,
messages,
};
- const response = await fetch(GROQ_API_URL, {
+ const response = await fetch(MISTRAL_API_URL, {
method: "POST",
headers: {
- Authorization: `Bearer ${GROQ_API_KEY}`,
+ Authorization: `Bearer ${MISTRAL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
@@ -443,7 +449,7 @@ export class AIService {
if (!response.ok) {
const details = await response.text();
throw new Error(
- `Groq API error ${response.status}: ${details.slice(0, 300)}`,
+ `Mistral API error ${response.status}: ${details.slice(0, 300)}`,
);
}
@@ -453,13 +459,92 @@ export class AIService {
const text = json.choices?.[0]?.message?.content?.trim() || "";
if (!text) {
- throw new Error("Empty response from Groq fallback model.");
+ throw new Error("Empty response from Mistral fallback model.");
}
return text;
}
- private static async generateWithGroqModelChain(input: {
+ /**
+ * Multimodal analysis using Mistral Pixtral vision model.
+ * Sends base64-encoded images directly to Pixtral for analysis,
+ * eliminating the need for a separate OCR bridge when Gemini is down.
+ */
+ private static async generateWithMistralVision(input: {
+ prompt: string;
+ base64: string;
+ mimeType: string;
+ systemPrompt?: string;
+ responseAsJson?: boolean;
+ maxOutputTokens?: number;
+ }): Promise {
+ if (!this.isMistralConfigured()) {
+ throw new Error(
+ "Mistral fallback is not configured. Set MISTRAL_API_KEY.",
+ );
+ }
+
+ const messages: Array<{ role: string; content: unknown }> = [];
+ if (input.systemPrompt) {
+ messages.push({ role: "system", content: input.systemPrompt });
+ }
+
+ // OpenAI-compatible multimodal content format for Pixtral vision
+ messages.push({
+ role: "user",
+ content: [
+ { type: "text", text: input.prompt },
+ {
+ type: "image_url",
+ image_url: {
+ url: `data:${input.mimeType};base64,${input.base64}`,
+ },
+ },
+ ],
+ });
+
+ const responseFormat: Record | undefined =
+ input.responseAsJson ? { type: "json_object" as const } : undefined;
+
+ const body: Record = {
+ model: MISTRAL_VISION_MODEL,
+ temperature: 0,
+ top_p: 1,
+ max_tokens: input.maxOutputTokens ?? 16384,
+ response_format: responseFormat,
+ messages,
+ };
+
+ const response = await fetch(MISTRAL_API_URL, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${MISTRAL_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const details = await response.text();
+ throw new Error(
+ `Mistral Vision API error ${response.status}: ${details.slice(0, 300)}`,
+ );
+ }
+
+ const json = (await response.json()) as {
+ choices?: Array<{ message?: { content?: string | null } }>;
+ };
+ const text = json.choices?.[0]?.message?.content?.trim() || "";
+
+ if (!text) {
+ throw new Error("Empty response from Mistral Pixtral vision model.");
+ }
+
+ console.log(`✅ Mistral Pixtral vision analysis succeeded`);
+ return text;
+ }
+
+ private static async generateWithMistralModelChain(input: {
preferredModel?: string;
prompt: string;
systemPrompt?: string;
@@ -473,9 +558,9 @@ export class AIService {
[
input.preferredModel,
FALLBACK_ANALYSIS_MODEL,
- "llama-3.3-70b-versatile",
- "qwen-2.5-32b",
- "llama-3.1-8b-instant",
+ "mistral-large-latest",
+ "mistral-small-latest",
+ "open-mistral-nemo",
].filter(Boolean),
),
) as string[];
@@ -484,7 +569,7 @@ export class AIService {
for (const modelName of candidates) {
try {
- const text = await this.generateWithGroq({
+ const text = await this.generateWithMistral({
model: modelName,
prompt: input.prompt,
systemPrompt: input.systemPrompt,
@@ -495,14 +580,14 @@ export class AIService {
});
if (modelName !== (input.preferredModel || FALLBACK_ANALYSIS_MODEL)) {
console.warn(
- `Groq switched to fallback model ${modelName} after primary fallback model failed.`,
+ `Mistral switched to fallback model ${modelName} after primary fallback model failed.`,
);
}
return text;
} catch (error) {
lastError = error;
console.warn(
- `Groq model ${modelName} failed. Trying next fallback model.`,
+ `Mistral model ${modelName} failed. Trying next fallback model.`,
error instanceof Error ? error.message : String(error),
);
}
@@ -510,36 +595,79 @@ export class AIService {
throw lastError instanceof Error
? lastError
- : new Error("All Groq fallback models failed.");
+ : new Error("All Mistral fallback models failed.");
}
/**
- * Build a Groq-optimized system prompt that mirrors the Gemini behavior.
+ * Build a Mistral-optimized system prompt that mirrors the Gemini behavior.
* This separates role & formatting rules from user content for better
* instruction adherence on open-source models.
+ *
+ * Unlike the Gemini prompt which sends examples with the file inline,
+ * this prompt is designed to prevent hallucination by using explicit
+ * placeholder markers instead of realistic example values.
*/
- private static buildGroqSystemPrompt(): string {
+ private static buildMistralSystemPrompt(): string {
return `You are an expert contract analysis engine for the BFSI (Banking, Financial Services, and Insurance) sector.
-You receive the full text content of a contract document below and must extract structured information from it.
+You receive the full text content of a contract document and must extract structured information from it.
-CRITICAL OUTPUT RULES:
+ABSOLUTE RULES — VIOLATION OF THESE IS A CRITICAL FAILURE:
1. Return ONLY valid, parseable JSON — no markdown, no backticks, no explanations, no commentary.
-2. Your JSON must conform EXACTLY to the schema specified in the user prompt.
-3. Every required field MUST be present. Use null for missing strings/numbers and [] for missing arrays.
-4. All dates MUST be in ISO YYYY-MM-DD format or null.
-5. The "premium" field must be a positive number or null — NO currency symbols.
-6. The "type" field MUST be one of: INSURANCE_AUTO, INSURANCE_HOME, INSURANCE_HEALTH, INSURANCE_LIFE, LOAN, CREDIT_CARD, INVESTMENT, OTHER.
-7. Do NOT hallucinate or invent data that is not present in the document.
-8. Preserve original language in extractedText and sourceSnippet fields (accents, special characters).
-9. The "summary" must be 4-6 professional sentences covering parties, obligations, coverage, exclusions, and deadlines.
-10. The "extractedText" must contain at least 30 characters of actual document content.
-11. The "keyPoints.explainability" array must have at least 4 items for critical fields when data is available.
-12. contractValidation.confidence must reflect actual extraction certainty (0-100).
-13. When uncertain about a value, use null and set a lower confidence — never guess.
-14. Parse localized number formats correctly (e.g., 1.234,56 vs 1,234.56).
-15. Detect the contract language and set the "language" field accordingly (ISO 639-1).
+2. EVERY value you output MUST come directly from the document text provided to you.
+3. If a piece of information does NOT exist in the document text, you MUST use null (for strings/numbers) or [] (for arrays). NEVER invent, assume, or guess data.
+4. Do NOT copy example values from the schema description — they are placeholders, not real data.
+5. The "extractedText" field MUST contain actual verbatim text from the document — not a summary, not examples.
-You are replacing a more capable multimodal model (Gemini) as a fallback. Your output quality MUST match production standards.`;
+JSON SCHEMA (use exact field names):
+{
+ "language": "",
+ "title": "",
+ "type": "",
+ "provider": "",
+ "policyNumber": "",
+ "startDate": "",
+ "endDate": "",
+ "premium": ,
+ "premiumCurrency": "",
+ "summary": "<4-6 sentences summarizing the actual contract content>",
+ "keyPoints": {
+ "guarantees": [""],
+ "exclusions": [""],
+ "franchise": "",
+ "importantDates": [""],
+ "explainability": [
+ {
+ "field": "",
+ "why": "",
+ "sourceSnippet": "",
+ "sourceHints": { "page": "", "section": "", "confidence": <0-100> }
+ }
+ ]
+ },
+ "keyPeople": [{"name": "", "role": "", "email": "", "phone": ""}],
+ "contactInfo": {"name": "", "email": null, "phone": null, "address": null, "role": null},
+ "importantContacts": [],
+ "relevantDates": [{"date": "", "description": "", "type": ""}],
+ "extractedText": "",
+ "contractValidation": {
+ "isValidContract": true,
+ "confidence": <0-100 reflecting how much data you actually found>,
+ "reason": null
+ }
+}
+
+FIELD RULES:
+- All dates: ISO YYYY-MM-DD or null
+- premium: positive number or null — NO currency symbols, NO text
+- type: must be exactly one of the 8 values listed
+- summary: 4-6 professional sentences about THIS specific contract. If no contract text is found, output "No contract data found in the document text."
+- extractedText: must contain at least 30 characters of ACTUAL document content. If no text is found, output "No document text could be extracted. Please ensure the document is not a scanned image."
+- explainability: at least 4 items with real sourceSnippets from the document
+- confidence: reflects how much data you actually found (not how confident the model is)
+- Parse localized number formats correctly (1.234,56 vs 1,234.56)
+- Detect the contract language and set "language" accordingly
+
+You are replacing a more capable multimodal model (Gemini) as a fallback. Your output quality MUST match production standards. ACCURACY is more important than completeness — it is better to return null than to guess.`;
}
private static async generateAnalysisWithFallback(input: {
@@ -551,27 +679,52 @@ You are replacing a more capable multimodal model (Gemini) as a fallback. Your o
let lastError: unknown = null;
const forceFallback = Boolean(input.forceFallbackModelTest);
- const buildGroundedGroqPrompt = async (basePrompt: string) => {
- const groundingText = await this.extractGroqGroundingText({
+ const buildGroundedMistralPrompt = async () => {
+ const groundingText = await this.extractMistralGroundingText({
base64: input.base64,
mimeType: input.mimeType,
});
if (!groundingText) {
- return `${basePrompt}\n\nGROQ FALLBACK RULES:\n- You do not have direct binary file access in this fallback path.\n- Do not hallucinate values; use null/empty arrays when data is missing.\n- Keep contractValidation conservative when uncertain.\n- Set contractValidation.confidence to at most 60 when no grounding text is available.`;
+ throw new Error(
+ "INVALID_CONTRACT:No extractable text found in this PDF after OCR fallback. Please verify the file is readable and not password-protected.",
+ );
}
- return `${basePrompt}\n\n--- BEGIN GROUNDED DOCUMENT TEXT (AUTHORITATIVE SOURCE) ---\n${groundingText}\n--- END GROUNDED DOCUMENT TEXT ---\n\nGROQ FALLBACK RULES:\n- Extract fields ONLY from the grounded document text above. This text is the full contract content.\n- Do not invent, assume, or hallucinate any values not explicitly present in the above text.\n- If a field's data is not found in the text, use null (for strings/numbers) or [] (for arrays).\n- Dates: convert any date format found in the text to YYYY-MM-DD.\n- Numbers: parse localized formats (comma vs period) correctly before setting numeric fields.\n- contractValidation.confidence should reflect how much data you could extract from the text.`;
+ return `--- BEGIN GROUNDED DOCUMENT TEXT (AUTHORITATIVE SOURCE) ---
+${groundingText}
+--- END GROUNDED DOCUMENT TEXT ---
+
+MISTRAL FALLBACK RULES:
+- Extract fields ONLY from the grounded document text above. This text is the full contract content.
+- Do not invent, assume, or hallucinate any values not explicitly present in the above text.
+- If a field's data is not found in the text, use null (for strings/numbers) or [] (for arrays).
+- Dates: convert any date format found in the text to YYYY-MM-DD.
+- Numbers: parse localized formats (comma vs period) correctly before setting numeric fields.
+- contractValidation.confidence should reflect how much data you could extract from the text.`;
};
if (forceFallback) {
console.warn(
- `🧪 Fallback test mode enabled. Skipping Gemini and forcing Groq model ${FALLBACK_ANALYSIS_MODEL}.`,
+ `🧪 Fallback test mode enabled. Skipping Gemini and forcing Mistral model ${FALLBACK_ANALYSIS_MODEL}.`,
);
- const groundedPrompt = await buildGroundedGroqPrompt(input.prompt);
- return this.generateWithGroqModelChain({
+
+ // For images: use Pixtral vision model directly (multimodal — no OCR bridge needed)
+ if (input.mimeType.startsWith("image/") && this.isMistralConfigured()) {
+ return this.generateWithMistralVision({
+ systemPrompt: this.buildMistralSystemPrompt(),
+ prompt: `TEST MODE: You are the forced fallback model. Return ONLY valid JSON and preserve the required schema exactly. Extract information from the provided image.`,
+ base64: input.base64,
+ mimeType: input.mimeType,
+ responseAsJson: true,
+ maxOutputTokens: 16384,
+ });
+ }
+
+ const groundedPrompt = await buildGroundedMistralPrompt();
+ return this.generateWithMistralModelChain({
preferredModel: FALLBACK_ANALYSIS_MODEL,
- systemPrompt: this.buildGroqSystemPrompt(),
+ systemPrompt: this.buildMistralSystemPrompt(),
prompt: `${groundedPrompt}\n\nTEST MODE: You are the forced fallback model. Return ONLY valid JSON and preserve the required schema exactly.`,
responseAsJson: true,
maxOutputTokens: 8192,
@@ -610,7 +763,6 @@ You are replacing a more capable multimodal model (Gemini) as a fallback. Your o
throw new Error("Empty response");
});
} catch (error: any) {
- if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
lastError = error;
console.warn(
`Analysis with model ${modelName} failed. Trying next model.`,
@@ -654,35 +806,54 @@ You are replacing a more capable multimodal model (Gemini) as a fallback. Your o
throw new Error("Empty response from fallback");
});
} catch (error: any) {
- if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
console.warn("Lenient generation also failed:", error);
}
- // === Groq fallback path ===
+ // === Mistral AI fallback path ===
console.warn(
- "All Gemini models exhausted. Activating Groq fallback pipeline...",
+ "All Gemini models exhausted. Activating Mistral AI fallback pipeline...",
);
try {
- const groundedPrompt = await buildGroundedGroqPrompt(input.prompt);
- const groqText = await this.generateWithGroqModelChain({
+ // For images: use Pixtral vision model directly (multimodal — no OCR bridge needed)
+ if (input.mimeType.startsWith("image/") && this.isMistralConfigured()) {
+ const mistralText = await this.generateWithMistralVision({
+ systemPrompt: this.buildMistralSystemPrompt(),
+ prompt: `IMPORTANT: Return ONLY valid JSON and preserve the required schema exactly. Do not add any text outside of the JSON object. Extract data from the provided image.`,
+ base64: input.base64,
+ mimeType: input.mimeType,
+ responseAsJson: true,
+ maxOutputTokens: 16384,
+ });
+ console.log(
+ `✅ Analysis fallback with Mistral Pixtral vision succeeded`,
+ );
+ return mistralText;
+ }
+
+ // For PDFs/text: extract text and use text-only Mistral
+ const groundedPrompt = await buildGroundedMistralPrompt();
+ const mistralText = await this.generateWithMistralModelChain({
preferredModel: FALLBACK_ANALYSIS_MODEL,
- systemPrompt: this.buildGroqSystemPrompt(),
+ systemPrompt: this.buildMistralSystemPrompt(),
prompt: `${groundedPrompt}\n\nIMPORTANT: Return ONLY valid JSON and preserve the required schema exactly. Do not add any text outside of the JSON object.`,
responseAsJson: true,
maxOutputTokens: 8192,
});
console.log(
- `✅ Analysis fallback with Groq model ${FALLBACK_ANALYSIS_MODEL} succeeded`,
+ `✅ Analysis fallback with Mistral model ${FALLBACK_ANALYSIS_MODEL} succeeded`,
+ );
+ return mistralText;
+ } catch (mistralError) {
+ console.warn("Mistral analysis fallback failed:", mistralError);
+ lastError = new Error(
+ `Mistral fallback also failed: ${mistralError instanceof Error ? mistralError.message : String(mistralError)}. Original error: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
);
- return groqText;
- } catch (groqError) {
- console.warn("Groq analysis fallback failed:", groqError);
}
throw lastError instanceof Error
? lastError
: new Error(
- "All analysis models (Gemini + Groq fallback) failed to generate content.",
+ "All analysis models (Gemini + Mistral fallback) failed to generate content.",
);
}
@@ -746,7 +917,7 @@ Original parse error: ${parseError}
Malformed response to fix:
${malformedResponse.slice(0, 14000)}`;
- const repairedText = await this.generateWithGroqModelChain({
+ const repairedText = await this.generateWithMistralModelChain({
preferredModel: FALLBACK_REPAIR_MODEL,
prompt: repairPrompt,
responseAsJson: true,
@@ -766,7 +937,7 @@ ${malformedResponse.slice(0, 14000)}`;
} catch (firstRepairParseError) {
const secondPassPrompt = `${repairPrompt}\n\nSECOND PASS CORRECTION:\nYour previous repaired JSON was still invalid.\nReason: ${firstRepairParseError instanceof Error ? firstRepairParseError.message : "Invalid JSON"}.\nReturn ONLY strict valid JSON.`;
- const secondPass = await this.generateWithGroqModelChain({
+ const secondPass = await this.generateWithMistralModelChain({
preferredModel: FALLBACK_REPAIR_MODEL,
prompt: secondPassPrompt,
responseAsJson: true,
@@ -785,7 +956,12 @@ ${malformedResponse.slice(0, 14000)}`;
}
}
- private static async extractGroqGroundingText(input: {
+ /**
+ * Extract grounding text for Mistral text-only fallback.
+ * For PDFs: extracts text directly using pdf-parse (local, no AI needed).
+ * For images: returns empty string — Pixtral vision handles images directly.
+ */
+ private static async extractMistralGroundingText(input: {
base64: string;
mimeType: string;
}): Promise {
@@ -793,13 +969,43 @@ ${malformedResponse.slice(0, 14000)}`;
if (input.mimeType === "application/pdf") {
try {
const pdfBuffer = Buffer.from(input.base64, "base64");
- const { PDFParse } = await import("pdf-parse");
- const parser = new PDFParse({ data: pdfBuffer });
- let parsed: { text?: string };
+
+ // Handle Next.js Webpack/Turbopack CJS/ESM interop
+ let pdfParseModule: any;
try {
- parsed = await parser.getText();
- } finally {
- await parser.destroy();
+ pdfParseModule = require("pdf-parse");
+ } catch {
+ pdfParseModule = await import("pdf-parse");
+ }
+
+ const PDFParseClass =
+ pdfParseModule?.PDFParse ||
+ pdfParseModule?.default?.PDFParse ||
+ (typeof pdfParseModule === "function" ? pdfParseModule : null);
+
+ if (!PDFParseClass) {
+ throw new Error(
+ "Could not resolve PDFParse constructor from pdf-parse module.",
+ );
+ }
+
+ let parsed: { text?: string };
+
+ if (
+ typeof PDFParseClass === "function" &&
+ !PDFParseClass.prototype?.getText
+ ) {
+ // Fallback if it's actually the legacy function export
+ parsed = await PDFParseClass(pdfBuffer);
+ } else {
+ const parser = new PDFParseClass({ data: pdfBuffer });
+ try {
+ parsed = await parser.getText();
+ } finally {
+ if (typeof parser.destroy === "function") {
+ await parser.destroy();
+ }
+ }
}
const text = (parsed?.text || "")
@@ -807,66 +1013,110 @@ ${malformedResponse.slice(0, 14000)}`;
.replace(/\n{3,}/g, "\n\n")
.trim();
- if (text && text.length > 50) {
+ if (text && text.length >= 10) {
console.log(
- `📄 Groq grounding: extracted ${text.length} chars from PDF`,
+ `📄 Mistral grounding: extracted ${text.length} chars from PDF`,
);
return text.slice(0, 50000);
}
+
+ console.warn(
+ `📄 Mistral grounding: native PDF text extraction too short (length: ${text?.length || 0}). Trying OCR fallback...`,
+ );
} catch (error) {
console.warn(
- "PDF grounding extraction failed for Groq fallback.",
- error,
+ "📄 PDF grounding extraction failed for Mistral fallback:",
+ error instanceof Error ? error.message : error,
+ );
+ }
+
+ // OCR fallback for scanned PDFs.
+ try {
+ const ocrText = await this.extractMistralPdfTextWithOcr(input.base64);
+ if (ocrText.length >= 10) {
+ console.log(
+ `📄 Mistral grounding OCR: extracted ${ocrText.length} chars from scanned PDF`,
+ );
+ return ocrText.slice(0, 50000);
+ }
+ } catch (ocrError) {
+ console.warn(
+ "📄 PDF OCR fallback failed for Mistral grounding:",
+ ocrError instanceof Error ? ocrError.message : ocrError,
);
}
}
- // For images: try to extract text using Gemini OCR as grounding bridge.
- // This gives Groq the text content it needs since it can't read images.
- if (input.mimeType.startsWith("image/")) {
- try {
- const ocrText = await keyManager.execute(async (genAI) => {
- const model = genAI.getGenerativeModel({
- model: PRIMARY_ANALYSIS_MODEL,
- generationConfig: {
- temperature: 0,
- maxOutputTokens: 8192,
- },
- });
-
- const result = await model.generateContent([
- "Extract ALL text from this document image exactly as it appears. Preserve structure, formatting, and all content. Return ONLY the raw text, no JSON, no commentary.",
- {
- inlineData: {
- data: input.base64,
- mimeType: input.mimeType,
- },
- },
- ]);
-
- return result.response.text()?.trim() || "";
- });
-
- if (ocrText && ocrText.length > 50) {
- console.log(
- `🖼️ Groq grounding: extracted ${ocrText.length} chars from image via Gemini OCR bridge`,
- );
- return ocrText.slice(0, 50000);
- }
- } catch (error: any) {
- // Gemini OCR bridge failed (likely key exhaustion), continue without
- if (!error.message?.includes("CRITICAL_KEY_EXHAUSTION")) {
- console.warn(
- "Image grounding via Gemini OCR failed for Groq fallback; continuing without grounded text.",
- error,
- );
- }
- }
- }
+ // For images: Pixtral vision model handles images directly via
+ // generateWithMistralVision, so no grounding text extraction is needed.
+ // The calling code in generateAnalysisWithFallback routes images
+ // to the vision path instead of the text-only grounded path.
return "";
}
+ private static async extractMistralPdfTextWithOcr(
+ pdfBase64: string,
+ ): Promise {
+ if (!this.isMistralConfigured()) {
+ return "";
+ }
+
+ const body = {
+ model: MISTRAL_OCR_MODEL,
+ document: {
+ type: "document_url",
+ document_url: `data:application/pdf;base64,${pdfBase64}`,
+ },
+ include_image_base64: false,
+ };
+
+ const response = await fetch(MISTRAL_OCR_API_URL, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${MISTRAL_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const details = await response.text();
+ throw new Error(
+ `Mistral OCR API error ${response.status}: ${details.slice(0, 300)}`,
+ );
+ }
+
+ const json = (await response.json()) as {
+ text?: string;
+ pages?: Array<{
+ text?: string;
+ markdown?: string;
+ content?: string;
+ }>;
+ output?: Array<{
+ text?: string;
+ markdown?: string;
+ content?: string;
+ }>;
+ };
+
+ const pageTexts = [
+ ...(Array.isArray(json.pages) ? json.pages : []),
+ ...(Array.isArray(json.output) ? json.output : []),
+ ]
+ .map((page) => page.markdown || page.text || page.content || "")
+ .filter((value) => value.trim().length > 0);
+
+ const merged = [json.text || "", ...pageTexts]
+ .join("\n\n")
+ .replace(/\r/g, "\n")
+ .replace(/\n{3,}/g, "\n\n")
+ .trim();
+
+ return merged;
+ }
+
/**
* Emergency fallback: Extract key contract fields from raw text when JSON is completely malformed.
* Builds a minimal but valid JSON structure from pattern-matched fields.
@@ -1406,7 +1656,7 @@ Include one short disclaimer only when legal context is discussed: "This is gene
if (!rawAnswer) {
try {
- rawAnswer = await this.generateWithGroqModelChain({
+ rawAnswer = await this.generateWithMistralModelChain({
preferredModel: FALLBACK_ANALYSIS_MODEL,
systemPrompt: `You are a senior BFSI contract advisor. Answer questions about contracts accurately and professionally. Respond entirely in ${languageName}. Use plain text only — no markdown, no bold, no headers, no bullet points. Base your answers ONLY on the provided contract content. If information is missing, say so.`,
prompt,
@@ -1416,10 +1666,10 @@ Include one short disclaimer only when legal context is discussed: "This is gene
topP: 0.95,
});
console.log(
- `✅ Q&A fallback with Groq model ${FALLBACK_ANALYSIS_MODEL} succeeded in ${languageName}`,
+ `✅ Q&A fallback with Mistral model ${FALLBACK_ANALYSIS_MODEL} succeeded in ${languageName}`,
);
- } catch (groqError) {
- lastError = groqError;
+ } catch (mistralError) {
+ lastError = mistralError;
}
}
@@ -1444,11 +1694,11 @@ Include one short disclaimer only when legal context is discussed: "This is gene
const errorMessage =
error instanceof Error ? error.message : String(error);
if (errorMessage.includes("API key")) {
- throw new Error("Invalid or missing AI API key (Gemini/Groq).");
+ throw new Error("Invalid or missing AI API key (Gemini/Mistral).");
}
- if (this.isTransientGeminiError(errorMessage)) {
+ if (this.isTransientAIError(errorMessage)) {
throw new Error(
- `Gemini is temporarily overloaded for the configured Q&A models (${ANALYSIS_MODELS.join(", ")}). Please try again in a few minutes.`,
+ `The AI providers (Gemini/Mistral) are temporarily overloaded for the configured Q&A models (${ANALYSIS_MODELS.join(", ")}). Please try again in a few minutes.`,
);
}
throw new Error(`Error answering question: ${errorMessage}`);
diff --git a/lib/services/ai/analysis.normalizer.ts b/lib/services/ai/analysis.normalizer.ts
index a602c78..9e0b046 100644
--- a/lib/services/ai/analysis.normalizer.ts
+++ b/lib/services/ai/analysis.normalizer.ts
@@ -76,7 +76,12 @@ function toDateOrNull(value: unknown): string | null {
function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value
- .map((item) => String(item ?? "").trim())
+ .map((item) => {
+ if (typeof item === "object" && item !== null) {
+ return Object.values(item).filter(Boolean).join(" - ");
+ }
+ return String(item ?? "").trim();
+ })
.filter((item) => item.length > 0)
.slice(0, 25);
}
diff --git a/lib/services/email.service.ts b/lib/services/email.service.ts
new file mode 100644
index 0000000..f366c28
--- /dev/null
+++ b/lib/services/email.service.ts
@@ -0,0 +1,280 @@
+import nodemailer from "nodemailer";
+
+interface ContractBlueprint {
+ type: string;
+ provider: string | null;
+ policyNumber: string | null;
+ startDate: string | null;
+ endDate: string | null;
+ premium: number | null;
+ premiumCurrency: string | null;
+ summary: string;
+}
+
+interface BlockchainEmailData {
+ documentHash: string;
+ txHash: string;
+ blockNumber: number;
+ blockTimestamp: Date;
+ network: string;
+ contractAddress: string;
+ explorerUrl: string | null;
+}
+
+interface ContractAnalysisEmailInput {
+ to: string;
+ userDisplayName?: string | null;
+ contractId: string;
+ contractFileName: string;
+ contractTitle: string;
+ blueprint: ContractBlueprint;
+ blockchain?: BlockchainEmailData | null;
+}
+
+let transporter: nodemailer.Transporter | null = null;
+let transportMode: "smtp" | "ethereal" | null = null;
+let hasWarnedMissingEmailConfig = false;
+
+const asBoolean = (value: string | undefined, fallback: boolean): boolean => {
+ if (!value) return fallback;
+ return value.toLowerCase() === "true" || value === "1";
+};
+
+const isEmailConfigured = (): boolean => {
+ return Boolean(
+ process.env.EMAIL_HOST &&
+ process.env.EMAIL_PORT &&
+ process.env.EMAIL_USER &&
+ process.env.EMAIL_PASS,
+ );
+};
+
+const warnMissingEmailConfigOnce = () => {
+ if (hasWarnedMissingEmailConfig) return;
+ hasWarnedMissingEmailConfig = true;
+ console.warn(
+ "Email notifications are disabled. Configure EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, and MAIL_FROM to enable contract summary emails.",
+ );
+};
+
+const getTransporter = async (): Promise => {
+ if (transporter) {
+ return transporter;
+ }
+
+ if (isEmailConfigured()) {
+ transportMode = "smtp";
+ transporter = nodemailer.createTransport({
+ host: process.env.EMAIL_HOST,
+ port: Number(process.env.EMAIL_PORT),
+ secure: asBoolean(
+ process.env.EMAIL_SECURE,
+ Number(process.env.EMAIL_PORT) === 465,
+ ),
+ auth: {
+ user: process.env.EMAIL_USER,
+ pass: process.env.EMAIL_PASS,
+ },
+ });
+
+ return transporter;
+ }
+
+ if (process.env.NODE_ENV !== "production") {
+ const testAccount = await nodemailer.createTestAccount();
+ transportMode = "ethereal";
+ transporter = nodemailer.createTransport({
+ host: testAccount.smtp.host,
+ port: testAccount.smtp.port,
+ secure: testAccount.smtp.secure,
+ auth: {
+ user: testAccount.user,
+ pass: testAccount.pass,
+ },
+ });
+
+ console.warn(
+ "Email service is running in development fallback mode using Ethereal. Configure SMTP env vars for real inbox delivery.",
+ );
+
+ return transporter;
+ }
+
+ warnMissingEmailConfigOnce();
+ return null;
+};
+
+const formatPremium = (
+ premium: number | null,
+ currency: string | null,
+): string => {
+ if (premium === null || premium === undefined) return "N/A";
+ const formattedAmount = new Intl.NumberFormat("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(premium);
+
+ if (!currency) return formattedAmount;
+ if (["€", "$", "£"].includes(currency))
+ return `${currency}${formattedAmount}`;
+ return `${formattedAmount} ${currency}`;
+};
+
+const formatDateValue = (dateValue: string | null): string => {
+ if (!dateValue) return "N/A";
+ const date = new Date(dateValue);
+ if (Number.isNaN(date.getTime())) return dateValue;
+ return date.toISOString().split("T")[0];
+};
+
+const formatContractLink = (contractId: string): string | null => {
+ const baseUrl =
+ process.env.NEXT_PUBLIC_APP_URL?.trim() || process.env.APP_URL?.trim();
+ if (!baseUrl) return null;
+ return `${baseUrl.replace(/\/$/, "")}/contacts?contract=${contractId}`;
+};
+
+export class EmailService {
+ static async sendContractAnalysisCompletedEmail(
+ input: ContractAnalysisEmailInput,
+ ): Promise<{
+ success: boolean;
+ error?: string;
+ skipped?: boolean;
+ previewUrl?: string | null;
+ }> {
+ try {
+ const mailer = await getTransporter();
+ if (!mailer) {
+ return {
+ success: false,
+ skipped: true,
+ error: "Email service not configured",
+ };
+ }
+
+ const from =
+ process.env.MAIL_FROM?.trim() ||
+ process.env.EMAIL_USER?.trim() ||
+ (transportMode === "ethereal"
+ ? "LexiChain "
+ : "");
+ if (!from) {
+ warnMissingEmailConfigOnce();
+ return { success: false, skipped: true, error: "MAIL_FROM is missing" };
+ }
+
+ if (!input.to?.trim()) {
+ return {
+ success: false,
+ skipped: true,
+ error: "Recipient email is missing",
+ };
+ }
+
+ const recipientName = input.userDisplayName || "there";
+ const premiumLabel = formatPremium(
+ input.blueprint.premium,
+ input.blueprint.premiumCurrency,
+ );
+ const contractUrl = formatContractLink(input.contractId);
+ const blockchainStatus = input.blockchain
+ ? "Registered"
+ : "Not registered (blockchain unavailable or skipped)";
+
+ const textBody = [
+ `Hello ${recipientName},`,
+ "",
+ "Your contract analysis is complete.",
+ "",
+ "Blueprint:",
+ `- Contract title: ${input.contractTitle}`,
+ `- Original file: ${input.contractFileName}`,
+ `- Type: ${input.blueprint.type}`,
+ `- Provider: ${input.blueprint.provider ?? "N/A"}`,
+ `- Policy number: ${input.blueprint.policyNumber ?? "N/A"}`,
+ `- Start date: ${formatDateValue(input.blueprint.startDate)}`,
+ `- End date: ${formatDateValue(input.blueprint.endDate)}`,
+ `- Premium: ${premiumLabel}`,
+ "",
+ "Summary:",
+ input.blueprint.summary,
+ "",
+ "Blockchain proof:",
+ `- Status: ${blockchainStatus}`,
+ `- Document hash: ${input.blockchain?.documentHash ?? "N/A"}`,
+ `- Transaction hash: ${input.blockchain?.txHash ?? "N/A"}`,
+ `- Block number: ${input.blockchain?.blockNumber ?? "N/A"}`,
+ `- Block time: ${input.blockchain?.blockTimestamp?.toISOString() ?? "N/A"}`,
+ `- Network: ${input.blockchain?.network ?? "N/A"}`,
+ `- Contract address: ${input.blockchain?.contractAddress ?? "N/A"}`,
+ `- Explorer URL: ${input.blockchain?.explorerUrl ?? "N/A"}`,
+ "",
+ contractUrl ? `Open in app: ${contractUrl}` : "",
+ "",
+ "Keep this email for your records.",
+ ]
+ .filter(Boolean)
+ .join("\n");
+
+ const htmlBody = `
+
+
Contract Analysis Completed
+
Hello ${recipientName},
+
Your contract analysis has been completed successfully.
+
+
Blueprint
+
+ Contract title: ${input.contractTitle}
+ Original file: ${input.contractFileName}
+ Type: ${input.blueprint.type}
+ Provider: ${input.blueprint.provider ?? "N/A"}
+ Policy number: ${input.blueprint.policyNumber ?? "N/A"}
+ Start date: ${formatDateValue(input.blueprint.startDate)}
+ End date: ${formatDateValue(input.blueprint.endDate)}
+ Premium: ${premiumLabel}
+
+
+
Summary
+
${input.blueprint.summary.replace(/\n/g, " ")}
+
+
Blockchain Proof
+
+ Status: ${blockchainStatus}
+ Document hash: ${input.blockchain?.documentHash ?? "N/A"}
+ Transaction hash: ${input.blockchain?.txHash ?? "N/A"}
+ Block number: ${input.blockchain?.blockNumber ?? "N/A"}
+ Block time: ${input.blockchain?.blockTimestamp?.toISOString() ?? "N/A"}
+ Network: ${input.blockchain?.network ?? "N/A"}
+ Contract address: ${input.blockchain?.contractAddress ?? "N/A"}
+ Explorer URL: ${input.blockchain?.explorerUrl ? `Open transaction ` : "N/A"}
+
+
+ ${contractUrl ? `
Open this contract in your dashboard
` : ""}
+
Keep this email for your records.
+
+ `;
+
+ const info = await mailer.sendMail({
+ from,
+ to: input.to,
+ subject: `Contract analyzed: ${input.contractTitle}`,
+ text: textBody,
+ html: htmlBody,
+ });
+
+ const previewUrl = nodemailer.getTestMessageUrl(info);
+ if (previewUrl) {
+ console.log(`📨 Ethereal preview URL: ${previewUrl}`);
+ }
+
+ return { success: true, previewUrl };
+ } catch (error) {
+ console.error("Failed to send analysis completion email:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown email error",
+ };
+ }
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 2d49cef..090dd60 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,6 +40,7 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
+ "@types/nodemailer": "^8.0.0",
"@uploadthing/react": "^7.3.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -55,6 +56,7 @@
"motion": "^12.34.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
+ "nodemailer": "^8.0.7",
"pdf-parse": "^2.4.5",
"prisma": "^6.19.2",
"react": "19.2.3",
@@ -3714,12 +3716,20 @@
"version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/nodemailer": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz",
+ "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
@@ -7937,6 +7947,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nodemailer": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
+ "integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -9962,7 +9981,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
diff --git a/package.json b/package.json
index 73d3408..72399a2 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
+ "@types/nodemailer": "^8.0.0",
"@uploadthing/react": "^7.3.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -56,6 +57,7 @@
"motion": "^12.34.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
+ "nodemailer": "^8.0.7",
"pdf-parse": "^2.4.5",
"prisma": "^6.19.2",
"react": "19.2.3",
diff --git a/test-mistral.js b/test-mistral.js
new file mode 100644
index 0000000..05f5b0f
--- /dev/null
+++ b/test-mistral.js
@@ -0,0 +1,100 @@
+const fs = require('fs');
+
+const sysPrompt = `You are an expert contract analysis engine for the BFSI (Banking, Financial Services, and Insurance) sector.
+You receive the full text content of a contract document and must extract structured information from it.
+
+ABSOLUTE RULES — VIOLATION OF THESE IS A CRITICAL FAILURE:
+1. Return ONLY valid, parseable JSON — no markdown, no backticks, no explanations, no commentary.
+2. EVERY value you output MUST come directly from the document text provided to you.
+3. If a piece of information does NOT exist in the document text, you MUST use null (for strings/numbers) or [] (for arrays). NEVER invent, assume, or guess data.
+4. Do NOT copy example values from the schema description — they are placeholders, not real data.
+5. The "extractedText" field MUST contain actual verbatim text from the document — not a summary, not examples.
+
+JSON SCHEMA (use exact field names):
+{
+ "language": "",
+ "title": "",
+ "type": "",
+ "provider": "",
+ "policyNumber": "",
+ "startDate": "",
+ "endDate": "",
+ "premium": ,
+ "premiumCurrency": "",
+ "summary": "<4-6 sentences summarizing the actual contract content>",
+ "keyPoints": {
+ "guarantees": [""],
+ "exclusions": [""],
+ "franchise": "",
+ "importantDates": [""],
+ "explainability": [
+ {
+ "field": "",
+ "why": "",
+ "sourceSnippet": "",
+ "sourceHints": { "page": "", "section": "", "confidence": <0-100> }
+ }
+ ]
+ },
+ "keyPeople": [{"name": "", "role": "", "email": "", "phone": ""}],
+ "contactInfo": {"name": "", "email": null, "phone": null, "address": null, "role": null},
+ "importantContacts": [],
+ "relevantDates": [{"date": "", "description": "", "type": ""}],
+ "extractedText": "",
+ "contractValidation": {
+ "isValidContract": true,
+ "confidence": <0-100 reflecting how much data you actually found>,
+ "reason": null
+ }
+}
+
+FIELD RULES:
+- All dates: ISO YYYY-MM-DD or null
+- premium: positive number or null — NO currency symbols, NO text
+- type: must be exactly one of the 8 values listed
+- summary: 4-6 professional sentences about THIS specific contract
+- extractedText: must contain at least 30 characters of ACTUAL document content
+- explainability: at least 4 items with real sourceSnippets from the document
+- confidence: reflects how much data you actually found (not how confident the model is)
+- Parse localized number formats correctly (1.234,56 vs 1,234.56)
+- Detect the contract language and set "language" accordingly
+
+You are replacing a more capable multimodal model (Gemini) as a fallback. Your output quality MUST match production standards. ACCURACY is more important than completeness — it is better to return null than to guess.`;
+
+const prompt = `--- BEGIN GROUNDED DOCUMENT TEXT (AUTHORITATIVE SOURCE) ---
+CONFIDENTIALITY AGREEMENT
+This Confidentiality Agreement (the "Agreement") is entered into as of May 1, 2025 (the "Effective Date"), by and between Acme Corp ("Disclosing Party") and Beta Inc ("Receiving Party").
+1. Confidential Information. "Confidential Information" means all non-public information disclosed by the Disclosing Party to the Receiving Party.
+2. Obligations. The Receiving Party shall hold and maintain the Confidential Information in strictest confidence.
+3. Term. This Agreement shall remain in effect for a period of two (2) years from the Effective Date.
+Signatures:
+John Doe, CEO Acme Corp
+Jane Smith, VP Beta Inc
+--- END GROUNDED DOCUMENT TEXT ---
+
+MISTRAL FALLBACK RULES:
+- Extract fields ONLY from the grounded document text above. This text is the full contract content.
+- Do not invent, assume, or hallucinate any values not explicitly present in the above text.
+- If a field's data is not found in the text, use null (for strings/numbers) or [] (for arrays).
+- Dates: convert any date format found in the text to YYYY-MM-DD.
+- Numbers: parse localized formats (comma vs period) correctly before setting numeric fields.
+- contractValidation.confidence should reflect how much data you could extract from the text.
+IMPORTANT: Return ONLY valid JSON and preserve the required schema exactly. Do not add any text outside of the JSON object.`;
+
+fetch('https://api.mistral.ai/v1/chat/completions', {
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Bearer 7yRx3izDA2ECDblZvAaUoZhgQnYqiiKj',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: 'mistral-large-latest',
+ temperature: 0,
+ top_p: 1,
+ response_format: { type: 'json_object' },
+ messages: [
+ { role: 'system', content: sysPrompt },
+ { role: 'user', content: prompt }
+ ]
+ })
+}).then(r => r.json()).then(data => console.log(data.choices[0].message.content)).catch(console.error);