backup point before blockchain
This commit is contained in:
@@ -64,14 +64,18 @@ The AI subsystem is centered on:
|
|||||||
### 2.2 Models
|
### 2.2 Models
|
||||||
|
|
||||||
- Primary model: gemini-2.5-flash
|
- Primary model: gemini-2.5-flash
|
||||||
- Fallback model: gemini-2.0-flash
|
- Optional secondary Gemini model: AI_MODEL_SECONDARY_GEMINI
|
||||||
- Model list is de-duplicated and iterated in order
|
- Fallback model provider: Groq (default: llama-3.3-70b-versatile)
|
||||||
|
- Gemini models are de-duplicated and iterated in order before Groq fallback
|
||||||
|
- Groq extraction fallback currently applies to image inputs in this pipeline; JSON repair and Q&A fallback are text-based
|
||||||
|
|
||||||
### 2.3 Environment Variables
|
### 2.3 Environment Variables
|
||||||
|
|
||||||
- AI_API_KEY (or AI_API_KEY2 / AI_API_KEY3 fallback)
|
- AI_API_KEY (or AI_API_KEY2 / AI_API_KEY3 fallback)
|
||||||
- AI_MODEL_PRIMARY (optional override)
|
- AI_MODEL_PRIMARY (optional override)
|
||||||
|
- AI_MODEL_SECONDARY_GEMINI (optional override)
|
||||||
- AI_MODEL_FALLBACK (optional override)
|
- AI_MODEL_FALLBACK (optional override)
|
||||||
|
- GROQ_API_KEY (or AI_GROQ_API_KEY)
|
||||||
|
|
||||||
## 3. AI Capability Matrix
|
## 3. AI Capability Matrix
|
||||||
|
|
||||||
@@ -162,13 +166,13 @@ sequenceDiagram
|
|||||||
autonumber
|
autonumber
|
||||||
participant AIS as AIService
|
participant AIS as AIService
|
||||||
participant G1 as Gemini Primary
|
participant G1 as Gemini Primary
|
||||||
participant G2 as Gemini Fallback
|
participant G2 as Gemini Secondary (optional)
|
||||||
|
|
||||||
AIS->>G1: buildPrevalidationPrompt + inline file
|
AIS->>G1: buildPrevalidationPrompt + inline file
|
||||||
alt Primary succeeds
|
alt Primary succeeds
|
||||||
G1-->>AIS: JSON precheck
|
G1-->>AIS: JSON precheck
|
||||||
else Primary fails
|
else Primary fails
|
||||||
AIS->>G2: same precheck request
|
AIS->>G2: same precheck request (if configured)
|
||||||
G2-->>AIS: JSON precheck
|
G2-->>AIS: JSON precheck
|
||||||
end
|
end
|
||||||
AIS->>AIS: parse precheck JSON
|
AIS->>AIS: parse precheck JSON
|
||||||
@@ -201,26 +205,32 @@ sequenceDiagram
|
|||||||
autonumber
|
autonumber
|
||||||
participant AIS as AIService
|
participant AIS as AIService
|
||||||
participant GP as Gemini Primary
|
participant GP as Gemini Primary
|
||||||
participant GF as Gemini Fallback
|
participant GS as Gemini Secondary (optional)
|
||||||
|
participant GR as Groq Fallback
|
||||||
|
|
||||||
AIS->>GP: generate analysis (strict JSON)
|
AIS->>GP: generate analysis (strict JSON)
|
||||||
alt GP success with usable output
|
alt GP success with usable output
|
||||||
GP-->>AIS: text
|
GP-->>AIS: text
|
||||||
else GP fails
|
else GP fails
|
||||||
AIS->>GF: generate analysis (strict JSON)
|
AIS->>GS: generate analysis (strict JSON)
|
||||||
alt GF success
|
alt GS success
|
||||||
GF-->>AIS: text
|
GS-->>AIS: text
|
||||||
else GF fails
|
else GS fails
|
||||||
AIS->>GP: lenient generation attempt
|
AIS->>GP: lenient generation attempt
|
||||||
GP-->>AIS: raw text
|
alt lenient success
|
||||||
|
GP-->>AIS: raw text
|
||||||
|
else lenient fails
|
||||||
|
AIS->>GR: generate analysis (strict JSON)
|
||||||
|
GR-->>AIS: text
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
AIS->>AIS: parseJsonResponse
|
AIS->>AIS: parseJsonResponse
|
||||||
alt parse failed
|
alt parse failed
|
||||||
AIS->>GF: repairMalformedJson(originalText, parseError)
|
AIS->>GR: repairMalformedJson(originalText, parseError)
|
||||||
alt repair success
|
alt repair success
|
||||||
GF-->>AIS: repaired JSON text
|
GR-->>AIS: repaired JSON text
|
||||||
AIS->>AIS: parse repaired JSON
|
AIS->>AIS: parse repaired JSON
|
||||||
else repair failed
|
else repair failed
|
||||||
AIS->>AIS: emergencyExtractFields(rawText)
|
AIS->>AIS: emergencyExtractFields(rawText)
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ export default function DashboardPage() {
|
|||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
className="grid gap-8 xl:grid-cols-[1.45fr,0.95fr] xl:items-end"
|
className="grid gap-8 lg:grid-cols-[1.45fr,0.95fr] lg:items-end"
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="inline-flex items-center gap-2 rounded-full border border-primary/25 bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
|
<p className="inline-flex items-center gap-2 rounded-full border border-primary/25 bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
|
||||||
@@ -429,7 +429,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto max-w-7xl px-6 py-10">
|
<div className="mx-auto max-w-7xl px-6 py-10">
|
||||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<Card className="rounded-2xl border-border/60 bg-card/70 p-6">
|
<Card className="rounded-2xl border-border/60 bg-card/70 p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-muted-foreground">Total Files</p>
|
<p className="text-sm text-muted-foreground">Total Files</p>
|
||||||
@@ -583,7 +583,7 @@ export default function DashboardPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<div className="rounded-xl border border-border/50 bg-muted/25 px-4 py-3">
|
<div className="rounded-xl border border-border/50 bg-muted/25 px-4 py-3">
|
||||||
<p className="text-xs text-muted-foreground">Completed Samples</p>
|
<p className="text-xs text-muted-foreground">Completed Samples</p>
|
||||||
<p className="mt-1 text-2xl font-semibold text-foreground">
|
<p className="mt-1 text-2xl font-semibold text-foreground">
|
||||||
@@ -648,9 +648,9 @@ export default function DashboardPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{hasChartData ? (
|
{hasChartData ? (
|
||||||
<div className="mt-6 grid grid-cols-1 gap-5 xl:grid-cols-12">
|
<div className="mt-6 grid grid-cols-1 gap-5 lg:grid-cols-12">
|
||||||
{chartData && chartData.trends.length > 0 && (
|
{chartData && chartData.trends.length > 0 && (
|
||||||
<Card className="rounded-2xl border-border/60 p-5 xl:col-span-8">
|
<Card className="rounded-2xl border-border/60 p-5 lg:col-span-8">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<BarChart3 className="h-4 w-4 text-primary" />
|
<BarChart3 className="h-4 w-4 text-primary" />
|
||||||
<h2 className="text-sm font-medium">
|
<h2 className="text-sm font-medium">
|
||||||
@@ -664,7 +664,7 @@ export default function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{chartData && chartData.byStatus.length > 0 && (
|
{chartData && chartData.byStatus.length > 0 && (
|
||||||
<Card className="rounded-2xl border-border/60 p-5 xl:col-span-4">
|
<Card className="rounded-2xl border-border/60 p-5 lg:col-span-4">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 text-primary" />
|
<CheckCircle2 className="h-4 w-4 text-primary" />
|
||||||
<h2 className="text-sm font-medium">Processing Status</h2>
|
<h2 className="text-sm font-medium">Processing Status</h2>
|
||||||
@@ -681,7 +681,7 @@ export default function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{chartData && chartData.byType.length > 0 && (
|
{chartData && chartData.byType.length > 0 && (
|
||||||
<Card className="rounded-2xl border-border/60 p-5 xl:col-span-7">
|
<Card className="rounded-2xl border-border/60 p-5 lg:col-span-7">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<FileText className="h-4 w-4 text-primary" />
|
<FileText className="h-4 w-4 text-primary" />
|
||||||
<h2 className="text-sm font-medium">
|
<h2 className="text-sm font-medium">
|
||||||
@@ -694,7 +694,7 @@ export default function DashboardPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="rounded-2xl border-border/60 p-5 xl:col-span-5">
|
<Card className="rounded-2xl border-border/60 p-5 lg:col-span-5">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Sparkles className="h-4 w-4 text-primary" />
|
<Sparkles className="h-4 w-4 text-primary" />
|
||||||
<h2 className="text-sm font-medium">Recent Analyses</h2>
|
<h2 className="text-sm font-medium">Recent Analyses</h2>
|
||||||
|
|||||||
45
docs/ROUTING_ENDPOINTS.md
Normal file
45
docs/ROUTING_ENDPOINTS.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# API & Routing Endpoints
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Clerk Webhook
|
||||||
|
- **Path:** `/api/webhooks/clerk`
|
||||||
|
- **Method:** `POST`
|
||||||
|
- **Purpose:** Handles Clerk webhook events (authentication, user sync, etc.)
|
||||||
|
|
||||||
|
### 2. File Upload (UploadThing)
|
||||||
|
- **Path:** `/api/uploadthing`
|
||||||
|
- **Methods:** `GET`, `POST`
|
||||||
|
- **Purpose:** Handles file uploads (contracts, images) with authentication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protected App Routes (Require Login)
|
||||||
|
|
||||||
|
Defined in middleware ([proxy.ts](../proxy.ts)):
|
||||||
|
- `/dashboard` and all sub-pages
|
||||||
|
- `/contracts` and all sub-pages
|
||||||
|
- `/chat` and all sub-pages
|
||||||
|
- `/claims` and all sub-pages
|
||||||
|
- `/blockchain` and all sub-pages
|
||||||
|
- `/settings` and all sub-pages
|
||||||
|
- `/api/contracts` and all sub-pages
|
||||||
|
- `/api/chat`
|
||||||
|
- `/api/claims`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature API Actions (Server Actions)
|
||||||
|
|
||||||
|
These are called from the frontend, not as REST endpoints:
|
||||||
|
- `/features/contracts/api/contract.action.ts` (contract CRUD, AI analysis)
|
||||||
|
- `/features/analytics/api/stats.action.ts` (dashboard stats)
|
||||||
|
- `/features/notifications/api/notification.action.ts` (notifications)
|
||||||
|
- `/features/auth/api/user.action.ts` (user sync)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note:**
|
||||||
|
- All `/dashboard`, `/contracts`, `/chat`, `/claims`, `/blockchain`, `/settings` routes are protected and require authentication.
|
||||||
|
- API endpoints under `/api/` may also be protected by middleware.
|
||||||
|
- For more details, see the middleware configuration in [proxy.ts](../proxy.ts).
|
||||||
309
docs/implementation_plan.md
Normal file
309
docs/implementation_plan.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# Blockchain Integration — LexiChain BFSI Platform
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add a **fully functional, 100% free** blockchain module to the existing LexiChain platform. This module provides:
|
||||||
|
|
||||||
|
1. **Proof of Deposit** — SHA-256 hash of each contract document is stored on-chain with a timestamp, making submission dates provable and tamper-proof.
|
||||||
|
2. **On-chain Verification** — Anyone can verify that a document existed at a specific time.
|
||||||
|
3. **Blockchain Explorer UI** — A new `/blockchain` page showing all on-chain transactions + per-contract verification status.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Review Required
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Zero cost guaranteed.** The entire implementation uses:
|
||||||
|
> - **Hardhat local node** for development (free, instant, unlimited)
|
||||||
|
> - **Ethereum Sepolia testnet** for demo/presentation (free test ETH from faucets)
|
||||||
|
> - No MetaMask required for end-users — all blockchain operations run **server-side** via a backend wallet
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **Server-side wallet approach**: Instead of requiring users to install MetaMask and sign transactions, the server holds a private key and signs transactions on behalf of the platform. This is the right UX for a BFSI enterprise platform (users shouldn't need crypto knowledge). The private key is stored in `.env` and never exposed to the client.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Dual-mode architecture**:
|
||||||
|
> - `NODE_ENV=development` → Hardhat local node (`http://127.0.0.1:8545`)
|
||||||
|
> - `NODE_ENV=production` or env flag → Sepolia testnet (via free Alchemy/Infura RPC)
|
||||||
|
>
|
||||||
|
> You can switch between modes with a single env variable change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Changes
|
||||||
|
|
||||||
|
### Component 1: Smart Contract (Hardhat + Solidity)
|
||||||
|
|
||||||
|
Creates a standalone `blockchain/` directory at the project root with a Hardhat project for developing, testing, and deploying the smart contract.
|
||||||
|
|
||||||
|
#### [NEW] [blockchain/contracts/DocumentRegistry.sol](file:///c:/Stage/Project-PFE/bfsi-project/blockchain/contracts/DocumentRegistry.sol)
|
||||||
|
|
||||||
|
Solidity smart contract with:
|
||||||
|
- `registerDocument(bytes32 docHash, string calldata fileName)` — stores hash + metadata on-chain
|
||||||
|
- `verifyDocument(bytes32 docHash)` — checks if a hash exists and returns timestamp + depositor
|
||||||
|
- `getDocumentsByDepositor(address depositor)` — lists all docs registered by an address
|
||||||
|
- Events: `DocumentRegistered(bytes32 indexed docHash, uint256 timestamp, address indexed depositor, string fileName)`
|
||||||
|
- Modifier to prevent duplicate registrations
|
||||||
|
|
||||||
|
#### [NEW] [blockchain/hardhat.config.ts](file:///c:/Stage/Project-PFE/bfsi-project/blockchain/hardhat.config.ts)
|
||||||
|
|
||||||
|
Hardhat config with:
|
||||||
|
- Local Hardhat network (default, free)
|
||||||
|
- Sepolia network config (reads RPC URL + private key from env)
|
||||||
|
- Solidity 0.8.24 compiler
|
||||||
|
|
||||||
|
#### [NEW] [blockchain/test/DocumentRegistry.test.ts](file:///c:/Stage/Project-PFE/bfsi-project/blockchain/test/DocumentRegistry.test.ts)
|
||||||
|
|
||||||
|
Comprehensive tests:
|
||||||
|
- Register a document and verify timestamp
|
||||||
|
- Prevent duplicate registration
|
||||||
|
- Verify non-existent document returns zero
|
||||||
|
- Multiple documents by same depositor
|
||||||
|
|
||||||
|
#### [NEW] [blockchain/scripts/deploy.ts](file:///c:/Stage/Project-PFE/bfsi-project/blockchain/scripts/deploy.ts)
|
||||||
|
|
||||||
|
Deployment script that outputs the contract address for use in `.env`.
|
||||||
|
|
||||||
|
#### [NEW] [blockchain/package.json](file:///c:/Stage/Project-PFE/bfsi-project/blockchain/package.json)
|
||||||
|
|
||||||
|
Separate package.json for the Hardhat project (keeps blockchain dependencies isolated from the Next.js app).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Component 2: Next.js Blockchain Service Layer
|
||||||
|
|
||||||
|
Server-side service that connects to the blockchain from Next.js server actions. No browser wallet needed.
|
||||||
|
|
||||||
|
#### [NEW] [lib/services/blockchain.service.ts](file:///c:/Stage/Project-PFE/bfsi-project/lib/services/blockchain.service.ts)
|
||||||
|
|
||||||
|
Core blockchain service:
|
||||||
|
- `hashDocument(fileUrl: string): Promise<string>` — downloads contract PDF and computes SHA-256
|
||||||
|
- `registerOnChain(documentHash: string, fileName: string): Promise<{ txHash, blockNumber, blockTimestamp }>` — sends transaction to smart contract
|
||||||
|
- `verifyOnChain(documentHash: string): Promise<{ exists, timestamp, depositor }>` — reads on-chain data
|
||||||
|
- Uses `ethers.js v6` with `JsonRpcProvider` + `Wallet` (server-side, no MetaMask)
|
||||||
|
- Auto-detects network mode from env vars
|
||||||
|
|
||||||
|
#### [NEW] [lib/services/blockchain.types.ts](file:///c:/Stage/Project-PFE/bfsi-project/lib/services/blockchain.types.ts)
|
||||||
|
|
||||||
|
TypeScript types for blockchain data:
|
||||||
|
```typescript
|
||||||
|
interface BlockchainProof {
|
||||||
|
documentHash: string;
|
||||||
|
txHash: string;
|
||||||
|
blockNumber: number;
|
||||||
|
blockTimestamp: Date;
|
||||||
|
network: 'hardhat' | 'sepolia';
|
||||||
|
contractAddress: string;
|
||||||
|
explorerUrl: string | null; // Sepolia etherscan link
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Component 3: Server Actions & Integration
|
||||||
|
|
||||||
|
#### [NEW] [features/blockchain/api/blockchain.action.ts](file:///c:/Stage/Project-PFE/bfsi-project/features/blockchain/api/blockchain.action.ts)
|
||||||
|
|
||||||
|
New server actions:
|
||||||
|
- `registerContractOnBlockchain(contractId: string)` — hashes + registers + saves proof to DB
|
||||||
|
- `verifyContractOnBlockchain(contractId: string)` — verifies on-chain status
|
||||||
|
- `getBlockchainTransactions()` — fetches all blockchain proofs for the authenticated user
|
||||||
|
|
||||||
|
#### [MODIFY] [features/contracts/api/contract.action.ts](file:///c:/Stage/Project-PFE/bfsi-project/features/contracts/api/contract.action.ts)
|
||||||
|
|
||||||
|
After successful AI analysis, **automatically trigger** blockchain registration:
|
||||||
|
- In `analyzeContractAction()`, after `ContractService.updateWithAIResults()`, call `BlockchainService.hashAndRegister()`
|
||||||
|
- Save `documentHash`, `txHash` to the contract record
|
||||||
|
- This means every analyzed contract gets an automatic on-chain proof
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Component 4: Database Schema Update
|
||||||
|
|
||||||
|
#### [MODIFY] [prisma/schema.prisma](file:///c:/Stage/Project-PFE/bfsi-project/prisma/schema.prisma)
|
||||||
|
|
||||||
|
Expand the existing blockchain placeholder fields on `Contract`:
|
||||||
|
```diff
|
||||||
|
// Blockchain (later)
|
||||||
|
- documentHash String?
|
||||||
|
- txHash String?
|
||||||
|
- ipfsUrl String?
|
||||||
|
+ documentHash String?
|
||||||
|
+ txHash String?
|
||||||
|
+ blockNumber Int?
|
||||||
|
+ blockTimestamp DateTime?
|
||||||
|
+ blockchainNetwork String? // 'hardhat' | 'sepolia'
|
||||||
|
+ contractAddress String? // smart contract address used
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a new `BlockchainTransaction` model for the explorer view:
|
||||||
|
```prisma
|
||||||
|
model BlockchainTransaction {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(...)
|
||||||
|
contractId String
|
||||||
|
contract Contract @relation(...)
|
||||||
|
|
||||||
|
documentHash String
|
||||||
|
txHash String @unique
|
||||||
|
blockNumber Int
|
||||||
|
blockTimestamp DateTime
|
||||||
|
network String // 'hardhat' | 'sepolia'
|
||||||
|
contractAddress String
|
||||||
|
status String @default("CONFIRMED") // PENDING, CONFIRMED, FAILED
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Component 5: Frontend — Blockchain Explorer Page
|
||||||
|
|
||||||
|
#### [NEW] [app/(dashboard)/blockchain/page.tsx](file:///c:/Stage/Project-PFE/bfsi-project/app/(dashboard)/blockchain/page.tsx)
|
||||||
|
|
||||||
|
New dashboard page at `/blockchain` with:
|
||||||
|
- **Header stats**: Total verified contracts, latest block, network status
|
||||||
|
- **Transaction table**: All blockchain proofs with txHash, contract name, timestamp, verification status
|
||||||
|
- **Verification panel**: Paste a document hash to check its on-chain status
|
||||||
|
- **Network indicator**: Shows whether connected to Hardhat (local) or Sepolia
|
||||||
|
|
||||||
|
#### [NEW] [features/blockchain/components/blockchain-explorer.tsx](file:///c:/Stage/Project-PFE/bfsi-project/features/blockchain/components/blockchain-explorer.tsx)
|
||||||
|
|
||||||
|
Main explorer component with:
|
||||||
|
- Animated blockchain-themed cards
|
||||||
|
- Transaction list with expandable details
|
||||||
|
- Real-time verification status badges
|
||||||
|
- Network health indicator
|
||||||
|
|
||||||
|
#### [NEW] [features/blockchain/components/verify-document.tsx](file:///c:/Stage/Project-PFE/bfsi-project/features/blockchain/components/verify-document.tsx)
|
||||||
|
|
||||||
|
Standalone verification widget:
|
||||||
|
- File upload → compute hash → check on-chain
|
||||||
|
- Shows proof details if found (timestamp, block, depositor)
|
||||||
|
- Visual "Verified ✓" / "Not Found ✗" result
|
||||||
|
|
||||||
|
#### [NEW] [features/blockchain/components/blockchain-proof-badge.tsx](file:///c:/Stage/Project-PFE/bfsi-project/features/blockchain/components/blockchain-proof-badge.tsx)
|
||||||
|
|
||||||
|
Small badge component to show on contract cards:
|
||||||
|
- 🟢 "On-Chain Verified" (with txHash link)
|
||||||
|
- 🟡 "Pending" (registration in progress)
|
||||||
|
- ⚫ "Not Registered" (no blockchain proof yet)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Component 6: Navigation & Integration Updates
|
||||||
|
|
||||||
|
#### [MODIFY] [components/layout/navigation.tsx](file:///c:/Stage/Project-PFE/bfsi-project/components/layout/navigation.tsx)
|
||||||
|
|
||||||
|
Add new nav item:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
href: "/blockchain",
|
||||||
|
label: "Blockchain",
|
||||||
|
icon: <Link2 className="w-5 h-5" />,
|
||||||
|
description: "On-chain proofs",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### [MODIFY] [types/contract.types.ts](file:///c:/Stage/Project-PFE/bfsi-project/types/contract.types.ts)
|
||||||
|
|
||||||
|
Add blockchain fields to the Contract interface:
|
||||||
|
```typescript
|
||||||
|
blockNumber: number | null;
|
||||||
|
blockTimestamp: Date | null;
|
||||||
|
blockchainNetwork: string | null;
|
||||||
|
contractAddress: string | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### [MODIFY] [.env.example](file:///c:/Stage/Project-PFE/bfsi-project/.env.example)
|
||||||
|
|
||||||
|
Add blockchain env vars:
|
||||||
|
```env
|
||||||
|
# Blockchain
|
||||||
|
BLOCKCHAIN_PRIVATE_KEY= # Server wallet private key (Hardhat default for dev)
|
||||||
|
BLOCKCHAIN_RPC_URL= # RPC endpoint (empty = Hardhat local)
|
||||||
|
BLOCKCHAIN_CONTRACT_ADDRESS= # Deployed DocumentRegistry address
|
||||||
|
BLOCKCHAIN_NETWORK=hardhat # 'hardhat' or 'sepolia'
|
||||||
|
SEPOLIA_RPC_URL= # Free Alchemy/Infura Sepolia RPC
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
U[User Browser] --> UI[Next.js App Router]
|
||||||
|
|
||||||
|
UI -->|Server Action| BA[blockchain.action.ts]
|
||||||
|
BA --> BS[BlockchainService]
|
||||||
|
BS -->|ethers.js v6| BC[Smart Contract on Blockchain]
|
||||||
|
BS -->|Hash document| PDF[Contract PDF from UploadThing]
|
||||||
|
BA --> DB[(PostgreSQL - BlockchainTransaction)]
|
||||||
|
|
||||||
|
UI -->|Server Action| CA[contract.action.ts]
|
||||||
|
CA -->|After AI analysis| BA
|
||||||
|
|
||||||
|
subgraph "Local Dev"
|
||||||
|
HN[Hardhat Node :8545]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Demo/Production"
|
||||||
|
SN[Sepolia Testnet]
|
||||||
|
end
|
||||||
|
|
||||||
|
BC -.-> HN
|
||||||
|
BC -.-> SN
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
| Step | Component | Estimated Effort |
|
||||||
|
|------|-----------|-----------------|
|
||||||
|
| 1 | Hardhat project + Smart Contract + Tests | ~30 min |
|
||||||
|
| 2 | Deploy to local Hardhat node | ~5 min |
|
||||||
|
| 3 | `BlockchainService` (server-side ethers.js) | ~30 min |
|
||||||
|
| 4 | Prisma schema update + migration | ~10 min |
|
||||||
|
| 5 | Server actions (`blockchain.action.ts`) | ~20 min |
|
||||||
|
| 6 | Integration into `analyzeContractAction` | ~10 min |
|
||||||
|
| 7 | Blockchain Explorer page + components | ~45 min |
|
||||||
|
| 8 | Navigation update + proof badges | ~15 min |
|
||||||
|
| 9 | Deploy to Sepolia testnet (optional, for demo) | ~15 min |
|
||||||
|
|
||||||
|
**Total: ~3 hours of implementation**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **1. Auto-register vs. Manual button?**
|
||||||
|
> The plan currently auto-registers contracts on the blockchain after AI analysis completes. Alternatively, we could add a manual "Register on Blockchain" button per contract. Which do you prefer? (I recommend **both**: auto-register + manual re-register option)
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **2. Sepolia for presentation?**
|
||||||
|
> Do you want me to also set up the Sepolia testnet deployment for your PFE presentation/jury? This gives you real Etherscan links to show. You'll just need to grab free Sepolia ETH from a faucet (takes 2 minutes). We can start with Hardhat local and add Sepolia later.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **3. IPFS integration?**
|
||||||
|
> Your schema has an `ipfsUrl` field. Do you want to also store contract files on IPFS (using a free service like Pinata with 1GB free tier)? This would give you decentralized file storage + on-chain hash. Or should we skip this to keep things simpler?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Plan
|
||||||
|
|
||||||
|
### Automated Tests
|
||||||
|
1. **Smart contract tests** — `cd blockchain && npx hardhat test` (tests register, verify, duplicate prevention)
|
||||||
|
2. **Integration test** — Upload a contract → AI analysis → verify blockchain fields are populated in DB
|
||||||
|
3. **Service test** — Call `BlockchainService.hashDocument()` + `registerOnChain()` directly
|
||||||
|
|
||||||
|
### Manual Verification
|
||||||
|
1. Start Hardhat node → deploy contract → upload a contract in the app → check `/blockchain` page shows the transaction
|
||||||
|
2. Use the verification panel to re-verify a document hash
|
||||||
|
3. Check the contract detail view shows the "On-Chain Verified" badge
|
||||||
|
4. If Sepolia is configured: verify the txHash on [Sepolia Etherscan](https://sepolia.etherscan.io/)
|
||||||
|
|
||||||
@@ -428,10 +428,15 @@ export async function analyzeContractAction(id: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Analyze with AI
|
// Analyze with AI
|
||||||
|
const forceFallbackModelTest =
|
||||||
|
process.env.AI_FORCE_FALLBACK_TEST === "1" ||
|
||||||
|
String(process.env.AI_FORCE_FALLBACK_TEST).toLowerCase() === "true";
|
||||||
|
|
||||||
const aiResults = await AIService.analyzeContract(contract.fileUrl, {
|
const aiResults = await AIService.analyzeContract(contract.fileUrl, {
|
||||||
userId: contract.userId,
|
userId: contract.userId,
|
||||||
fileName: contract.fileName,
|
fileName: contract.fileName,
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
|
forceFallbackModelTest,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate results
|
// Validate results
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ContractChatModal } from "@/features/contracts/components/modals/contract-chat-modal";
|
import { ContractChatModal } from "@/features/contracts/components/modals/contract-chat-modal";
|
||||||
import { ContractProofModal } from "@/features/contracts/components/modals/contract-proof-modal";
|
import { ContractProofModal } from "@/features/contracts/components/modals/contract-proof-modal";
|
||||||
import { stripMarkdown, exportToCSV, exportToPDF } from "@/features/contracts/utils/export.utils";
|
import {
|
||||||
|
stripMarkdown,
|
||||||
|
exportToCSV,
|
||||||
|
exportToPDF,
|
||||||
|
} from "@/features/contracts/utils/export.utils";
|
||||||
|
|
||||||
interface Contract {
|
interface Contract {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1080,12 +1084,12 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
|
|||||||
title="Download contract"
|
title="Download contract"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (contract.fileUrl) {
|
if (contract.fileUrl) {
|
||||||
const downloadUrl = contract.fileUrl + "?download=1";
|
|
||||||
|
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = downloadUrl;
|
link.href = contract.fileUrl;
|
||||||
link.download =
|
link.download =
|
||||||
contract.fileUrl.split("/").pop() || "contract";
|
contract.fileUrl.split("/").pop() || "contract";
|
||||||
|
link.target = "_blank";
|
||||||
|
link.rel = "noopener noreferrer";
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
@@ -1307,7 +1311,8 @@ export function ContractsList({ refreshTrigger }: { refreshTrigger?: number }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
|
<p className="mt-2 min-h-[62px] rounded-xl border border-white/10 dark:border-white/5 bg-background/50 px-3 py-2 font-medium text-foreground whitespace-pre-wrap break-words shadow-inner">
|
||||||
{stripMarkdown(selectedContract.policyNumber) || "N/A"}
|
{stripMarkdown(selectedContract.policyNumber) ||
|
||||||
|
"N/A"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
|
<div className="flex min-h-[120px] flex-col rounded-2xl border border-border/30 bg-muted/20 px-3 py-3 backdrop-blur-md transition-all duration-300 hover:bg-muted/40 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30">
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
||||||
import { Sparkles, Github, Twitter, Linkedin, Mail } from "lucide-react";
|
import { Github, Twitter, Linkedin, Mail } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
// Social Icon Component
|
// ==========================================
|
||||||
|
// Composant : Icône Sociale Premium
|
||||||
|
// ==========================================
|
||||||
function SocialIcon({
|
function SocialIcon({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
href,
|
href,
|
||||||
@@ -20,13 +22,20 @@ function SocialIcon({
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className="text-slate-500 hover:text-slate-900 dark:hover:text-white transition-colors duration-200"
|
className="group relative flex items-center justify-center w-10 h-10 rounded-full bg-slate-50 dark:bg-white/[0.03] border border-slate-200 dark:border-white/10 transition-all duration-300 hover:-translate-y-1 hover:border-primary/50 hover:shadow-[0_0_20px_rgba(var(--primary),0.2)]"
|
||||||
>
|
>
|
||||||
<Icon className="w-5 h-5" />
|
{/* Lueur d'arrière-plan au survol */}
|
||||||
|
<div className="absolute inset-0 rounded-full bg-primary/10 opacity-0 group-hover:opacity-100 blur-md transition-opacity duration-300" />
|
||||||
|
|
||||||
|
{/* Icône */}
|
||||||
|
<Icon className="w-4 h-4 text-slate-500 dark:text-slate-400 group-hover:text-primary relative z-10 transition-colors duration-300" />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Composant Principal : Footer
|
||||||
|
// ==========================================
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const { ref, isVisible } = useScrollAnimation<HTMLElement>({
|
const { ref, isVisible } = useScrollAnimation<HTMLElement>({
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
@@ -42,60 +51,66 @@ export function Footer() {
|
|||||||
<footer
|
<footer
|
||||||
id="footer"
|
id="footer"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="relative bg-white dark:bg-slate-950 border-t border-slate-200 dark:border-slate-800 py-12 px-4 sm:px-6 lg:px-8"
|
className="relative overflow-hidden bg-background pt-20 pb-12 px-4 sm:px-6 lg:px-8"
|
||||||
>
|
>
|
||||||
|
{/* Effet Wow 1 : Ligne de démarcation en dégradé */}
|
||||||
|
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-foreground/20 dark:via-foreground/15 to-transparent" />
|
||||||
|
|
||||||
|
{/* Effet Wow 2 : Lueur d'ambiance à la base de la page */}
|
||||||
|
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[800px] h-[300px] bg-[radial-gradient(ellipse_at_bottom,hsla(var(--primary),0.15)_0%,transparent_60%)] pointer-events-none" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="max-w-7xl mx-auto"
|
className="relative z-10 max-w-7xl mx-auto"
|
||||||
style={{
|
style={{
|
||||||
opacity: isVisible ? 1 : 0,
|
opacity: isVisible ? 1 : 0,
|
||||||
transform: isVisible ? "translateY(0)" : "translateY(20px)",
|
transform: isVisible ? "translateY(0)" : "translateY(30px)",
|
||||||
transition: "all 0.6s ease-out",
|
transition: "all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Main Content */}
|
<div className="flex flex-col items-center space-y-10">
|
||||||
<div className="flex flex-col items-center space-y-8">
|
{/* Logo & Nom de la marque avec Lueur */}
|
||||||
{/* Logo */}
|
<a href="#" className="inline-flex flex-col items-center gap-3 group">
|
||||||
<a href="#" className="inline-flex items-center gap-2 group">
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
{/* Lueur sous le logo */}
|
||||||
|
<div className="absolute inset-0 bg-primary/20 blur-xl rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
<Image
|
<Image
|
||||||
src="/LexiChain.png"
|
src="/LexiChain.png"
|
||||||
alt="LexiChain Logo"
|
alt="LexiChain Logo"
|
||||||
width={48}
|
width={56}
|
||||||
height={48}
|
height={56}
|
||||||
className="
|
className="relative z-10 w-14 h-14 object-contain transition-transform duration-500 ease-out group-hover:scale-110"
|
||||||
relative z-10
|
|
||||||
w-12 h-12 object-contain
|
|
||||||
transition-all duration-300 ease-out
|
|
||||||
group-hover:scale-110
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-xl font-bold tracking-tight text-foreground/90 group-hover:text-foreground transition-colors duration-300">
|
||||||
|
LexiChain
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Navigation Links */}
|
{/* Liens de Navigation avec animation de soulignement */}
|
||||||
<nav className="flex items-center gap-8">
|
<nav className="flex items-center gap-8 md:gap-12">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<a
|
<a
|
||||||
key={link.label}
|
key={link.label}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="text-sm text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors duration-200"
|
className="relative text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-300 group py-1"
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
|
{/* Ligne d'animation au survol */}
|
||||||
|
<span className="absolute bottom-0 left-0 w-0 h-[2px] bg-primary transition-all duration-300 ease-out group-hover:w-full rounded-full" />
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Séparateur minimaliste */}
|
||||||
<div className="w-full max-w-4xl h-px bg-slate-200 dark:bg-slate-800" />
|
<div className="w-full max-w-md h-px bg-gradient-to-r from-transparent via-border to-transparent" />
|
||||||
|
|
||||||
{/* Bottom Row */}
|
{/* Section Inférieure (Copyright & Réseaux) */}
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex flex-col md:flex-row items-center justify-between w-full max-w-4xl gap-6">
|
||||||
{/* Copyright */}
|
<p className="text-sm font-medium text-muted-foreground/60 flex items-center gap-2">
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-500">
|
<span className="w-2 h-2 rounded-full bg-primary/80 animate-pulse" />
|
||||||
© LexiChain
|
© {new Date().getFullYear()} LexiChain. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Social Icons */}
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<SocialIcon
|
<SocialIcon
|
||||||
icon={Twitter}
|
icon={Twitter}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import {
|
import { Target, Zap, Shield, Lock, TrendingUp } from "lucide-react";
|
||||||
Target,
|
|
||||||
Zap,
|
|
||||||
Shield,
|
|
||||||
Lock,
|
|
||||||
TrendingUp,
|
|
||||||
Award,
|
|
||||||
Check,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { BentoGrid } from "@/components/ui/bento-grid";
|
import { BentoGrid } from "@/components/ui/bento-grid";
|
||||||
import { Spotlight } from "@/components/ui/spotlight-new";
|
import { Spotlight } from "@/components/ui/spotlight-new";
|
||||||
import { GlowingEffect } from "@/components/ui/glowing-effect";
|
import { GlowingEffect } from "@/components/ui/glowing-effect";
|
||||||
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
||||||
|
|
||||||
// Count Up Hook
|
// ==========================================
|
||||||
|
// Hooks (Kept your optimized hook)
|
||||||
|
// ==========================================
|
||||||
function useCountUp(
|
function useCountUp(
|
||||||
end: number,
|
end: number,
|
||||||
duration: number = 2000,
|
duration: number = 2000,
|
||||||
@@ -26,63 +20,44 @@ function useCountUp(
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!startOnView) {
|
if (!startOnView) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
if (entry.isIntersecting && !hasStarted) {
|
if (entry.isIntersecting && !hasStarted) setHasStarted(true);
|
||||||
setHasStarted(true);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ threshold: 0.5 },
|
{ threshold: 0.5 },
|
||||||
);
|
);
|
||||||
|
if (ref.current) observer.observe(ref.current);
|
||||||
if (ref.current) {
|
|
||||||
observer.observe(ref.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [hasStarted, startOnView]);
|
}, [hasStarted, startOnView]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasStarted) return;
|
if (!hasStarted || end === 0) return;
|
||||||
|
|
||||||
let startTime: number | null = null;
|
let startTime: number | null = null;
|
||||||
let animationFrame: number;
|
let animationFrame: number;
|
||||||
|
|
||||||
const animate = (timestamp: number) => {
|
const animate = (timestamp: number) => {
|
||||||
if (!startTime) startTime = timestamp;
|
if (!startTime) startTime = timestamp;
|
||||||
const progress = Math.min((timestamp - startTime) / duration, 1);
|
const progress = Math.min((timestamp - startTime) / duration, 1);
|
||||||
|
const easeOut = 1 - Math.pow(1 - progress, 4); // Smoother cubic out
|
||||||
// Ease out cubic
|
|
||||||
const easeOut = 1 - Math.pow(1 - progress, 3);
|
|
||||||
const nextValue = end * easeOut;
|
const nextValue = end * easeOut;
|
||||||
const formattedValue = Number.isInteger(end)
|
setCount(
|
||||||
? Math.floor(nextValue)
|
Number.isInteger(end)
|
||||||
: Number(nextValue.toFixed(1));
|
? Math.floor(nextValue)
|
||||||
setCount(formattedValue);
|
: Number(nextValue.toFixed(1)),
|
||||||
|
);
|
||||||
if (progress < 1) {
|
if (progress < 1) animationFrame = requestAnimationFrame(animate);
|
||||||
animationFrame = requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
animationFrame = requestAnimationFrame(animate);
|
animationFrame = requestAnimationFrame(animate);
|
||||||
|
return () => cancelAnimationFrame(animationFrame);
|
||||||
return () => {
|
|
||||||
if (animationFrame) {
|
|
||||||
cancelAnimationFrame(animationFrame);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [hasStarted, end, duration]);
|
}, [hasStarted, end, duration]);
|
||||||
|
|
||||||
return { count, ref };
|
return { count, ref };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat Card Component
|
// ==========================================
|
||||||
interface StatCardProps {
|
// Types
|
||||||
|
// ==========================================
|
||||||
|
interface StatItem {
|
||||||
value: string;
|
value: string;
|
||||||
numericValue?: number;
|
numericValue?: number;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
@@ -91,7 +66,6 @@ interface StatCardProps {
|
|||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
gradient: string;
|
gradient: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
cardColor?: string;
|
|
||||||
glowColor?: string;
|
glowColor?: string;
|
||||||
spotlight?: {
|
spotlight?: {
|
||||||
gradientFirst?: string;
|
gradientFirst?: string;
|
||||||
@@ -100,11 +74,13 @@ interface StatCardProps {
|
|||||||
duration?: number;
|
duration?: number;
|
||||||
xOffset?: number;
|
xOffset?: number;
|
||||||
};
|
};
|
||||||
delay: number;
|
|
||||||
additional?: string;
|
additional?: string;
|
||||||
isText?: boolean;
|
isText?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// High-Impact Stat Card
|
||||||
|
// ==========================================
|
||||||
function StatCard({
|
function StatCard({
|
||||||
value,
|
value,
|
||||||
numericValue,
|
numericValue,
|
||||||
@@ -114,143 +90,130 @@ function StatCard({
|
|||||||
icon: Icon,
|
icon: Icon,
|
||||||
gradient,
|
gradient,
|
||||||
className,
|
className,
|
||||||
cardColor,
|
|
||||||
glowColor,
|
glowColor,
|
||||||
spotlight,
|
spotlight,
|
||||||
delay,
|
delay,
|
||||||
additional,
|
additional,
|
||||||
isText = false,
|
isText = false,
|
||||||
}: StatCardProps) {
|
}: StatItem & { delay: number }) {
|
||||||
const { ref: scrollRef, isVisible } = useScrollAnimation<HTMLDivElement>({
|
const { ref: scrollRef, isVisible } = useScrollAnimation<HTMLDivElement>({
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
});
|
});
|
||||||
const { count, ref: countRef } = useCountUp(numericValue || 0, 2000);
|
const { count, ref: countRef } = useCountUp(numericValue || 0, 2500);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className={`relative ${className || ""}`}
|
className={`relative group h-full ${className || ""}`}
|
||||||
style={{
|
style={{
|
||||||
opacity: isVisible ? 1 : 0,
|
opacity: isVisible ? 1 : 0,
|
||||||
transform: isVisible ? "translateY(0)" : "translateY(30px)",
|
transform: isVisible
|
||||||
transition: `all 0.6s ease-out ${delay}s`,
|
? "translateY(0) scale(1)"
|
||||||
|
: "translateY(40px) scale(0.95)",
|
||||||
|
transition: `all 0.8s cubic-bezier(0.22, 1, 0.36, 1) ${delay}s`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Outer Glow behind the card */}
|
||||||
<div
|
<div
|
||||||
className={`relative h-full overflow-hidden rounded-2xl border border-border/60 p-7 shadow-[0_20px_60px_-30px_rgba(15,23,42,0.25)] backdrop-blur group ${cardColor || "bg-card/80"}`}
|
className={`absolute -inset-0.5 rounded-3xl blur-2xl opacity-0 group-hover:opacity-40 transition-opacity duration-700 ${gradient}`}
|
||||||
>
|
/>
|
||||||
{/* Glowing Effect */}
|
|
||||||
<GlowingEffect
|
|
||||||
disabled={false}
|
|
||||||
blur={40}
|
|
||||||
spread={60}
|
|
||||||
proximity={80}
|
|
||||||
variant="default"
|
|
||||||
borderWidth={2}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute inset-0">
|
{/* The Card Body:
|
||||||
<Spotlight
|
Gradient border wrapper + inner glassmorphism
|
||||||
gradientFirst={spotlight?.gradientFirst}
|
*/}
|
||||||
gradientSecond={spotlight?.gradientSecond}
|
<div className="relative h-full rounded-[22px] p-[1px] bg-gradient-to-b from-white/20 via-white/5 to-transparent overflow-hidden">
|
||||||
gradientThird={spotlight?.gradientThird}
|
<div className="relative h-full rounded-[21px] bg-card/60 dark:bg-black/40 backdrop-blur-2xl p-8 overflow-hidden shadow-2xl flex flex-col justify-between">
|
||||||
duration={spotlight?.duration ?? 8}
|
<GlowingEffect
|
||||||
xOffset={spotlight?.xOffset ?? 120}
|
blur={40}
|
||||||
|
spread={60}
|
||||||
|
proximity={80}
|
||||||
|
variant="default"
|
||||||
|
borderWidth={2}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity duration-700"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hover Glow */}
|
<div className="absolute inset-0 pointer-events-none mix-blend-screen">
|
||||||
{glowColor && (
|
<Spotlight
|
||||||
<div
|
{...spotlight}
|
||||||
className={`absolute -inset-1 ${glowColor} opacity-0 group-hover:opacity-20 blur-2xl transition-opacity duration-500 rounded-2xl`}
|
duration={spotlight?.duration ?? 8}
|
||||||
/>
|
xOffset={spotlight?.xOffset ?? 120}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
{/* Background Icon */}
|
|
||||||
<div className="absolute -top-6 -right-6 opacity-10">
|
|
||||||
<Icon className="w-28 h-28 text-foreground" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="relative z-10 flex h-full flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
{/* Icon */}
|
|
||||||
<div
|
|
||||||
className={`inline-flex items-center gap-2 rounded-full ${gradient} px-4 py-2 text-foreground shadow-lg`}
|
|
||||||
>
|
|
||||||
<Icon className="w-4 h-4" />
|
|
||||||
<span className="text-xs font-semibold tracking-widest uppercase text-foreground/80">
|
|
||||||
Performance
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Value */}
|
|
||||||
<div ref={countRef} className="mt-6">
|
|
||||||
{isText ? (
|
|
||||||
<span className="text-4xl md:text-5xl lg:text-6xl font-semibold text-foreground tracking-tight">
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-4xl md:text-5xl lg:text-6xl font-semibold text-foreground tracking-tight">
|
|
||||||
{prefix}
|
|
||||||
{numericValue !== undefined && !Number.isInteger(numericValue)
|
|
||||||
? count.toFixed(1)
|
|
||||||
: numericValue !== undefined
|
|
||||||
? count
|
|
||||||
: value}
|
|
||||||
{suffix}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Label */}
|
|
||||||
<p className="mt-4 text-base md:text-lg font-medium text-foreground/90">
|
|
||||||
{label}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Additional Info */}
|
{/* Majestic Background Icon */}
|
||||||
{additional && (
|
<div className="absolute -top-10 -right-10 opacity-5 dark:opacity-10 transition-transform duration-1000 ease-out group-hover:scale-125 group-hover:-rotate-12 pointer-events-none">
|
||||||
<div className="flex items-center gap-2 pt-6">
|
<Icon
|
||||||
<TrendingUp className="w-4 h-4 text-emerald-500" />
|
className={`w-56 h-56 ${glowColor?.replace("bg-", "text-")}`}
|
||||||
<span className="text-sm text-muted-foreground">
|
aria-hidden="true"
|
||||||
{additional}
|
/>
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="relative z-10 flex flex-col h-full">
|
||||||
|
<div>
|
||||||
|
{/* Vibrant Badge */}
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center gap-2 rounded-full ${gradient} px-4 py-1.5 shadow-[0_0_20px_rgba(0,0,0,0.3)] ring-1 ring-white/20`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4 text-white drop-shadow-md" />
|
||||||
|
<span className="text-[11px] font-black tracking-widest uppercase text-white drop-shadow-md">
|
||||||
|
Performance
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Massive Gradient Text */}
|
||||||
|
<div ref={countRef} className="mt-8 mb-2">
|
||||||
|
{isText ? (
|
||||||
|
<span className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-b from-foreground to-foreground/50">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-b from-foreground to-foreground/50 flex items-baseline">
|
||||||
|
{prefix}
|
||||||
|
{numericValue !== undefined &&
|
||||||
|
!Number.isInteger(numericValue)
|
||||||
|
? count.toFixed(1)
|
||||||
|
: numericValue !== undefined
|
||||||
|
? count
|
||||||
|
: value}
|
||||||
|
<span className="text-4xl md:text-5xl text-foreground/40 ml-1 font-semibold">
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-lg font-medium text-foreground/80 tracking-wide">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* Glowing Additional Info */}
|
||||||
|
{additional && (
|
||||||
|
<div className="flex items-center gap-3 pt-8 mt-auto">
|
||||||
|
<div className="relative flex items-center justify-center w-8 h-8 rounded-full bg-emerald-500/20 ring-1 ring-emerald-500/40">
|
||||||
|
<div className="absolute inset-0 rounded-full bg-emerald-500/20 blur-md animate-pulse" />
|
||||||
|
<TrendingUp className="w-4 h-4 text-emerald-400 relative z-10" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-semibold text-foreground/70 tracking-wide uppercase">
|
||||||
|
{additional}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Main Component
|
||||||
|
// ==========================================
|
||||||
export function Stats() {
|
export function Stats() {
|
||||||
const { ref: headerRef, isVisible: headerVisible } =
|
const { ref: headerRef, isVisible: headerVisible } =
|
||||||
useScrollAnimation<HTMLDivElement>();
|
useScrollAnimation<HTMLDivElement>();
|
||||||
|
|
||||||
interface StatItem {
|
|
||||||
value: string;
|
|
||||||
numericValue?: number;
|
|
||||||
suffix?: string;
|
|
||||||
prefix?: string;
|
|
||||||
label: string;
|
|
||||||
icon: React.ElementType;
|
|
||||||
gradient: string;
|
|
||||||
className?: string;
|
|
||||||
cardColor?: string;
|
|
||||||
glowColor?: string;
|
|
||||||
spotlight?: {
|
|
||||||
gradientFirst?: string;
|
|
||||||
gradientSecond?: string;
|
|
||||||
gradientThird?: string;
|
|
||||||
duration?: number;
|
|
||||||
xOffset?: number;
|
|
||||||
};
|
|
||||||
additional?: string;
|
|
||||||
isText?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats: StatItem[] = [
|
const stats: StatItem[] = [
|
||||||
{
|
{
|
||||||
value: "99.9",
|
value: "99.9",
|
||||||
@@ -258,35 +221,27 @@ export function Stats() {
|
|||||||
suffix: "%",
|
suffix: "%",
|
||||||
label: "OCR + AI Accuracy",
|
label: "OCR + AI Accuracy",
|
||||||
icon: Target,
|
icon: Target,
|
||||||
gradient: "bg-gradient-to-r from-blue-500 to-indigo-600",
|
gradient: "bg-gradient-to-r from-cyan-500 to-blue-600",
|
||||||
cardColor:
|
glowColor: "bg-cyan-500",
|
||||||
"bg-gradient-to-br from-blue-50/80 to-indigo-50/80 dark:from-blue-950/30 dark:to-indigo-950/30",
|
|
||||||
glowColor: "bg-blue-500",
|
|
||||||
className: "md:col-span-2",
|
className: "md:col-span-2",
|
||||||
spotlight: {
|
spotlight: {
|
||||||
gradientFirst:
|
gradientFirst:
|
||||||
"radial-gradient(68.54% 68.72% at 55.02% 31.46%, hsla(217, 91%, 60%, .28) 0, hsla(217, 91%, 60%, .12) 55%, hsla(217, 91%, 60%, 0) 80%)",
|
"radial-gradient(68% 68% at 55% 31%, hsla(217, 100%, 60%, 0.4) 0, transparent 80%)",
|
||||||
gradientSecond:
|
|
||||||
"radial-gradient(50% 50% at 50% 50%, hsla(258, 90%, 66%, .24) 0, hsla(258, 90%, 66%, .08) 80%, transparent 100%)",
|
|
||||||
gradientThird:
|
|
||||||
"radial-gradient(50% 50% at 50% 50%, hsla(217, 91%, 60%, .18) 0, hsla(217, 91%, 60%, .06) 80%, transparent 100%)",
|
|
||||||
},
|
},
|
||||||
additional: "+0.3% this month",
|
additional: "+0.3% this month",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "< 3",
|
value: "< 10",
|
||||||
label: "Average AI Response Time",
|
label: "Avg. AI Response Time",
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
gradient: "bg-gradient-to-r from-amber-500 to-orange-600",
|
gradient: "bg-gradient-to-r from-orange-500 to-amber-600",
|
||||||
cardColor:
|
glowColor: "bg-orange-500",
|
||||||
"bg-gradient-to-br from-amber-50/80 to-orange-50/80 dark:from-amber-950/30 dark:to-orange-950/30",
|
isText: true,
|
||||||
glowColor: "bg-amber-500",
|
|
||||||
spotlight: {
|
spotlight: {
|
||||||
gradientFirst:
|
gradientFirst:
|
||||||
"radial-gradient(68.54% 68.72% at 55.02% 31.46%, hsla(48, 96%, 53%, .28) 0, hsla(48, 96%, 53%, .12) 55%, hsla(48, 96%, 53%, 0) 80%)",
|
"radial-gradient(68% 68% at 55% 31%, hsla(30, 100%, 60%, 0.4) 0, transparent 80%)",
|
||||||
},
|
},
|
||||||
additional: "Average under 3 seconds",
|
additional: "Under 10 seconds",
|
||||||
isText: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "100",
|
value: "100",
|
||||||
@@ -294,93 +249,80 @@ export function Stats() {
|
|||||||
suffix: "%",
|
suffix: "%",
|
||||||
label: "Blockchain Verified",
|
label: "Blockchain Verified",
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
gradient: "bg-gradient-to-r from-emerald-500 to-teal-600",
|
gradient: "bg-gradient-to-r from-emerald-400 to-teal-600",
|
||||||
cardColor:
|
|
||||||
"bg-gradient-to-br from-emerald-50/80 to-teal-50/80 dark:from-emerald-950/30 dark:to-teal-950/30",
|
|
||||||
glowColor: "bg-emerald-500",
|
glowColor: "bg-emerald-500",
|
||||||
spotlight: {
|
spotlight: {
|
||||||
gradientFirst:
|
gradientFirst:
|
||||||
"radial-gradient(68.54% 68.72% at 55.02% 31.46%, hsla(158, 64%, 52%, .28) 0, hsla(158, 64%, 52%, .12) 55%, hsla(158, 64%, 52%, 0) 80%)",
|
"radial-gradient(68% 68% at 55% 31%, hsla(150, 100%, 50%, 0.3) 0, transparent 80%)",
|
||||||
},
|
},
|
||||||
additional: "All documents certified",
|
additional: "All docs certified",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "GDPR",
|
value: "GDPR",
|
||||||
label: "Full European Compliance",
|
label: "European Compliance",
|
||||||
icon: Lock,
|
icon: Lock,
|
||||||
gradient: "bg-gradient-to-r from-violet-500 to-purple-600",
|
gradient: "bg-gradient-to-r from-fuchsia-500 to-purple-600",
|
||||||
cardColor:
|
glowColor: "bg-fuchsia-500",
|
||||||
"bg-gradient-to-br from-violet-50/80 to-purple-50/80 dark:from-violet-950/30 dark:to-purple-950/30",
|
|
||||||
glowColor: "bg-violet-500",
|
|
||||||
className: "md:col-span-2",
|
className: "md:col-span-2",
|
||||||
|
isText: true,
|
||||||
spotlight: {
|
spotlight: {
|
||||||
gradientFirst:
|
gradientFirst:
|
||||||
"radial-gradient(68.54% 68.72% at 55.02% 31.46%, hsla(258, 90%, 66%, .28) 0, hsla(258, 90%, 66%, .12) 55%, hsla(258, 90%, 66%, 0) 80%)",
|
"radial-gradient(68% 68% at 55% 31%, hsla(280, 100%, 60%, 0.4) 0, transparent 80%)",
|
||||||
},
|
},
|
||||||
additional: "ISO 27001 certified",
|
additional: "ISO 27001 Certified",
|
||||||
isText: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="stats"
|
id="stats"
|
||||||
className="relative py-16 px-4 sm:px-6 lg:px-8 overflow-hidden"
|
className="relative py-32 px-4 sm:px-6 lg:px-8 overflow-hidden bg-background"
|
||||||
>
|
>
|
||||||
{/* Gradient Background */}
|
{/* Massive Aurora Background Effect
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-background via-muted/40 to-background">
|
This replaces the simple gradient with deep, colorful glowing orbs
|
||||||
{/* Grid Pattern Overlay */}
|
*/}
|
||||||
<div className="absolute inset-0 grid-pattern opacity-15" />
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[500px] opacity-30 dark:opacity-40 pointer-events-none blur-[120px] rounded-full mix-blend-screen bg-gradient-to-b from-primary/60 via-indigo-500/20 to-transparent" />
|
||||||
|
<div className="absolute bottom-0 right-0 w-[600px] h-[600px] opacity-20 pointer-events-none blur-[150px] rounded-full mix-blend-screen bg-purple-600/40" />
|
||||||
|
|
||||||
{/* Radial Glow */}
|
{/* Grid Pattern */}
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,hsla(var(--primary),0.2)_0%,transparent_55%)]" />
|
<div className="absolute inset-0 bg-[url('https://res.cloudinary.com/aceternity/image/upload/v1705626490/grid-pattern_q5ymq0.png')] opacity-[0.05] dark:opacity-[0.1]" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative max-w-7xl mx-auto">
|
<div className="relative max-w-7xl mx-auto z-10">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div
|
<div
|
||||||
ref={headerRef}
|
ref={headerRef}
|
||||||
className="text-center mb-14"
|
className="flex flex-col items-center text-center mb-24"
|
||||||
style={{
|
style={{
|
||||||
opacity: headerVisible ? 1 : 0,
|
opacity: headerVisible ? 1 : 0,
|
||||||
transform: headerVisible ? "translateY(0)" : "translateY(30px)",
|
transform: headerVisible ? "translateY(0)" : "translateY(30px)",
|
||||||
transition: "all 0.6s ease-out",
|
transition: "all 1s cubic-bezier(0.22, 1, 0.36, 1)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/60 px-4 py-1 text-xs uppercase tracking-[0.3em] text-muted-foreground">
|
{/* Glowing Pill */}
|
||||||
Platform Metrics
|
<div className="relative inline-flex mb-8">
|
||||||
</span>
|
<div className="absolute inset-0 rounded-full blur-md bg-primary/30 animate-pulse" />
|
||||||
|
<span className="relative inline-flex items-center gap-2 rounded-full border border-primary/50 bg-background/80 backdrop-blur-xl px-5 py-2 text-xs font-black uppercase tracking-[0.3em] text-primary shadow-[0_0_20px_rgba(var(--primary),0.2)]">
|
||||||
|
Platform Metrics
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 className="mt-6 text-4xl md:text-5xl lg:text-6xl font-semibold text-foreground">
|
<h2 className="text-5xl md:text-6xl lg:text-7xl font-bold text-foreground tracking-tighter leading-[1.1]">
|
||||||
Corporate-Grade Results, Measured.
|
Corporate-Grade Results, <br className="hidden md:block" />
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-purple-500 to-blue-500">
|
||||||
|
Quantified.
|
||||||
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mt-4 text-base md:text-lg text-muted-foreground max-w-2xl mx-auto">
|
<p className="mt-8 text-xl text-foreground/60 max-w-2xl mx-auto font-medium">
|
||||||
Transparent performance benchmarks that prove reliability, accuracy,
|
Transparent performance benchmarks proving reliability, accuracy,
|
||||||
and compliance at enterprise scale.
|
and compliance at enterprise scale.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<BentoGrid className="max-w-6xl gap-6 md:auto-rows-[16rem]">
|
<BentoGrid className="max-w-6xl mx-auto gap-8 md:auto-rows-[22rem]">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
<StatCard
|
<StatCard key={stat.label} {...stat} delay={index * 0.15} />
|
||||||
key={stat.label}
|
|
||||||
value={stat.value}
|
|
||||||
numericValue={stat.numericValue}
|
|
||||||
suffix={stat.suffix || ""}
|
|
||||||
prefix={stat.prefix || ""}
|
|
||||||
label={stat.label}
|
|
||||||
icon={stat.icon}
|
|
||||||
gradient={stat.gradient}
|
|
||||||
className={stat.className}
|
|
||||||
cardColor={stat.cardColor}
|
|
||||||
glowColor={stat.glowColor}
|
|
||||||
spotlight={stat.spotlight}
|
|
||||||
delay={index * 0.1}
|
|
||||||
additional={stat.additional}
|
|
||||||
isText={stat.isText}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</BentoGrid>
|
</BentoGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,13 +19,30 @@ import { keyManager } from "@/lib/services/ai/key-manager";
|
|||||||
|
|
||||||
const PRIMARY_ANALYSIS_MODEL =
|
const PRIMARY_ANALYSIS_MODEL =
|
||||||
process.env.AI_MODEL_PRIMARY || "gemini-3.1-flash-lite-preview";
|
process.env.AI_MODEL_PRIMARY || "gemini-3.1-flash-lite-preview";
|
||||||
|
const GEMINI_SECONDARY_ANALYSIS_MODEL =
|
||||||
|
process.env.AI_MODEL_SECONDARY_GEMINI || "";
|
||||||
const FALLBACK_ANALYSIS_MODEL =
|
const FALLBACK_ANALYSIS_MODEL =
|
||||||
process.env.AI_MODEL_FALLBACK || "gemini-2.0-flash";
|
process.env.AI_MODEL_FALLBACK || "llama-3.3-70b-versatile";
|
||||||
|
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";
|
||||||
|
|
||||||
|
const GEMINI_ANALYSIS_MODELS = Array.from(
|
||||||
|
new Set(
|
||||||
|
[PRIMARY_ANALYSIS_MODEL, GEMINI_SECONDARY_ANALYSIS_MODEL].filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const ANALYSIS_MODELS = Array.from(
|
const ANALYSIS_MODELS = Array.from(
|
||||||
new Set([PRIMARY_ANALYSIS_MODEL, FALLBACK_ANALYSIS_MODEL]),
|
new Set([...GEMINI_ANALYSIS_MODELS, `groq:${FALLBACK_ANALYSIS_MODEL}`]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FORCE_FALLBACK_TEST =
|
||||||
|
process.env.AI_FORCE_FALLBACK_TEST === "1" ||
|
||||||
|
String(process.env.AI_FORCE_FALLBACK_TEST).toLowerCase() === "true";
|
||||||
|
|
||||||
type ValidationEnvelope = {
|
type ValidationEnvelope = {
|
||||||
contractValidation?: {
|
contractValidation?: {
|
||||||
isValidContract?: boolean;
|
isValidContract?: boolean;
|
||||||
@@ -72,6 +89,21 @@ const isAdaptiveKeyPoints = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class AIService {
|
export class AIService {
|
||||||
|
private static isTransientGeminiError(message: string): boolean {
|
||||||
|
const normalized = message.toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized.includes("503") ||
|
||||||
|
normalized.includes("service unavailable") ||
|
||||||
|
normalized.includes("high demand") ||
|
||||||
|
normalized.includes("temporarily unavailable") ||
|
||||||
|
normalized.includes("backend error") ||
|
||||||
|
normalized.includes("internal server error") ||
|
||||||
|
normalized.includes("bad gateway") ||
|
||||||
|
normalized.includes("gateway timeout") ||
|
||||||
|
normalized.includes("deadline exceeded")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain-specific guidance for contract Q&A.
|
* Domain-specific guidance for contract Q&A.
|
||||||
* This keeps responses focused on what matters most for each contract family.
|
* This keeps responses focused on what matters most for each contract family.
|
||||||
@@ -116,6 +148,8 @@ export class AIService {
|
|||||||
keyManager.resetKeys();
|
keyManager.resetKeys();
|
||||||
try {
|
try {
|
||||||
const maxRetries = Math.min(3, Math.max(1, options?.maxRetries ?? 2));
|
const maxRetries = Math.min(3, Math.max(1, options?.maxRetries ?? 2));
|
||||||
|
const forceFallbackModelTest =
|
||||||
|
options?.forceFallbackModelTest ?? FORCE_FALLBACK_TEST;
|
||||||
|
|
||||||
// Step 1: Download raw file bytes from storage URL.
|
// Step 1: Download raw file bytes from storage URL.
|
||||||
const response = await fetch(fileUrl);
|
const response = await fetch(fileUrl);
|
||||||
@@ -168,6 +202,7 @@ export class AIService {
|
|||||||
prompt: `${basePrompt}${correctionHint}`,
|
prompt: `${basePrompt}${correctionHint}`,
|
||||||
base64,
|
base64,
|
||||||
mimeType,
|
mimeType,
|
||||||
|
forceFallbackModelTest,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -247,7 +282,7 @@ export class AIService {
|
|||||||
// Better error messages
|
// Better error messages
|
||||||
if (errorMessage.includes("API key")) {
|
if (errorMessage.includes("API key")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid or missing Gemini API key. Check AI_API_KEY in your .env file",
|
"Invalid or missing AI API key. Check AI_API_KEY1/2/3 for Gemini and GROQ_API_KEY for Groq fallback.",
|
||||||
);
|
);
|
||||||
} else if (errorMessage.includes("INVALID_CONTRACT:")) {
|
} else if (errorMessage.includes("INVALID_CONTRACT:")) {
|
||||||
const reason = String(errorMessage)
|
const reason = String(errorMessage)
|
||||||
@@ -256,6 +291,10 @@ export class AIService {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
reason || "Uploaded file is not recognized as a valid contract.",
|
reason || "Uploaded file is not recognized as a valid contract.",
|
||||||
);
|
);
|
||||||
|
} else if (this.isTransientGeminiError(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.`,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
errorMessage.includes("not found") ||
|
errorMessage.includes("not found") ||
|
||||||
errorMessage.includes("404")
|
errorMessage.includes("404")
|
||||||
@@ -298,7 +337,7 @@ export class AIService {
|
|||||||
}
|
}
|
||||||
} else if (errorMessage.includes("quota")) {
|
} else if (errorMessage.includes("quota")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Limit exceeded. Your Gemini API quota may be exhausted. Check your Google Cloud Console for usage details.",
|
"Limit exceeded. Gemini or Groq quota may be exhausted. Check your provider dashboards for usage and limits.",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Error analyzing contract: ${errorMessage}`);
|
throw new Error(`Error analyzing contract: ${errorMessage}`);
|
||||||
@@ -350,14 +389,196 @@ export class AIService {
|
|||||||
return parseAiJsonResponse(text);
|
return parseAiJsonResponse(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static isGroqConfigured(): boolean {
|
||||||
|
return GROQ_API_KEY.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async generateWithGroq(input: {
|
||||||
|
model?: string;
|
||||||
|
prompt: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
responseAsJson: boolean;
|
||||||
|
maxOutputTokens: number;
|
||||||
|
temperature?: number;
|
||||||
|
topP?: number;
|
||||||
|
}): Promise<string> {
|
||||||
|
if (!this.isGroqConfigured()) {
|
||||||
|
throw new Error(
|
||||||
|
"Groq fallback is not configured. Set GROQ_API_KEY (or AI_GROQ_API_KEY).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelName = input.model || FALLBACK_ANALYSIS_MODEL;
|
||||||
|
|
||||||
|
// Build messages with system/user role separation for better instruction adherence
|
||||||
|
const messages: Array<{ role: string; content: string }> = [];
|
||||||
|
if (input.systemPrompt) {
|
||||||
|
messages.push({ role: "system", content: input.systemPrompt });
|
||||||
|
}
|
||||||
|
messages.push({ role: "user", content: input.prompt });
|
||||||
|
|
||||||
|
// Use json_object mode (compatible with all models)
|
||||||
|
const responseFormat: Record<string, unknown> | undefined = input.responseAsJson
|
||||||
|
? { type: "json_object" as const }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
model: modelName,
|
||||||
|
temperature: input.temperature ?? 0,
|
||||||
|
top_p: input.topP ?? 0.95,
|
||||||
|
max_tokens: input.maxOutputTokens,
|
||||||
|
response_format: responseFormat,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(GROQ_API_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${GROQ_API_KEY}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const details = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`Groq 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 Groq fallback model.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async generateWithGroqModelChain(input: {
|
||||||
|
preferredModel?: string;
|
||||||
|
prompt: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
responseAsJson: boolean;
|
||||||
|
maxOutputTokens: number;
|
||||||
|
temperature?: number;
|
||||||
|
topP?: number;
|
||||||
|
}): Promise<string> {
|
||||||
|
const candidates = Array.from(
|
||||||
|
new Set(
|
||||||
|
[
|
||||||
|
input.preferredModel,
|
||||||
|
FALLBACK_ANALYSIS_MODEL,
|
||||||
|
"llama-3.3-70b-versatile",
|
||||||
|
"qwen-2.5-32b",
|
||||||
|
"llama-3.1-8b-instant",
|
||||||
|
].filter(Boolean),
|
||||||
|
),
|
||||||
|
) as string[];
|
||||||
|
|
||||||
|
let lastError: unknown = null;
|
||||||
|
|
||||||
|
for (const modelName of candidates) {
|
||||||
|
try {
|
||||||
|
const text = await this.generateWithGroq({
|
||||||
|
model: modelName,
|
||||||
|
prompt: input.prompt,
|
||||||
|
systemPrompt: input.systemPrompt,
|
||||||
|
responseAsJson: input.responseAsJson,
|
||||||
|
maxOutputTokens: input.maxOutputTokens,
|
||||||
|
temperature: input.temperature,
|
||||||
|
topP: input.topP,
|
||||||
|
});
|
||||||
|
if (modelName !== (input.preferredModel || FALLBACK_ANALYSIS_MODEL)) {
|
||||||
|
console.warn(
|
||||||
|
`Groq 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.`,
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError instanceof Error
|
||||||
|
? lastError
|
||||||
|
: new Error("All Groq fallback models failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a Groq-optimized system prompt that mirrors the Gemini behavior.
|
||||||
|
* This separates role & formatting rules from user content for better
|
||||||
|
* instruction adherence on open-source models.
|
||||||
|
*/
|
||||||
|
private static buildGroqSystemPrompt(): 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.
|
||||||
|
|
||||||
|
CRITICAL OUTPUT RULES:
|
||||||
|
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).
|
||||||
|
|
||||||
|
You are replacing a more capable multimodal model (Gemini) as a fallback. Your output quality MUST match production standards.`;
|
||||||
|
}
|
||||||
|
|
||||||
private static async generateAnalysisWithFallback(input: {
|
private static async generateAnalysisWithFallback(input: {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
base64: string;
|
base64: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
|
forceFallbackModelTest?: boolean;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
const forceFallback = Boolean(input.forceFallbackModelTest);
|
||||||
|
|
||||||
for (const modelName of ANALYSIS_MODELS) {
|
const buildGroundedGroqPrompt = async (basePrompt: string) => {
|
||||||
|
const groundingText = await this.extractGroqGroundingText({
|
||||||
|
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.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (forceFallback) {
|
||||||
|
console.warn(
|
||||||
|
`🧪 Fallback test mode enabled. Skipping Gemini and forcing Groq model ${FALLBACK_ANALYSIS_MODEL}.`,
|
||||||
|
);
|
||||||
|
const groundedPrompt = await buildGroundedGroqPrompt(input.prompt);
|
||||||
|
return this.generateWithGroqModelChain({
|
||||||
|
preferredModel: FALLBACK_ANALYSIS_MODEL,
|
||||||
|
systemPrompt: this.buildGroqSystemPrompt(),
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const modelName of GEMINI_ANALYSIS_MODELS) {
|
||||||
try {
|
try {
|
||||||
return await keyManager.execute(async (genAI) => {
|
return await keyManager.execute(async (genAI) => {
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
@@ -437,9 +658,32 @@ export class AIService {
|
|||||||
console.warn("Lenient generation also failed:", error);
|
console.warn("Lenient generation also failed:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Groq fallback path ===
|
||||||
|
console.warn(
|
||||||
|
"All Gemini models exhausted. Activating Groq fallback pipeline...",
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const groundedPrompt = await buildGroundedGroqPrompt(input.prompt);
|
||||||
|
const groqText = await this.generateWithGroqModelChain({
|
||||||
|
preferredModel: FALLBACK_ANALYSIS_MODEL,
|
||||||
|
systemPrompt: this.buildGroqSystemPrompt(),
|
||||||
|
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`,
|
||||||
|
);
|
||||||
|
return groqText;
|
||||||
|
} catch (groqError) {
|
||||||
|
console.warn("Groq analysis fallback failed:", groqError);
|
||||||
|
}
|
||||||
|
|
||||||
throw lastError instanceof Error
|
throw lastError instanceof Error
|
||||||
? lastError
|
? lastError
|
||||||
: new Error("All analysis models failed to generate content.");
|
: new Error(
|
||||||
|
"All analysis models (Gemini + Groq fallback) failed to generate content.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async repairMalformedJson(
|
private static async repairMalformedJson(
|
||||||
@@ -447,47 +691,34 @@ export class AIService {
|
|||||||
parseError: string,
|
parseError: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
return await keyManager.execute(async (genAI) => {
|
const expectedSchema = {
|
||||||
const repairModelName = FALLBACK_ANALYSIS_MODEL;
|
language: "string|null",
|
||||||
const model = genAI.getGenerativeModel({
|
title: "string",
|
||||||
model: repairModelName,
|
type: "enum: INSURANCE_AUTO|INSURANCE_HOME|INSURANCE_HEALTH|INSURANCE_LIFE|LOAN|CREDIT_CARD|INVESTMENT|OTHER",
|
||||||
generationConfig: {
|
provider: "string|null",
|
||||||
temperature: 0,
|
policyNumber: "string|null",
|
||||||
topP: 0.9,
|
startDate: "YYYY-MM-DD|null",
|
||||||
topK: 20,
|
endDate: "YYYY-MM-DD|null",
|
||||||
maxOutputTokens: 16384,
|
premium: "number|null",
|
||||||
responseMimeType: "application/json",
|
premiumCurrency: "string|null (ISO code like EUR/USD/TND or symbol)",
|
||||||
},
|
summary: "string (min 10 chars)",
|
||||||
});
|
extractedText: "string (min 30 chars)",
|
||||||
|
keyPoints: {
|
||||||
const expectedSchema = {
|
guarantees: "string[]",
|
||||||
language: "string|null",
|
exclusions: "string[]",
|
||||||
title: "string",
|
franchise: "string|null",
|
||||||
type: "enum: INSURANCE_AUTO|INSURANCE_HOME|INSURANCE_HEALTH|INSURANCE_LIFE|LOAN|CREDIT_CARD|INVESTMENT|OTHER",
|
importantDates: "string[]",
|
||||||
provider: "string|null",
|
explainability:
|
||||||
policyNumber: "string|null",
|
"[{ field, why, sourceSnippet, sourceHints:{ page|null, section|null, confidence|null } }]",
|
||||||
startDate: "YYYY-MM-DD|null",
|
},
|
||||||
endDate: "YYYY-MM-DD|null",
|
keyPeople: "[{ name, role|null, email|null, phone|null }]",
|
||||||
premium: "number|null",
|
contactInfo:
|
||||||
premiumCurrency: "string|null (ISO code like EUR/USD/TND or symbol)",
|
"{ name|null, email|null, phone|null, address|null, role|null }",
|
||||||
summary: "string (min 10 chars)",
|
importantContacts:
|
||||||
extractedText: "string (min 30 chars)",
|
"[{ name|null, email|null, phone|null, address|null, role|null }]",
|
||||||
keyPoints: {
|
relevantDates:
|
||||||
guarantees: "string[]",
|
"[{ date:'YYYY-MM-DD', description, type:'EXPIRATION|RENEWAL|PAYMENT|REVIEW|OTHER' }]",
|
||||||
exclusions: "string[]",
|
contractValidation: {
|
||||||
franchise: "string|null",
|
|
||||||
importantDates: "string[]",
|
|
||||||
explainability:
|
|
||||||
"[{ field, why, sourceSnippet, sourceHints:{ page|null, section|null, confidence|null } }]",
|
|
||||||
},
|
|
||||||
keyPeople: "[{ name, role|null, email|null, phone|null }]",
|
|
||||||
contactInfo:
|
|
||||||
"{ name|null, email|null, phone|null, address|null, role|null }",
|
|
||||||
importantContacts:
|
|
||||||
"[{ name|null, email|null, phone|null, address|null, role|null }]",
|
|
||||||
relevantDates:
|
|
||||||
"[{ date:'YYYY-MM-DD', description, type:'EXPIRATION|RENEWAL|PAYMENT|REVIEW|OTHER' }]",
|
|
||||||
contractValidation: {
|
|
||||||
isValidContract: "boolean",
|
isValidContract: "boolean",
|
||||||
confidence: "number (0-100)",
|
confidence: "number (0-100)",
|
||||||
reason: "string|null",
|
reason: "string|null",
|
||||||
@@ -515,20 +746,38 @@ Original parse error: ${parseError}
|
|||||||
Malformed response to fix:
|
Malformed response to fix:
|
||||||
${malformedResponse.slice(0, 14000)}`;
|
${malformedResponse.slice(0, 14000)}`;
|
||||||
|
|
||||||
const repaired = await model.generateContent(repairPrompt);
|
const repairedText = await this.generateWithGroqModelChain({
|
||||||
const repairedText = repaired.response.text()?.trim() || "";
|
preferredModel: FALLBACK_REPAIR_MODEL,
|
||||||
|
prompt: repairPrompt,
|
||||||
|
responseAsJson: true,
|
||||||
|
maxOutputTokens: 6144,
|
||||||
|
});
|
||||||
|
|
||||||
if (repairedText.length === 0) {
|
if (repairedText.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the repaired text is at least JSON-like before returning
|
|
||||||
if (!repairedText.includes("{")) {
|
if (!repairedText.includes("{")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.parseJsonResponse(repairedText);
|
||||||
|
} 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({
|
||||||
|
preferredModel: FALLBACK_REPAIR_MODEL,
|
||||||
|
prompt: secondPassPrompt,
|
||||||
|
responseAsJson: true,
|
||||||
|
maxOutputTokens: 6144,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.parseJsonResponse(secondPass);
|
||||||
|
return secondPass;
|
||||||
|
}
|
||||||
|
|
||||||
return repairedText;
|
return repairedText;
|
||||||
});
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
|
if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
|
||||||
console.warn("JSON repair step failed:", error);
|
console.warn("JSON repair step failed:", error);
|
||||||
@@ -536,6 +785,88 @@ ${malformedResponse.slice(0, 14000)}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async extractGroqGroundingText(input: {
|
||||||
|
base64: string;
|
||||||
|
mimeType: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
// For PDFs: extract text directly using pdf-parse
|
||||||
|
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 };
|
||||||
|
try {
|
||||||
|
parsed = await parser.getText();
|
||||||
|
} finally {
|
||||||
|
await parser.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = (parsed?.text || "")
|
||||||
|
.replace(/\r/g, "\n")
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (text && text.length > 50) {
|
||||||
|
console.log(
|
||||||
|
`📄 Groq grounding: extracted ${text.length} chars from PDF`,
|
||||||
|
);
|
||||||
|
return text.slice(0, 50000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"PDF grounding extraction failed for Groq fallback.",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emergency fallback: Extract key contract fields from raw text when JSON is completely malformed.
|
* 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.
|
* Builds a minimal but valid JSON structure from pattern-matched fields.
|
||||||
@@ -641,7 +972,7 @@ ${malformedResponse.slice(0, 14000)}`;
|
|||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
|
||||||
for (const modelName of ANALYSIS_MODELS) {
|
for (const modelName of GEMINI_ANALYSIS_MODELS) {
|
||||||
try {
|
try {
|
||||||
return await keyManager.execute(async (genAI) => {
|
return await keyManager.execute(async (genAI) => {
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
@@ -1036,7 +1367,7 @@ Include one short disclaimer only when legal context is discussed: "This is gene
|
|||||||
let rawAnswer = "";
|
let rawAnswer = "";
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
|
||||||
for (const modelName of ANALYSIS_MODELS) {
|
for (const modelName of GEMINI_ANALYSIS_MODELS) {
|
||||||
try {
|
try {
|
||||||
rawAnswer = await keyManager.execute(async (genAI) => {
|
rawAnswer = await keyManager.execute(async (genAI) => {
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({
|
||||||
@@ -1073,6 +1404,25 @@ Include one short disclaimer only when legal context is discussed: "This is gene
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!rawAnswer) {
|
||||||
|
try {
|
||||||
|
rawAnswer = await this.generateWithGroqModelChain({
|
||||||
|
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,
|
||||||
|
responseAsJson: false,
|
||||||
|
maxOutputTokens: 2048,
|
||||||
|
temperature: 0.2,
|
||||||
|
topP: 0.95,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`✅ Q&A fallback with Groq model ${FALLBACK_ANALYSIS_MODEL} succeeded in ${languageName}`,
|
||||||
|
);
|
||||||
|
} catch (groqError) {
|
||||||
|
lastError = groqError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!rawAnswer) {
|
if (!rawAnswer) {
|
||||||
if (lastError instanceof Error) {
|
if (lastError instanceof Error) {
|
||||||
throw lastError;
|
throw lastError;
|
||||||
@@ -1094,7 +1444,12 @@ Include one short disclaimer only when legal context is discussed: "This is gene
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
if (errorMessage.includes("API key")) {
|
if (errorMessage.includes("API key")) {
|
||||||
throw new Error("Invalid or missing Gemini API key.");
|
throw new Error("Invalid or missing AI API key (Gemini/Groq).");
|
||||||
|
}
|
||||||
|
if (this.isTransientGeminiError(errorMessage)) {
|
||||||
|
throw new Error(
|
||||||
|
`Gemini is 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}`);
|
throw new Error(`Error answering question: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type AnalyzeOptions = {
|
|||||||
userId?: string;
|
userId?: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
|
forceFallbackModelTest?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContactInfo = {
|
export type ContactInfo = {
|
||||||
|
|||||||
@@ -15,20 +15,14 @@ type RetrievedChunk = {
|
|||||||
score: number;
|
score: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const API_KEY =
|
import { keyManager } from "@/lib/services/ai/key-manager";
|
||||||
process.env.AI_API_KEY1 || process.env.AI_API_KEY2 || process.env.AI_API_KEY3;
|
|
||||||
|
|
||||||
if (!API_KEY) {
|
|
||||||
throw new Error("AI_API_KEY is not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMBEDDING_MODEL = process.env.AI_EMBEDDING_MODEL || "text-embedding-004";
|
const EMBEDDING_MODEL = process.env.AI_EMBEDDING_MODEL || "text-embedding-004";
|
||||||
const EMBEDDING_MODEL_FALLBACKS = [
|
const EMBEDDING_MODEL_FALLBACKS = [
|
||||||
EMBEDDING_MODEL,
|
EMBEDDING_MODEL,
|
||||||
|
"gemini-embedding-001",
|
||||||
"text-embedding-004",
|
"text-embedding-004",
|
||||||
"embedding-001",
|
|
||||||
];
|
];
|
||||||
const genAI = new GoogleGenerativeAI(API_KEY);
|
|
||||||
|
|
||||||
export class RAGService {
|
export class RAGService {
|
||||||
private static readonly MAX_CHUNK_CHARS = 1400;
|
private static readonly MAX_CHUNK_CHARS = 1400;
|
||||||
@@ -236,14 +230,18 @@ export class RAGService {
|
|||||||
|
|
||||||
for (const modelName of Array.from(new Set(EMBEDDING_MODEL_FALLBACKS))) {
|
for (const modelName of Array.from(new Set(EMBEDDING_MODEL_FALLBACKS))) {
|
||||||
try {
|
try {
|
||||||
const model = genAI.getGenerativeModel({ model: modelName });
|
return await keyManager.execute(async (genAI) => {
|
||||||
const result = await model.embedContent(text);
|
const model = genAI.getGenerativeModel({ model: modelName });
|
||||||
const values = result.embedding?.values;
|
const result = await model.embedContent(text);
|
||||||
|
const values = result.embedding?.values;
|
||||||
|
|
||||||
if (values && Array.isArray(values) && values.length > 0) {
|
if (values && Array.isArray(values) && values.length > 0) {
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
throw new Error("Empty embedding");
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
|
||||||
lastError = error;
|
lastError = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
232
package-lock.json
generated
232
package-lock.json
generated
@@ -54,6 +54,7 @@
|
|||||||
"motion": "^12.34.0",
|
"motion": "^12.34.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"pdf-parse": "^2.4.5",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
@@ -1331,6 +1332,205 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/canvas": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"e2e/*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-android-arm64": "0.1.80",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "0.1.80",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "0.1.80",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
|
"version": "0.1.80",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
|
||||||
|
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@@ -7930,6 +8130,38 @@
|
|||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdf-parse": {
|
||||||
|
"version": "2.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz",
|
||||||
|
"integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@napi-rs/canvas": "0.1.80",
|
||||||
|
"pdfjs-dist": "5.4.296"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pdf-parse": "bin/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.16.0 <21 || >=22.3.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pdfjs-dist": {
|
||||||
|
"version": "5.4.296",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
|
||||||
|
"integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.16.0 || >=22.3.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas": "^0.1.80"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"motion": "^12.34.0",
|
"motion": "^12.34.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"pdf-parse": "^2.4.5",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user