midah commited on
Commit
762ea2d
·
0 Parent(s):

Initial commit: AI Training Data Deals Dashboard

Browse files

- Complete Next.js application with minimalist humanist design
- Database schema with Prisma (SQLite) seeded with 24 baseline deals
- Deals explorer as main view with search, filters, and sorting
- Modal view for deal details with full information
- Pricing normalizations displayed in table and modal
- Analytics dashboard with market structure visualizations
- Pricing normalization tool for per-unit comparisons
- Python ingestion pipeline structure (Exa/Perplexity integration)
- Public Sans font throughout
- All columns sortable (provider, buyer, modality, price, date, etc.)
- Responsive design with clean, accessible UI

.gitignore ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ .pnp
4
+ .pnp.js
5
+
6
+ # Testing
7
+ coverage/
8
+
9
+ # Next.js
10
+ .next/
11
+ out/
12
+ build/
13
+ dist/
14
+
15
+ # Production
16
+ *.log
17
+
18
+ # Misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # Debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # Local env files
28
+ .env
29
+ .env*.local
30
+
31
+ # Vercel
32
+ .vercel
33
+
34
+ # Prisma
35
+ prisma/dev.db
36
+ prisma/dev.db-journal
37
+ *.db
38
+ *.db-journal
39
+
40
+ # IDE
41
+ .idea/
42
+ .vscode/
43
+ *.swp
44
+ *.swo
45
+
46
+ # Python
47
+ __pycache__/
48
+ *.py[cod]
49
+ *$py.class
50
+ *.so
51
+ .Python
52
+ venv/
53
+ env/
54
+ ENV/
55
+
app/analytics/page.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { prisma } from '@/lib/prisma'
2
+ import Link from 'next/link'
3
+
4
+ async function getAnalytics() {
5
+ const deals = await prisma.deal.findMany({
6
+ include: {
7
+ buyerRelations: {
8
+ include: {
9
+ buyer: true,
10
+ },
11
+ },
12
+ providerRelation: true,
13
+ },
14
+ })
15
+
16
+ // Calculate stats
17
+ const totalDeals = deals.length
18
+ const totalSpend = deals.reduce((sum, deal) => sum + (deal.priceUsd || 0), 0)
19
+ const exclusiveDeals = deals.filter(d => d.exclusive === true).length
20
+ const compensatedDeals = deals.filter(d => d.creatorsCompensated === true).length
21
+ const compensatedPercent = totalDeals > 0 ? Math.round((compensatedDeals / totalDeals) * 100) : 0
22
+
23
+ // Modality breakdown
24
+ const modalityCounts: Record<string, number> = {}
25
+ deals.forEach(deal => {
26
+ modalityCounts[deal.modality] = (modalityCounts[deal.modality] || 0) + 1
27
+ })
28
+
29
+ // Buyer breakdown
30
+ const buyerCounts: Record<string, number> = {}
31
+ const buyerSpend: Record<string, number> = {}
32
+ deals.forEach(deal => {
33
+ const buyers = deal.buyer.split(',').map(b => b.trim())
34
+ buyers.forEach(buyer => {
35
+ buyerCounts[buyer] = (buyerCounts[buyer] || 0) + 1
36
+ buyerSpend[buyer] = (buyerSpend[buyer] || 0) + (deal.priceUsd || 0)
37
+ })
38
+ })
39
+
40
+ // Provider breakdown
41
+ const providerCounts: Record<string, number> = {}
42
+ const providerSpend: Record<string, number> = {}
43
+ deals.forEach(deal => {
44
+ providerCounts[deal.provider] = (providerCounts[deal.provider] || 0) + 1
45
+ providerSpend[deal.provider] = (providerSpend[deal.provider] || 0) + (deal.priceUsd || 0)
46
+ })
47
+
48
+ // Top buyers by spend
49
+ const topBuyers = Object.entries(buyerSpend)
50
+ .sort(([, a], [, b]) => b - a)
51
+ .slice(0, 10)
52
+ .map(([name, spend]) => ({ name, spend, count: buyerCounts[name] }))
53
+
54
+ // Top providers by spend
55
+ const topProviders = Object.entries(providerSpend)
56
+ .sort(([, a], [, b]) => b - a)
57
+ .slice(0, 10)
58
+ .map(([name, spend]) => ({ name, spend, count: providerCounts[name] }))
59
+
60
+ return {
61
+ totalDeals,
62
+ totalSpend,
63
+ exclusiveDeals,
64
+ compensatedDeals,
65
+ compensatedPercent,
66
+ modalityCounts,
67
+ topBuyers,
68
+ topProviders,
69
+ }
70
+ }
71
+
72
+ function formatCurrency(amount: number): string {
73
+ if (amount >= 1000000000) {
74
+ return `$${(amount / 1000000000).toFixed(1)}B`
75
+ }
76
+ if (amount >= 1000000) {
77
+ return `$${(amount / 1000000).toFixed(0)}M`
78
+ }
79
+ if (amount >= 1000) {
80
+ return `$${(amount / 1000).toFixed(0)}K`
81
+ }
82
+ return `$${amount.toFixed(0)}`
83
+ }
84
+
85
+ export default async function AnalyticsPage() {
86
+ const analytics = await getAnalytics()
87
+
88
+ return (
89
+ <main className="min-h-screen bg-background">
90
+ <div className="container-content section-padding">
91
+ <div className="mb-8">
92
+ <h1 className="text-4xl font-semibold mb-4">Market Analytics</h1>
93
+ <p className="text-text-muted text-lg">
94
+ Market structure, concentration, and trends
95
+ </p>
96
+ </div>
97
+
98
+ {/* Key Stats */}
99
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-12">
100
+ <div className="stat-card">
101
+ <div className="stat-value">{analytics.totalDeals}</div>
102
+ <div className="stat-label">Total Deals</div>
103
+ </div>
104
+ <div className="stat-card">
105
+ <div className="stat-value">{formatCurrency(analytics.totalSpend)}</div>
106
+ <div className="stat-label">Total Reported Spend</div>
107
+ </div>
108
+ <div className="stat-card">
109
+ <div className="stat-value">{analytics.exclusiveDeals}</div>
110
+ <div className="stat-label">Exclusive Deals</div>
111
+ </div>
112
+ <div className="stat-card">
113
+ <div className="stat-value">{analytics.compensatedPercent}%</div>
114
+ <div className="stat-label">With Creator Compensation</div>
115
+ </div>
116
+ </div>
117
+
118
+ {/* Modality Breakdown */}
119
+ <div className="card mb-8">
120
+ <h2 className="text-2xl font-semibold mb-6">Deals by Modality</h2>
121
+ <div className="space-y-4">
122
+ {Object.entries(analytics.modalityCounts)
123
+ .sort(([, a], [, b]) => b - a)
124
+ .map(([modality, count]) => (
125
+ <div key={modality} className="flex items-center justify-between py-2 border-b border-border-subtle last:border-0">
126
+ <span className="font-medium">{modality}</span>
127
+ <div className="flex items-center gap-4">
128
+ <div className="w-32 bg-border-subtle rounded-full h-2">
129
+ <div
130
+ className="bg-accent h-2 rounded-full"
131
+ style={{ width: `${(count / analytics.totalDeals) * 100}%` }}
132
+ />
133
+ </div>
134
+ <span className="text-text-muted w-12 text-right">{count}</span>
135
+ </div>
136
+ </div>
137
+ ))}
138
+ </div>
139
+ </div>
140
+
141
+ {/* Top Buyers */}
142
+ <div className="grid md:grid-cols-2 gap-8 mb-8">
143
+ <div className="card">
144
+ <h2 className="text-2xl font-semibold mb-6">Top Buyers by Spend</h2>
145
+ <div className="space-y-4">
146
+ {analytics.topBuyers.map((buyer, idx) => (
147
+ <div key={buyer.name} className="flex items-center justify-between py-2">
148
+ <div className="flex items-center gap-3">
149
+ <span className="text-text-muted w-6">#{idx + 1}</span>
150
+ <div>
151
+ <div className="font-medium">{buyer.name}</div>
152
+ <div className="text-sm text-text-muted">{buyer.count} deal{buyer.count !== 1 ? 's' : ''}</div>
153
+ </div>
154
+ </div>
155
+ <span className="font-semibold">{formatCurrency(buyer.spend)}</span>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ </div>
160
+
161
+ {/* Top Providers */}
162
+ <div className="card">
163
+ <h2 className="text-2xl font-semibold mb-6">Top Providers by Spend</h2>
164
+ <div className="space-y-4">
165
+ {analytics.topProviders.map((provider, idx) => (
166
+ <div key={provider.name} className="flex items-center justify-between py-2">
167
+ <div className="flex items-center gap-3">
168
+ <span className="text-text-muted w-6">#{idx + 1}</span>
169
+ <div>
170
+ <div className="font-medium">{provider.name}</div>
171
+ <div className="text-sm text-text-muted">{provider.count} deal{provider.count !== 1 ? 's' : ''}</div>
172
+ </div>
173
+ </div>
174
+ <span className="font-semibold">{formatCurrency(provider.spend)}</span>
175
+ </div>
176
+ ))}
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <div className="mt-8">
182
+ <Link href="/" className="text-accent hover:text-accent-hover">
183
+ ← Back to Deals
184
+ </Link>
185
+ </div>
186
+ </div>
187
+ </main>
188
+ )
189
+ }
190
+
app/api/deals/route.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server'
2
+ import { prisma } from '@/lib/prisma'
3
+
4
+ export async function GET(request: Request) {
5
+ try {
6
+ const { searchParams } = new URL(request.url)
7
+ const modality = searchParams.get('modality')
8
+ const buyer = searchParams.get('buyer')
9
+ const provider = searchParams.get('provider')
10
+ const exclusive = searchParams.get('exclusive')
11
+ const creatorsCompensated = searchParams.get('creatorsCompensated')
12
+ const limit = parseInt(searchParams.get('limit') || '100')
13
+ const offset = parseInt(searchParams.get('offset') || '0')
14
+
15
+ const where: any = {}
16
+
17
+ if (modality) {
18
+ where.modality = modality
19
+ }
20
+ if (buyer) {
21
+ where.buyer = { contains: buyer }
22
+ }
23
+ if (provider) {
24
+ where.provider = { contains: provider }
25
+ }
26
+ if (exclusive !== null) {
27
+ where.exclusive = exclusive === 'true'
28
+ }
29
+ if (creatorsCompensated !== null) {
30
+ where.creatorsCompensated = creatorsCompensated === 'true'
31
+ }
32
+
33
+ const [deals, total] = await Promise.all([
34
+ prisma.deal.findMany({
35
+ where,
36
+ take: limit,
37
+ skip: offset,
38
+ orderBy: { date: 'desc' },
39
+ include: {
40
+ buyerRelations: {
41
+ include: {
42
+ buyer: true,
43
+ },
44
+ },
45
+ providerRelation: true,
46
+ },
47
+ }),
48
+ prisma.deal.count({ where }),
49
+ ])
50
+
51
+ return NextResponse.json({
52
+ deals,
53
+ total,
54
+ limit,
55
+ offset,
56
+ })
57
+ } catch (error) {
58
+ console.error('Error fetching deals:', error)
59
+ return NextResponse.json(
60
+ { error: 'Failed to fetch deals' },
61
+ { status: 500 }
62
+ )
63
+ }
64
+ }
65
+
app/deals/DealModal.tsx ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ interface Deal {
6
+ id: string
7
+ provider: string
8
+ buyer: string
9
+ modality: string
10
+ dataType: string | null
11
+ priceUsd: number | null
12
+ priceRangeMinUsd: number | null
13
+ priceRangeMaxUsd: number | null
14
+ reportedTerms: string | null
15
+ exclusive: boolean | null
16
+ creatorsCompensated: boolean | null
17
+ creatorSplitPercentage: number | null
18
+ revenueShare: boolean | null
19
+ date: string | null
20
+ dealType: string | null
21
+ pricingMechanism: string | null
22
+ sourcePrimary: string | null
23
+ trainingAllowed: boolean | null
24
+ finetuningAllowed: boolean | null
25
+ inferenceAllowed: boolean | null
26
+ redistributionAllowed: boolean | null
27
+ deletionRequired: boolean | null
28
+ notes: string | null
29
+ sources: string | null
30
+ pricingNormalizations?: Array<{
31
+ unitType: string
32
+ normalizedCostPerUnit: number
33
+ normalizationMethod: string
34
+ }>
35
+ }
36
+
37
+ interface DealModalProps {
38
+ deal: Deal | null
39
+ isOpen: boolean
40
+ onClose: () => void
41
+ }
42
+
43
+ function formatPrice(deal: Deal) {
44
+ if (deal.priceUsd) {
45
+ if (deal.priceUsd >= 1000000000) {
46
+ return `$${(deal.priceUsd / 1000000000).toFixed(1)}B`
47
+ }
48
+ if (deal.priceUsd >= 1000000) {
49
+ return `$${(deal.priceUsd / 1000000).toFixed(0)}M`
50
+ }
51
+ if (deal.priceUsd >= 1000) {
52
+ return `$${(deal.priceUsd / 1000).toFixed(0)}K`
53
+ }
54
+ return `$${deal.priceUsd.toFixed(0)}`
55
+ }
56
+ if (deal.priceRangeMinUsd && deal.priceRangeMaxUsd) {
57
+ return `$${deal.priceRangeMinUsd.toFixed(2)}–${deal.priceRangeMaxUsd.toFixed(2)}`
58
+ }
59
+ return deal.reportedTerms || 'Undisclosed'
60
+ }
61
+
62
+ export default function DealModal({ deal, isOpen, onClose }: DealModalProps) {
63
+ useEffect(() => {
64
+ if (isOpen) {
65
+ document.body.style.overflow = 'hidden'
66
+ } else {
67
+ document.body.style.overflow = 'unset'
68
+ }
69
+ return () => {
70
+ document.body.style.overflow = 'unset'
71
+ }
72
+ }, [isOpen])
73
+
74
+ if (!isOpen || !deal) return null
75
+
76
+ return (
77
+ <div
78
+ className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
79
+ onClick={onClose}
80
+ >
81
+ <div
82
+ className="bg-surface rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
83
+ onClick={(e) => e.stopPropagation()}
84
+ >
85
+ {/* Header */}
86
+ <div className="sticky top-0 bg-surface border-b border-border px-6 py-4 flex items-start justify-between">
87
+ <div className="flex-1">
88
+ <h2 className="text-2xl font-semibold mb-1">
89
+ {deal.provider} → {deal.buyer}
90
+ </h2>
91
+ <p className="text-text-muted">{deal.dataType || deal.modality}</p>
92
+ </div>
93
+ <button
94
+ onClick={onClose}
95
+ className="text-text-muted hover:text-text text-2xl leading-none ml-4"
96
+ aria-label="Close"
97
+ >
98
+ ×
99
+ </button>
100
+ </div>
101
+
102
+ {/* Content */}
103
+ <div className="p-6 space-y-6">
104
+ {/* Summary Card */}
105
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 pb-6 border-b border-border">
106
+ <div>
107
+ <div className="text-sm text-text-muted mb-1">Price</div>
108
+ <div className="text-xl font-semibold">{formatPrice(deal)}</div>
109
+ </div>
110
+ <div>
111
+ <div className="text-sm text-text-muted mb-1">Deal Type</div>
112
+ <div className="font-medium">{deal.dealType || deal.pricingMechanism || '—'}</div>
113
+ </div>
114
+ <div>
115
+ <div className="text-sm text-text-muted mb-1">Date</div>
116
+ <div className="font-medium">{deal.date || '—'}</div>
117
+ </div>
118
+ <div>
119
+ <div className="text-sm text-text-muted mb-1">Source</div>
120
+ <div className="font-medium">{deal.sourcePrimary || '—'}</div>
121
+ </div>
122
+ </div>
123
+
124
+ {/* Reported Terms */}
125
+ {deal.reportedTerms && (
126
+ <div>
127
+ <h3 className="text-lg font-semibold mb-2">Reported Terms</h3>
128
+ <p className="text-text-muted leading-relaxed">{deal.reportedTerms}</p>
129
+ </div>
130
+ )}
131
+
132
+ {/* Rights & Compensation */}
133
+ <div className="grid md:grid-cols-2 gap-6">
134
+ <div>
135
+ <h3 className="text-lg font-semibold mb-4">Rights Granted</h3>
136
+ <div className="space-y-2">
137
+ <div className="flex justify-between">
138
+ <span className="text-text-muted">Training</span>
139
+ <span>{deal.trainingAllowed !== null ? (deal.trainingAllowed ? 'Yes' : 'No') : '—'}</span>
140
+ </div>
141
+ <div className="flex justify-between">
142
+ <span className="text-text-muted">Fine-tuning</span>
143
+ <span>{deal.finetuningAllowed !== null ? (deal.finetuningAllowed ? 'Yes' : 'No') : '—'}</span>
144
+ </div>
145
+ <div className="flex justify-between">
146
+ <span className="text-text-muted">Inference</span>
147
+ <span>{deal.inferenceAllowed !== null ? (deal.inferenceAllowed ? 'Yes' : 'No') : '—'}</span>
148
+ </div>
149
+ <div className="flex justify-between">
150
+ <span className="text-text-muted">Redistribution</span>
151
+ <span>{deal.redistributionAllowed !== null ? (deal.redistributionAllowed ? 'Yes' : 'No') : '—'}</span>
152
+ </div>
153
+ {deal.deletionRequired && (
154
+ <div className="flex justify-between">
155
+ <span className="text-text-muted">Deletion Required</span>
156
+ <span>Yes</span>
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+
162
+ <div>
163
+ <h3 className="text-lg font-semibold mb-4">Creator Compensation</h3>
164
+ <div className="space-y-2">
165
+ <div className="flex justify-between">
166
+ <span className="text-text-muted">Compensated</span>
167
+ <span>
168
+ {deal.creatorsCompensated === true ? (
169
+ <span className="badge badge-primary">Yes</span>
170
+ ) : deal.creatorsCompensated === false ? (
171
+ 'No'
172
+ ) : (
173
+ 'Unclear'
174
+ )}
175
+ </span>
176
+ </div>
177
+ {deal.creatorSplitPercentage && (
178
+ <div className="flex justify-between">
179
+ <span className="text-text-muted">Split</span>
180
+ <span>{deal.creatorSplitPercentage}%</span>
181
+ </div>
182
+ )}
183
+ {deal.revenueShare && (
184
+ <div className="flex justify-between">
185
+ <span className="text-text-muted">Revenue Share</span>
186
+ <span>Yes</span>
187
+ </div>
188
+ )}
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ {/* Pricing Normalizations */}
194
+ {(() => {
195
+ const normalizations = deal.pricingNormalizations || []
196
+ if (normalizations.length > 0) {
197
+ return (
198
+ <div>
199
+ <h3 className="text-lg font-semibold mb-4">Pricing Normalizations</h3>
200
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
201
+ {normalizations.map((norm, idx) => (
202
+ <div key={idx} className="border border-border-subtle rounded p-3">
203
+ <div className="text-xs text-text-muted mb-1">Per {norm.unitType}</div>
204
+ <div className="font-medium">
205
+ {norm.normalizedCostPerUnit < 0.001
206
+ ? `$${(norm.normalizedCostPerUnit * 1000000).toFixed(2)}/1M`
207
+ : norm.normalizedCostPerUnit < 1
208
+ ? `$${norm.normalizedCostPerUnit.toFixed(4)}`
209
+ : `$${norm.normalizedCostPerUnit.toFixed(2)}`}
210
+ </div>
211
+ <div className="text-xs text-text-muted mt-1">
212
+ {norm.normalizationMethod}
213
+ </div>
214
+ </div>
215
+ ))}
216
+ </div>
217
+ </div>
218
+ )
219
+ }
220
+ return null
221
+ })()}
222
+
223
+ {/* Notes */}
224
+ {deal.notes && (
225
+ <div>
226
+ <h3 className="text-lg font-semibold mb-2">Notes</h3>
227
+ <p className="text-text-muted leading-relaxed">{deal.notes}</p>
228
+ </div>
229
+ )}
230
+
231
+ {/* Sources */}
232
+ <div>
233
+ <h3 className="text-lg font-semibold mb-4">Sources</h3>
234
+ <div className="space-y-2">
235
+ {deal.sourcePrimary && (
236
+ <div className="text-text-muted">
237
+ Primary: <span className="text-text font-medium">{deal.sourcePrimary}</span>
238
+ </div>
239
+ )}
240
+ {deal.sources && (() => {
241
+ try {
242
+ const sourcesArray = JSON.parse(deal.sources)
243
+ if (Array.isArray(sourcesArray) && sourcesArray.length > 0) {
244
+ return (
245
+ <div>
246
+ <div className="text-text-muted mb-2">Additional sources:</div>
247
+ <ul className="list-disc list-inside space-y-1">
248
+ {sourcesArray.map((source: string, idx: number) => (
249
+ <li key={idx}>
250
+ <a
251
+ href={source}
252
+ target="_blank"
253
+ rel="noopener noreferrer"
254
+ className="text-accent hover:text-accent-hover break-all"
255
+ >
256
+ {source}
257
+ </a>
258
+ </li>
259
+ ))}
260
+ </ul>
261
+ </div>
262
+ )
263
+ }
264
+ } catch (e) {
265
+ // If not valid JSON, treat as plain string
266
+ if (deal.sources) {
267
+ return (
268
+ <div>
269
+ <a
270
+ href={deal.sources}
271
+ target="_blank"
272
+ rel="noopener noreferrer"
273
+ className="text-accent hover:text-accent-hover break-all"
274
+ >
275
+ {deal.sources}
276
+ </a>
277
+ </div>
278
+ )
279
+ }
280
+ }
281
+ return null
282
+ })()}
283
+ {!deal.sourcePrimary && (!deal.sources || (deal.sources && JSON.parse(deal.sources || '[]').length === 0)) && (
284
+ <div className="text-text-muted">No sources available</div>
285
+ )}
286
+ </div>
287
+ </div>
288
+
289
+ {/* Modality Badge */}
290
+ <div className="pt-4 border-t border-border">
291
+ <span className="badge badge-secondary">{deal.modality}</span>
292
+ {deal.exclusive && (
293
+ <span className="badge badge-primary ml-2">Exclusive</span>
294
+ )}
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ )
300
+ }
301
+
app/deals/DealsClient.tsx ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import DealModal from './DealModal'
5
+
6
+ interface Deal {
7
+ id: string
8
+ provider: string
9
+ buyer: string
10
+ modality: string
11
+ dataType: string | null
12
+ priceUsd: number | null
13
+ priceRangeMinUsd: number | null
14
+ priceRangeMaxUsd: number | null
15
+ reportedTerms: string | null
16
+ exclusive: boolean | null
17
+ creatorsCompensated: boolean | null
18
+ creatorSplitPercentage: number | null
19
+ revenueShare: boolean | null
20
+ date: string | null
21
+ dealType: string | null
22
+ pricingMechanism: string | null
23
+ sourcePrimary: string | null
24
+ trainingAllowed: boolean | null
25
+ finetuningAllowed: boolean | null
26
+ inferenceAllowed: boolean | null
27
+ redistributionAllowed: boolean | null
28
+ deletionRequired: boolean | null
29
+ notes: string | null
30
+ sources: string | null
31
+ pricingNormalizations?: Array<{
32
+ unitType: string
33
+ normalizedCostPerUnit: number
34
+ normalizationMethod: string
35
+ }>
36
+ }
37
+
38
+ interface DealsClientProps {
39
+ initialDeals: Deal[]
40
+ }
41
+
42
+ export default function DealsClient({ initialDeals }: DealsClientProps) {
43
+ const [deals] = useState<Deal[]>(initialDeals)
44
+ const [filters, setFilters] = useState({
45
+ modality: '',
46
+ buyer: '',
47
+ provider: '',
48
+ exclusive: '',
49
+ creatorsCompensated: '',
50
+ })
51
+ const [searchQuery, setSearchQuery] = useState('')
52
+ const [selectedDeal, setSelectedDeal] = useState<Deal | null>(null)
53
+ const [isModalOpen, setIsModalOpen] = useState(false)
54
+ const [sortBy, setSortBy] = useState<{ column: string; direction: 'asc' | 'desc' }>({
55
+ column: 'date',
56
+ direction: 'desc',
57
+ })
58
+
59
+ function formatPrice(deal: Deal) {
60
+ if (deal.priceUsd) {
61
+ if (deal.priceUsd >= 1000000000) {
62
+ return `$${(deal.priceUsd / 1000000000).toFixed(1)}B`
63
+ }
64
+ if (deal.priceUsd >= 1000000) {
65
+ return `$${(deal.priceUsd / 1000000).toFixed(0)}M`
66
+ }
67
+ if (deal.priceUsd >= 1000) {
68
+ return `$${(deal.priceUsd / 1000).toFixed(0)}K`
69
+ }
70
+ return `$${deal.priceUsd.toFixed(0)}`
71
+ }
72
+ if (deal.priceRangeMinUsd && deal.priceRangeMaxUsd) {
73
+ return `$${deal.priceRangeMinUsd.toFixed(2)}–${deal.priceRangeMaxUsd.toFixed(2)}`
74
+ }
75
+ return deal.reportedTerms || 'Undisclosed'
76
+ }
77
+
78
+ function calculateNormalization(deal: Deal, unitType: string): number | null {
79
+ if (!deal.priceUsd) return null
80
+
81
+ switch (unitType) {
82
+ case 'token':
83
+ if (deal.modality === 'Text') {
84
+ if (deal.dataType?.toLowerCase().includes('book')) {
85
+ return deal.priceUsd / 80000 // per token (80k tokens per book)
86
+ }
87
+ return deal.priceUsd / 1000 // per token (article estimate)
88
+ }
89
+ return null
90
+
91
+ case 'record':
92
+ if (deal.dealType === 'per-unit') {
93
+ return deal.priceUsd / 1000000 // rough estimate
94
+ }
95
+ return null
96
+
97
+ case 'image':
98
+ if (deal.modality === 'Image') {
99
+ if (deal.dataType?.includes('200M')) {
100
+ return deal.priceUsd / 200000000 // Freepik example
101
+ }
102
+ return deal.priceUsd / 1000000 // rough estimate
103
+ }
104
+ return null
105
+
106
+ case 'minute':
107
+ if (deal.modality === 'Video' || deal.modality === 'Audio') {
108
+ return deal.priceUsd / 60 // per minute (1 hour estimate)
109
+ }
110
+ return null
111
+
112
+ default:
113
+ return null
114
+ }
115
+ }
116
+
117
+ function formatNormalizedPrice(price: number | null, unitType: string): string {
118
+ if (price === null) return ''
119
+
120
+ if (price < 0.001) {
121
+ return `$${(price * 1000000).toFixed(2)}/1M ${unitType}s`
122
+ }
123
+ if (price < 1) {
124
+ return `$${price.toFixed(4)}/${unitType}`
125
+ }
126
+ return `$${price.toFixed(2)}/${unitType}`
127
+ }
128
+
129
+ function getNormalizations(deal: Deal) {
130
+ const normalizations: Array<{ unitType: string; price: number; method: 'stored' | 'calculated' }> = []
131
+
132
+ // Add stored normalizations
133
+ if (deal.pricingNormalizations) {
134
+ deal.pricingNormalizations.forEach(norm => {
135
+ normalizations.push({
136
+ unitType: norm.unitType,
137
+ price: norm.normalizedCostPerUnit,
138
+ method: 'stored',
139
+ })
140
+ })
141
+ }
142
+
143
+ // Calculate missing normalizations
144
+ const unitTypes = ['token', 'record', 'image', 'minute']
145
+ const storedUnitTypes = new Set(deal.pricingNormalizations?.map(n => n.unitType) || [])
146
+
147
+ unitTypes.forEach(unitType => {
148
+ if (!storedUnitTypes.has(unitType)) {
149
+ const calculated = calculateNormalization(deal, unitType)
150
+ if (calculated !== null) {
151
+ normalizations.push({
152
+ unitType,
153
+ price: calculated,
154
+ method: 'calculated',
155
+ })
156
+ }
157
+ }
158
+ })
159
+
160
+ return normalizations
161
+ }
162
+
163
+ let filteredDeals = deals.filter(deal => {
164
+ if (filters.modality && deal.modality !== filters.modality) return false
165
+ if (filters.buyer && !deal.buyer.toLowerCase().includes(filters.buyer.toLowerCase())) return false
166
+ if (filters.provider && !deal.provider.toLowerCase().includes(filters.provider.toLowerCase())) return false
167
+ if (filters.exclusive === 'true' && deal.exclusive !== true) return false
168
+ if (filters.exclusive === 'false' && deal.exclusive !== false) return false
169
+ if (filters.creatorsCompensated === 'true' && deal.creatorsCompensated !== true) return false
170
+ if (filters.creatorsCompensated === 'false' && deal.creatorsCompensated !== false) return false
171
+
172
+ if (searchQuery) {
173
+ const query = searchQuery.toLowerCase()
174
+ if (
175
+ !deal.provider.toLowerCase().includes(query) &&
176
+ !deal.buyer.toLowerCase().includes(query) &&
177
+ !deal.modality.toLowerCase().includes(query) &&
178
+ !(deal.reportedTerms && deal.reportedTerms.toLowerCase().includes(query))
179
+ ) {
180
+ return false
181
+ }
182
+ }
183
+ return true
184
+ })
185
+
186
+ // Sort deals
187
+ const sortedDeals = [...filteredDeals].sort((a, b) => {
188
+ const { column, direction } = sortBy
189
+ let comparison = 0
190
+
191
+ switch (column) {
192
+ case 'provider':
193
+ comparison = a.provider.localeCompare(b.provider)
194
+ break
195
+ case 'buyer':
196
+ comparison = a.buyer.localeCompare(b.buyer)
197
+ break
198
+ case 'modality':
199
+ comparison = a.modality.localeCompare(b.modality)
200
+ break
201
+ case 'price':
202
+ const priceA = a.priceUsd || a.priceRangeMinUsd || 0
203
+ const priceB = b.priceUsd || b.priceRangeMinUsd || 0
204
+ comparison = priceA - priceB
205
+ break
206
+ case 'exclusive':
207
+ const exclusiveA = a.exclusive === true ? 1 : a.exclusive === false ? 0 : -1
208
+ const exclusiveB = b.exclusive === true ? 1 : b.exclusive === false ? 0 : -1
209
+ comparison = exclusiveA - exclusiveB
210
+ break
211
+ case 'creatorsCompensated':
212
+ const compA = a.creatorsCompensated === true ? 1 : a.creatorsCompensated === false ? 0 : -1
213
+ const compB = b.creatorsCompensated === true ? 1 : b.creatorsCompensated === false ? 0 : -1
214
+ comparison = compA - compB
215
+ break
216
+ case 'date':
217
+ default:
218
+ if (!a.date && !b.date) comparison = 0
219
+ else if (!a.date) comparison = 1
220
+ else if (!b.date) comparison = -1
221
+ else comparison = a.date.localeCompare(b.date)
222
+ break
223
+ }
224
+
225
+ return direction === 'asc' ? comparison : -comparison
226
+ })
227
+
228
+ const modalities = Array.from(new Set(deals.map(d => d.modality))).sort()
229
+ const buyers = Array.from(new Set(deals.flatMap(d => d.buyer.split(',').map(b => b.trim())))).sort()
230
+ const providers = Array.from(new Set(deals.map(d => d.provider))).sort()
231
+
232
+ const handleDealClick = (deal: Deal) => {
233
+ setSelectedDeal(deal)
234
+ setIsModalOpen(true)
235
+ }
236
+
237
+ const handleSort = (column: string) => {
238
+ setSortBy(prev => ({
239
+ column,
240
+ direction: prev.column === column && prev.direction === 'asc' ? 'desc' : 'asc',
241
+ }))
242
+ }
243
+
244
+ const getSortIndicator = (column: string) => {
245
+ if (sortBy.column !== column) return null
246
+ return sortBy.direction === 'asc' ? '↑' : '↓'
247
+ }
248
+
249
+ return (
250
+ <>
251
+ <DealModal
252
+ deal={selectedDeal}
253
+ isOpen={isModalOpen}
254
+ onClose={() => {
255
+ setIsModalOpen(false)
256
+ setSelectedDeal(null)
257
+ }}
258
+ />
259
+ {/* Search and Filters */}
260
+ <div className="card mb-6">
261
+ <div className="mb-4">
262
+ <input
263
+ type="text"
264
+ placeholder="Search deals..."
265
+ value={searchQuery}
266
+ onChange={(e) => setSearchQuery(e.target.value)}
267
+ className="input w-full"
268
+ />
269
+ </div>
270
+
271
+ <div className="grid grid-cols-1 md:grid-cols-5 gap-4">
272
+ <div>
273
+ <label className="block text-sm font-medium text-text-muted mb-2">Modality</label>
274
+ <select
275
+ value={filters.modality}
276
+ onChange={(e) => setFilters({ ...filters, modality: e.target.value })}
277
+ className="input"
278
+ >
279
+ <option value="">All</option>
280
+ {modalities.map(m => (
281
+ <option key={m} value={m}>{m}</option>
282
+ ))}
283
+ </select>
284
+ </div>
285
+
286
+ <div>
287
+ <label className="block text-sm font-medium text-text-muted mb-2">Buyer</label>
288
+ <select
289
+ value={filters.buyer}
290
+ onChange={(e) => setFilters({ ...filters, buyer: e.target.value })}
291
+ className="input"
292
+ >
293
+ <option value="">All</option>
294
+ {buyers.map(b => (
295
+ <option key={b} value={b}>{b}</option>
296
+ ))}
297
+ </select>
298
+ </div>
299
+
300
+ <div>
301
+ <label className="block text-sm font-medium text-text-muted mb-2">Provider</label>
302
+ <select
303
+ value={filters.provider}
304
+ onChange={(e) => setFilters({ ...filters, provider: e.target.value })}
305
+ className="input"
306
+ >
307
+ <option value="">All</option>
308
+ {providers.map(p => (
309
+ <option key={p} value={p}>{p}</option>
310
+ ))}
311
+ </select>
312
+ </div>
313
+
314
+ <div>
315
+ <label className="block text-sm font-medium text-text-muted mb-2">Exclusive</label>
316
+ <select
317
+ value={filters.exclusive}
318
+ onChange={(e) => setFilters({ ...filters, exclusive: e.target.value })}
319
+ className="input"
320
+ >
321
+ <option value="">All</option>
322
+ <option value="true">Yes</option>
323
+ <option value="false">No</option>
324
+ </select>
325
+ </div>
326
+
327
+ <div>
328
+ <label className="block text-sm font-medium text-text-muted mb-2">Creators Comp.</label>
329
+ <select
330
+ value={filters.creatorsCompensated}
331
+ onChange={(e) => setFilters({ ...filters, creatorsCompensated: e.target.value })}
332
+ className="input"
333
+ >
334
+ <option value="">All</option>
335
+ <option value="true">Yes</option>
336
+ <option value="false">No</option>
337
+ </select>
338
+ </div>
339
+ </div>
340
+ </div>
341
+
342
+ {/* Results */}
343
+ <div className="card overflow-hidden p-0">
344
+ <div className="overflow-x-auto">
345
+ <table className="table">
346
+ <thead>
347
+ <tr>
348
+ <th
349
+ className="cursor-pointer hover:bg-border-subtle select-none"
350
+ onClick={() => handleSort('provider')}
351
+ title="Click to sort by provider"
352
+ >
353
+ Provider
354
+ {getSortIndicator('provider') && (
355
+ <span className="ml-2 text-text-muted">{getSortIndicator('provider')}</span>
356
+ )}
357
+ </th>
358
+ <th
359
+ className="cursor-pointer hover:bg-border-subtle select-none"
360
+ onClick={() => handleSort('buyer')}
361
+ title="Click to sort by buyer"
362
+ >
363
+ Buyer
364
+ {getSortIndicator('buyer') && (
365
+ <span className="ml-2 text-text-muted">{getSortIndicator('buyer')}</span>
366
+ )}
367
+ </th>
368
+ <th
369
+ className="cursor-pointer hover:bg-border-subtle select-none"
370
+ onClick={() => handleSort('modality')}
371
+ title="Click to sort by modality"
372
+ >
373
+ Modality
374
+ {getSortIndicator('modality') && (
375
+ <span className="ml-2 text-text-muted">{getSortIndicator('modality')}</span>
376
+ )}
377
+ </th>
378
+ <th
379
+ className="cursor-pointer hover:bg-border-subtle select-none"
380
+ onClick={() => handleSort('price')}
381
+ title="Click to sort by price"
382
+ >
383
+ Price
384
+ {getSortIndicator('price') && (
385
+ <span className="ml-2 text-text-muted">{getSortIndicator('price')}</span>
386
+ )}
387
+ </th>
388
+ <th
389
+ className="cursor-pointer hover:bg-border-subtle select-none"
390
+ onClick={() => handleSort('exclusive')}
391
+ title="Click to sort by exclusivity"
392
+ >
393
+ Exclusive
394
+ {getSortIndicator('exclusive') && (
395
+ <span className="ml-2 text-text-muted">{getSortIndicator('exclusive')}</span>
396
+ )}
397
+ </th>
398
+ <th
399
+ className="cursor-pointer hover:bg-border-subtle select-none"
400
+ onClick={() => handleSort('creatorsCompensated')}
401
+ title="Click to sort by creator compensation"
402
+ >
403
+ Creators Comp.
404
+ {getSortIndicator('creatorsCompensated') && (
405
+ <span className="ml-2 text-text-muted">{getSortIndicator('creatorsCompensated')}</span>
406
+ )}
407
+ </th>
408
+ <th
409
+ className="cursor-pointer hover:bg-border-subtle select-none"
410
+ onClick={() => handleSort('date')}
411
+ title="Click to sort by date"
412
+ >
413
+ Date
414
+ {getSortIndicator('date') && (
415
+ <span className="ml-2 text-text-muted">{getSortIndicator('date')}</span>
416
+ )}
417
+ </th>
418
+ </tr>
419
+ </thead>
420
+ <tbody>
421
+ {sortedDeals.length === 0 ? (
422
+ <tr>
423
+ <td colSpan={7} className="text-center py-8 text-text-muted">
424
+ No deals found
425
+ </td>
426
+ </tr>
427
+ ) : (
428
+ sortedDeals.map((deal) => (
429
+ <tr
430
+ key={deal.id}
431
+ onClick={() => handleDealClick(deal)}
432
+ className="cursor-pointer hover:bg-border-subtle transition-colors"
433
+ >
434
+ <td className="font-medium text-accent hover:text-accent-hover">
435
+ {deal.provider}
436
+ </td>
437
+ <td>{deal.buyer}</td>
438
+ <td>
439
+ <span className="badge badge-secondary">{deal.modality}</span>
440
+ </td>
441
+ <td>
442
+ <div>
443
+ <div className="font-medium">{formatPrice(deal)}</div>
444
+ {(() => {
445
+ const normalizations = getNormalizations(deal)
446
+ if (normalizations.length > 0) {
447
+ // Show stored normalizations first, then calculated
448
+ const stored = normalizations.filter(n => n.method === 'stored')
449
+ const calculated = normalizations.filter(n => n.method === 'calculated')
450
+ const toShow = [...stored, ...calculated].slice(0, 2)
451
+
452
+ return (
453
+ <div className="mt-1 space-y-0.5">
454
+ {toShow.map((norm, idx) => (
455
+ <div
456
+ key={idx}
457
+ className={`text-xs ${
458
+ norm.method === 'calculated'
459
+ ? 'text-text-muted/50 italic'
460
+ : 'text-text-muted/70'
461
+ }`}
462
+ title={norm.method === 'calculated' ? 'Estimated' : 'From database'}
463
+ >
464
+ {formatNormalizedPrice(norm.price, norm.unitType)}
465
+ {norm.method === 'calculated' && ' *'}
466
+ </div>
467
+ ))}
468
+ </div>
469
+ )
470
+ }
471
+ return null
472
+ })()}
473
+ </div>
474
+ </td>
475
+ <td>
476
+ {deal.exclusive === true ? (
477
+ <span className="badge badge-primary">Yes</span>
478
+ ) : deal.exclusive === false ? (
479
+ <span className="text-text-muted">No</span>
480
+ ) : (
481
+ <span className="text-text-muted">—</span>
482
+ )}
483
+ </td>
484
+ <td>
485
+ {deal.creatorsCompensated === true ? (
486
+ <span className="badge badge-primary">Yes</span>
487
+ ) : deal.creatorsCompensated === false ? (
488
+ <span className="text-text-muted">No</span>
489
+ ) : (
490
+ <span className="text-text-muted">—</span>
491
+ )}
492
+ </td>
493
+ <td className="text-text-muted">{deal.date || '—'}</td>
494
+ </tr>
495
+ ))
496
+ )}
497
+ </tbody>
498
+ </table>
499
+ </div>
500
+ </div>
501
+ </>
502
+ )
503
+ }
504
+
app/deals/[id]/page.tsx ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { notFound } from 'next/navigation'
2
+ import Link from 'next/link'
3
+ import { prisma } from '@/lib/prisma'
4
+
5
+ async function getDeal(id: string) {
6
+ const deal = await prisma.deal.findUnique({
7
+ where: { id },
8
+ include: {
9
+ buyerRelations: {
10
+ include: {
11
+ buyer: true,
12
+ },
13
+ },
14
+ providerRelation: true,
15
+ pricingNormalizations: true,
16
+ },
17
+ })
18
+ return deal
19
+ }
20
+
21
+ function formatPrice(deal: any) {
22
+ if (deal.priceUsd) {
23
+ if (deal.priceUsd >= 1000000000) {
24
+ return `$${(deal.priceUsd / 1000000000).toFixed(1)}B`
25
+ }
26
+ if (deal.priceUsd >= 1000000) {
27
+ return `$${(deal.priceUsd / 1000000).toFixed(0)}M`
28
+ }
29
+ if (deal.priceUsd >= 1000) {
30
+ return `$${(deal.priceUsd / 1000).toFixed(0)}K`
31
+ }
32
+ return `$${deal.priceUsd.toFixed(0)}`
33
+ }
34
+ if (deal.priceRangeMinUsd && deal.priceRangeMaxUsd) {
35
+ return `$${deal.priceRangeMinUsd.toFixed(2)}–${deal.priceRangeMaxUsd.toFixed(2)}`
36
+ }
37
+ return deal.reportedTerms || 'Undisclosed'
38
+ }
39
+
40
+ export default async function DealDetailPage({
41
+ params,
42
+ }: {
43
+ params: { id: string }
44
+ }) {
45
+ const deal = await getDeal(params.id)
46
+
47
+ if (!deal) {
48
+ notFound()
49
+ }
50
+
51
+ return (
52
+ <main className="min-h-screen bg-background">
53
+ <div className="container-content section-padding">
54
+ <Link
55
+ href="/deals"
56
+ className="text-accent hover:text-accent-hover mb-6 inline-block"
57
+ >
58
+ ← Back to Deals
59
+ </Link>
60
+
61
+ <div className="max-w-4xl">
62
+ {/* Header Card */}
63
+ <div className="card mb-8">
64
+ <div className="flex items-start justify-between mb-6">
65
+ <div>
66
+ <h1 className="text-3xl font-semibold mb-2">
67
+ {deal.provider} → {deal.buyer}
68
+ </h1>
69
+ <p className="text-text-muted">{deal.dataType}</p>
70
+ </div>
71
+ <div className="flex gap-2">
72
+ <span className="badge badge-secondary">{deal.modality}</span>
73
+ {deal.exclusive && (
74
+ <span className="badge badge-primary">Exclusive</span>
75
+ )}
76
+ </div>
77
+ </div>
78
+
79
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-6 border-t border-border">
80
+ <div>
81
+ <div className="text-sm text-text-muted mb-1">Price</div>
82
+ <div className="text-xl font-semibold">{formatPrice(deal)}</div>
83
+ </div>
84
+ <div>
85
+ <div className="text-sm text-text-muted mb-1">Deal Type</div>
86
+ <div className="font-medium">{deal.dealType || deal.pricingMechanism}</div>
87
+ </div>
88
+ <div>
89
+ <div className="text-sm text-text-muted mb-1">Date</div>
90
+ <div className="font-medium">{deal.date || '—'}</div>
91
+ </div>
92
+ <div>
93
+ <div className="text-sm text-text-muted mb-1">Source</div>
94
+ <div className="font-medium">{deal.sourcePrimary || '—'}</div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ {/* Reported Terms */}
100
+ {deal.reportedTerms && (
101
+ <div className="card mb-8">
102
+ <h2 className="text-xl font-semibold mb-4">Reported Terms</h2>
103
+ <p className="text-lg leading-relaxed">{deal.reportedTerms}</p>
104
+ </div>
105
+ )}
106
+
107
+ {/* Rights & Compensation */}
108
+ <div className="grid md:grid-cols-2 gap-6 mb-8">
109
+ <div className="card">
110
+ <h3 className="text-lg font-semibold mb-4">Rights Granted</h3>
111
+ <div className="space-y-2">
112
+ <div className="flex justify-between">
113
+ <span className="text-text-muted">Training</span>
114
+ <span>{deal.trainingAllowed !== null ? (deal.trainingAllowed ? 'Yes' : 'No') : '—'}</span>
115
+ </div>
116
+ <div className="flex justify-between">
117
+ <span className="text-text-muted">Fine-tuning</span>
118
+ <span>{deal.finetuningAllowed !== null ? (deal.finetuningAllowed ? 'Yes' : 'No') : '—'}</span>
119
+ </div>
120
+ <div className="flex justify-between">
121
+ <span className="text-text-muted">Inference</span>
122
+ <span>{deal.inferenceAllowed !== null ? (deal.inferenceAllowed ? 'Yes' : 'No') : '—'}</span>
123
+ </div>
124
+ <div className="flex justify-between">
125
+ <span className="text-text-muted">Redistribution</span>
126
+ <span>{deal.redistributionAllowed !== null ? (deal.redistributionAllowed ? 'Yes' : 'No') : '—'}</span>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <div className="card">
132
+ <h3 className="text-lg font-semibold mb-4">Creator Compensation</h3>
133
+ <div className="space-y-2">
134
+ <div className="flex justify-between">
135
+ <span className="text-text-muted">Compensated</span>
136
+ <span>
137
+ {deal.creatorsCompensated === true ? (
138
+ <span className="badge badge-primary">Yes</span>
139
+ ) : deal.creatorsCompensated === false ? (
140
+ 'No'
141
+ ) : (
142
+ 'Unclear'
143
+ )}
144
+ </span>
145
+ </div>
146
+ {deal.creatorSplitPercentage && (
147
+ <div className="flex justify-between">
148
+ <span className="text-text-muted">Split</span>
149
+ <span>{deal.creatorSplitPercentage}%</span>
150
+ </div>
151
+ )}
152
+ {deal.revenueShare && (
153
+ <div className="flex justify-between">
154
+ <span className="text-text-muted">Revenue Share</span>
155
+ <span>Yes</span>
156
+ </div>
157
+ )}
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ {/* Notes */}
163
+ {deal.notes && (
164
+ <div className="card mb-8">
165
+ <h3 className="text-lg font-semibold mb-4">Notes</h3>
166
+ <p className="leading-relaxed">{deal.notes}</p>
167
+ </div>
168
+ )}
169
+
170
+ {/* Sources */}
171
+ <div className="card">
172
+ <h3 className="text-lg font-semibold mb-4">Sources</h3>
173
+ <div className="space-y-2">
174
+ {deal.sourcePrimary && (
175
+ <div className="text-text-muted">
176
+ Primary: <span className="text-text">{deal.sourcePrimary}</span>
177
+ </div>
178
+ )}
179
+ {deal.sources && JSON.parse(deal.sources).length > 0 && (
180
+ <div>
181
+ <div className="text-text-muted mb-2">Additional sources:</div>
182
+ <ul className="list-disc list-inside space-y-1">
183
+ {JSON.parse(deal.sources).map((source: string, idx: number) => (
184
+ <li key={idx}>
185
+ <a
186
+ href={source}
187
+ target="_blank"
188
+ rel="noopener noreferrer"
189
+ className="text-accent hover:text-accent-hover"
190
+ >
191
+ {source}
192
+ </a>
193
+ </li>
194
+ ))}
195
+ </ul>
196
+ </div>
197
+ )}
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </main>
203
+ )
204
+ }
205
+
app/deals/page.tsx ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { redirect } from 'next/navigation'
2
+
3
+ export default function DealsPage() {
4
+ // Redirect to home page since deals explorer is now the main view
5
+ redirect('/')
6
+ }
app/globals.css ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Humanist Design System - Minimalist Typography */
6
+
7
+ @layer base {
8
+ :root {
9
+ /* Color Palette - Muted, Humanist */
10
+ --color-base: #0A0A0A;
11
+ --color-text: #1A1A1A;
12
+ --color-text-muted: #6B7280;
13
+ --color-accent: #2563EB;
14
+ --color-accent-hover: #1D4ED8;
15
+ --color-background: #FAFAFA;
16
+ --color-surface: #FFFFFF;
17
+ --color-border: #E5E7EB;
18
+ --color-border-subtle: #F3F4F6;
19
+
20
+ /* Typography */
21
+ --font-sans: 'Public Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
22
+ --font-serif: 'Charter', 'Georgia', serif;
23
+ --font-mono: 'JetBrains Mono', 'Menlo', monospace;
24
+
25
+ /* Spacing - Generous, Humanist */
26
+ --spacing-xs: 0.5rem; /* 8px */
27
+ --spacing-sm: 0.75rem; /* 12px */
28
+ --spacing-md: 1rem; /* 16px */
29
+ --spacing-lg: 1.5rem; /* 24px */
30
+ --spacing-xl: 2rem; /* 32px */
31
+ --spacing-2xl: 3rem; /* 48px */
32
+ --spacing-3xl: 4rem; /* 64px */
33
+
34
+ /* Typography Scale */
35
+ --text-xs: 0.75rem; /* 12px */
36
+ --text-sm: 0.875rem; /* 14px */
37
+ --text-base: 1rem; /* 16px */
38
+ --text-lg: 1.125rem; /* 18px */
39
+ --text-xl: 1.25rem; /* 20px */
40
+ --text-2xl: 1.5rem; /* 24px */
41
+ --text-3xl: 1.875rem; /* 30px */
42
+ --text-4xl: 2.25rem; /* 36px */
43
+ --text-5xl: 3rem; /* 48px */
44
+
45
+ /* Line Heights - Comfortable Reading */
46
+ --leading-tight: 1.25;
47
+ --leading-normal: 1.5;
48
+ --leading-relaxed: 1.75;
49
+ --leading-loose: 2;
50
+
51
+ /* Max Content Width */
52
+ --max-width-content: 1200px;
53
+ --max-width-narrow: 800px;
54
+ }
55
+
56
+ * {
57
+ @apply border-border;
58
+ }
59
+
60
+ body {
61
+ @apply bg-background text-text;
62
+ font-family: var(--font-sans);
63
+ font-size: var(--text-base);
64
+ line-height: var(--leading-relaxed);
65
+ -webkit-font-smoothing: antialiased;
66
+ -moz-osx-font-smoothing: grayscale;
67
+ }
68
+
69
+ /* Typography Hierarchy */
70
+ h1, h2, h3, h4, h5, h6 {
71
+ @apply font-semibold text-base;
72
+ line-height: var(--leading-tight);
73
+ letter-spacing: -0.02em;
74
+ }
75
+
76
+ h1 {
77
+ font-size: var(--text-4xl);
78
+ line-height: var(--leading-tight);
79
+ @apply mb-6;
80
+ }
81
+
82
+ h2 {
83
+ font-size: var(--text-3xl);
84
+ @apply mb-5;
85
+ }
86
+
87
+ h3 {
88
+ font-size: var(--text-2xl);
89
+ @apply mb-4;
90
+ }
91
+
92
+ h4 {
93
+ font-size: var(--text-xl);
94
+ @apply mb-3;
95
+ }
96
+
97
+ p {
98
+ @apply mb-4;
99
+ line-height: var(--leading-relaxed);
100
+ }
101
+
102
+ /* Links - Subtle, Humanist */
103
+ a {
104
+ @apply text-accent no-underline;
105
+ transition: color 0.2s ease;
106
+ }
107
+
108
+ a:hover {
109
+ @apply text-accent-hover;
110
+ text-decoration: underline;
111
+ text-underline-offset: 2px;
112
+ }
113
+
114
+ /* Focus States - Accessible */
115
+ *:focus-visible {
116
+ @apply outline-2 outline-offset-2 outline-accent;
117
+ }
118
+ }
119
+
120
+ @layer components {
121
+ /* Container - Max Width with Generous Padding */
122
+ .container-content {
123
+ @apply mx-auto px-6;
124
+ max-width: var(--max-width-content);
125
+ }
126
+
127
+ .container-narrow {
128
+ @apply mx-auto px-6;
129
+ max-width: var(--max-width-narrow);
130
+ }
131
+
132
+ /* Cards - Minimalist, Subtle */
133
+ .card {
134
+ @apply bg-surface border border-border rounded-lg p-6;
135
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
136
+ }
137
+
138
+ .card-hover {
139
+ @apply card transition-all duration-200;
140
+ }
141
+
142
+ .card-hover:hover {
143
+ @apply shadow-md;
144
+ transform: translateY(-1px);
145
+ }
146
+
147
+ /* Buttons - Minimalist */
148
+ .btn {
149
+ @apply px-4 py-2 rounded-md font-medium transition-all duration-200;
150
+ @apply focus:outline-none focus:ring-2 focus:ring-offset-2;
151
+ }
152
+
153
+ .btn-primary {
154
+ @apply btn bg-accent text-white;
155
+ @apply hover:bg-accent-hover;
156
+ @apply focus:ring-accent;
157
+ }
158
+
159
+ .btn-secondary {
160
+ @apply btn bg-surface border border-border text-text;
161
+ @apply hover:bg-border-subtle;
162
+ @apply focus:ring-border;
163
+ }
164
+
165
+ .btn-ghost {
166
+ @apply btn bg-transparent text-text-muted;
167
+ @apply hover:bg-border-subtle hover:text-text;
168
+ }
169
+
170
+ /* Tables - Clean, Readable */
171
+ .table {
172
+ @apply w-full border-collapse;
173
+ }
174
+
175
+ .table th {
176
+ @apply text-left font-semibold text-sm text-text-muted py-3 px-4;
177
+ @apply border-b border-border;
178
+ @apply select-none;
179
+ }
180
+
181
+ .table td {
182
+ @apply py-3 px-4 border-b border-border-subtle;
183
+ }
184
+
185
+ .table tr:hover {
186
+ @apply bg-border-subtle;
187
+ }
188
+
189
+ /* Badges/Tags - Subtle */
190
+ .badge {
191
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
192
+ }
193
+
194
+ .badge-primary {
195
+ @apply badge text-accent;
196
+ background-color: rgba(37, 99, 235, 0.1); /* accent color with 10% opacity */
197
+ }
198
+
199
+ .badge-secondary {
200
+ @apply badge bg-border text-text-muted;
201
+ }
202
+
203
+ /* Inputs - Clean */
204
+ .input {
205
+ @apply w-full px-4 py-2 border border-border rounded-md;
206
+ @apply bg-surface text-text;
207
+ @apply focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent;
208
+ @apply transition-all duration-200;
209
+ }
210
+
211
+ /* Stats Cards */
212
+ .stat-card {
213
+ @apply card;
214
+ }
215
+
216
+ .stat-value {
217
+ @apply text-3xl font-semibold text-base mb-1;
218
+ }
219
+
220
+ .stat-label {
221
+ @apply text-sm text-text-muted;
222
+ }
223
+ }
224
+
225
+ @layer utilities {
226
+ /* Text Utilities */
227
+ .text-balance {
228
+ text-wrap: balance;
229
+ }
230
+
231
+ /* Spacing Utilities */
232
+ .section-padding {
233
+ @apply py-12 md:py-16 lg:py-20;
234
+ }
235
+ }
236
+
app/layout.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from 'next'
2
+ import { Public_Sans } from 'next/font/google'
3
+ import './globals.css'
4
+
5
+ const publicSans = Public_Sans({
6
+ subsets: ['latin'],
7
+ variable: '--font-sans',
8
+ display: 'swap',
9
+ })
10
+
11
+ export const metadata: Metadata = {
12
+ title: 'AI Training Data Deals Dashboard',
13
+ description: 'Global licensing, acquisition, and commissioning deals for AI training data (2020–2025)',
14
+ }
15
+
16
+ export default function RootLayout({
17
+ children,
18
+ }: {
19
+ children: React.ReactNode
20
+ }) {
21
+ return (
22
+ <html lang="en" className={publicSans.variable}>
23
+ <body>{children}</body>
24
+ </html>
25
+ )
26
+ }
27
+
app/normalization/page.tsx ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { prisma } from '@/lib/prisma'
2
+ import Link from 'next/link'
3
+
4
+ async function getDealsForNormalization() {
5
+ const deals = await prisma.deal.findMany({
6
+ where: {
7
+ OR: [
8
+ { priceUsd: { not: null } },
9
+ { priceRangeMinUsd: { not: null } },
10
+ ],
11
+ },
12
+ orderBy: { priceUsd: 'desc' },
13
+ })
14
+ return deals
15
+ }
16
+
17
+ function normalizePrice(deal: any, unitType: string): number | null {
18
+ // This is a simplified normalization - in production, you'd have
19
+ // more sophisticated logic based on data type and assumptions
20
+
21
+ if (!deal.priceUsd) return null
22
+
23
+ switch (unitType) {
24
+ case 'token':
25
+ // Rough estimate: 1 book ≈ 80k tokens, 1 article ≈ 1k tokens
26
+ if (deal.modality === 'Text') {
27
+ if (deal.dataType?.toLowerCase().includes('book')) {
28
+ return deal.priceUsd / 80000 // per token
29
+ }
30
+ return deal.priceUsd / 1000 // per token (article estimate)
31
+ }
32
+ return null
33
+
34
+ case 'record':
35
+ // For per-unit deals, try to extract from reported terms
36
+ if (deal.dealType === 'per-unit') {
37
+ // This would need more sophisticated parsing
38
+ return deal.priceUsd / 1000000 // rough estimate
39
+ }
40
+ return null
41
+
42
+ case 'image':
43
+ if (deal.modality === 'Image') {
44
+ // Estimate based on deal size
45
+ if (deal.dataType?.includes('200M')) {
46
+ return deal.priceUsd / 200000000 // Freepik example
47
+ }
48
+ return deal.priceUsd / 1000000 // rough estimate
49
+ }
50
+ return null
51
+
52
+ case 'minute':
53
+ if (deal.modality === 'Video' || deal.modality === 'Audio') {
54
+ // Estimate: 1 hour of content
55
+ return deal.priceUsd / 60 // per minute
56
+ }
57
+ return null
58
+
59
+ default:
60
+ return null
61
+ }
62
+ }
63
+
64
+ function formatNormalizedPrice(price: number | null, unitType: string): string {
65
+ if (price === null) return 'N/A'
66
+
67
+ if (price < 0.001) {
68
+ return `$${(price * 1000000).toFixed(2)} per 1M ${unitType}s`
69
+ }
70
+ if (price < 1) {
71
+ return `$${price.toFixed(4)} per ${unitType}`
72
+ }
73
+ return `$${price.toFixed(2)} per ${unitType}`
74
+ }
75
+
76
+ export default async function NormalizationPage() {
77
+ const deals = await getDealsForNormalization()
78
+
79
+ const unitTypes = ['token', 'record', 'image', 'minute']
80
+
81
+ return (
82
+ <main className="min-h-screen bg-background">
83
+ <div className="container-content section-padding">
84
+ <div className="mb-8">
85
+ <h1 className="text-4xl font-semibold mb-4">Pricing Normalization Tool</h1>
86
+ <p className="text-text-muted text-lg">
87
+ Compare deals on an apples-to-apples basis by normalizing to per-unit pricing
88
+ </p>
89
+ </div>
90
+
91
+ <div className="card mb-8">
92
+ <h2 className="text-2xl font-semibold mb-4">How It Works</h2>
93
+ <p className="text-text-muted leading-relaxed mb-4">
94
+ Different deals use different pricing models (per-book, per-track, aggregate licensing, etc.).
95
+ This tool normalizes prices to common units (tokens, records, images, minutes) to enable
96
+ direct comparison.
97
+ </p>
98
+ <p className="text-text-muted text-sm">
99
+ <strong>Note:</strong> Normalizations are estimates based on deal descriptions and assumptions.
100
+ Actual per-unit costs may vary significantly.
101
+ </p>
102
+ </div>
103
+
104
+ <div className="card overflow-hidden p-0">
105
+ <div className="overflow-x-auto">
106
+ <table className="table">
107
+ <thead>
108
+ <tr>
109
+ <th>Deal</th>
110
+ <th>Raw Price</th>
111
+ <th>Per Token</th>
112
+ <th>Per Record</th>
113
+ <th>Per Image</th>
114
+ <th>Per Minute</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ {deals.map((deal) => {
119
+ const rawPrice = deal.priceUsd
120
+ ? deal.priceUsd >= 1000000000
121
+ ? `$${(deal.priceUsd / 1000000000).toFixed(1)}B`
122
+ : deal.priceUsd >= 1000000
123
+ ? `$${(deal.priceUsd / 1000000).toFixed(0)}M`
124
+ : `$${deal.priceUsd.toFixed(0)}`
125
+ : deal.reportedTerms || 'Undisclosed'
126
+
127
+ return (
128
+ <tr key={deal.id}>
129
+ <td>
130
+ <div>
131
+ <div className="font-medium">{deal.provider}</div>
132
+ <div className="text-sm text-text-muted">→ {deal.buyer}</div>
133
+ <div className="text-xs text-text-muted mt-1">{deal.modality}</div>
134
+ </div>
135
+ </td>
136
+ <td>{rawPrice}</td>
137
+ <td className="text-sm">
138
+ {formatNormalizedPrice(normalizePrice(deal, 'token'), 'token')}
139
+ </td>
140
+ <td className="text-sm">
141
+ {formatNormalizedPrice(normalizePrice(deal, 'record'), 'record')}
142
+ </td>
143
+ <td className="text-sm">
144
+ {formatNormalizedPrice(normalizePrice(deal, 'image'), 'image')}
145
+ </td>
146
+ <td className="text-sm">
147
+ {formatNormalizedPrice(normalizePrice(deal, 'minute'), 'minute')}
148
+ </td>
149
+ </tr>
150
+ )
151
+ })}
152
+ </tbody>
153
+ </table>
154
+ </div>
155
+ </div>
156
+
157
+ <div className="mt-8">
158
+ <Link href="/" className="text-accent hover:text-accent-hover">
159
+ ← Back to Deals
160
+ </Link>
161
+ </div>
162
+ </div>
163
+ </main>
164
+ )
165
+ }
166
+
app/page.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { prisma } from '@/lib/prisma'
2
+ import Link from 'next/link'
3
+ import DealsClient from './deals/DealsClient'
4
+
5
+ async function getDeals() {
6
+ const deals = await prisma.deal.findMany({
7
+ orderBy: { date: 'desc' },
8
+ take: 100,
9
+ select: {
10
+ id: true,
11
+ provider: true,
12
+ buyer: true,
13
+ modality: true,
14
+ dataType: true,
15
+ priceUsd: true,
16
+ priceRangeMinUsd: true,
17
+ priceRangeMaxUsd: true,
18
+ reportedTerms: true,
19
+ exclusive: true,
20
+ creatorsCompensated: true,
21
+ creatorSplitPercentage: true,
22
+ revenueShare: true,
23
+ date: true,
24
+ dealType: true,
25
+ pricingMechanism: true,
26
+ sourcePrimary: true,
27
+ trainingAllowed: true,
28
+ finetuningAllowed: true,
29
+ inferenceAllowed: true,
30
+ redistributionAllowed: true,
31
+ deletionRequired: true,
32
+ notes: true,
33
+ sources: true,
34
+ pricingNormalizations: {
35
+ select: {
36
+ unitType: true,
37
+ normalizedCostPerUnit: true,
38
+ normalizationMethod: true,
39
+ },
40
+ },
41
+ },
42
+ })
43
+ return deals
44
+ }
45
+
46
+ async function getStats() {
47
+ const deals = await prisma.deal.findMany()
48
+
49
+ const totalDeals = deals.length
50
+ const totalSpend = deals.reduce((sum, deal) => sum + (deal.priceUsd || 0), 0)
51
+ const compensatedDeals = deals.filter(d => d.creatorsCompensated === true).length
52
+ const compensatedPercent = totalDeals > 0 ? Math.round((compensatedDeals / totalDeals) * 100) : 0
53
+
54
+ return {
55
+ totalDeals,
56
+ totalSpend,
57
+ compensatedPercent,
58
+ }
59
+ }
60
+
61
+ function formatCurrency(amount: number): string {
62
+ if (amount >= 1000000000) {
63
+ return `$${(amount / 1000000000).toFixed(1)}B+`
64
+ }
65
+ if (amount >= 1000000) {
66
+ return `$${(amount / 1000000).toFixed(0)}M+`
67
+ }
68
+ return `$${amount.toFixed(0)}`
69
+ }
70
+
71
+ export default async function Home() {
72
+ const deals = await getDeals()
73
+ const stats = await getStats()
74
+
75
+ return (
76
+ <main className="min-h-screen bg-background">
77
+ <div className="container-content section-padding">
78
+ {/* Header with stats */}
79
+ <div className="mb-8">
80
+ <div className="flex items-center justify-between mb-4">
81
+ <div>
82
+ <h1 className="text-4xl font-semibold mb-2">AI Training Data Deals</h1>
83
+ <p className="text-text-muted text-lg">
84
+ Global licensing, acquisition, and commissioning deals (2020–2025)
85
+ </p>
86
+ </div>
87
+ <div className="flex gap-2">
88
+ <Link href="/analytics" className="btn-secondary text-sm">
89
+ Analytics
90
+ </Link>
91
+ <Link href="/normalization" className="btn-secondary text-sm">
92
+ Normalization
93
+ </Link>
94
+ </div>
95
+ </div>
96
+
97
+ {/* Quick stats */}
98
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
99
+ <div className="stat-card">
100
+ <div className="stat-value">{stats.totalDeals}</div>
101
+ <div className="stat-label">Total Deals</div>
102
+ </div>
103
+ <div className="stat-card">
104
+ <div className="stat-value">{formatCurrency(stats.totalSpend)}</div>
105
+ <div className="stat-label">Total Reported Spend</div>
106
+ </div>
107
+ <div className="stat-card">
108
+ <div className="stat-value">{stats.compensatedPercent}%</div>
109
+ <div className="stat-label">With Creator Compensation</div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ {/* Deals Explorer */}
115
+ <DealsClient initialDeals={deals} />
116
+ </div>
117
+ </main>
118
+ )
119
+ }
ingestion/.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ venv/
7
+ env/
8
+ ENV/
9
+ .venv
10
+ *.db
11
+ *.log
12
+
ingestion/config.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration for ingestion pipeline
3
+ """
4
+
5
+ import os
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+ # API Keys
11
+ EXA_API_KEY = os.getenv("EXA_API_KEY")
12
+ PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY")
13
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14
+
15
+ # Database
16
+ DATABASE_URL = os.getenv("DATABASE_URL")
17
+
18
+ # Scraping settings
19
+ SCRAPE_TIMEOUT = 30
20
+ SCRAPE_RETRIES = 3
21
+ SCRAPE_DELAY = 1 # seconds between requests
22
+
23
+ # LLM settings
24
+ PERPLEXITY_MODEL = "llama-3.1-sonar-large-128k-online"
25
+ PERPLEXITY_TEMPERATURE = 0.2
26
+
27
+ # Validation
28
+ MIN_CONFIDENCE_SCORE = 0.5 # Minimum confidence to save a deal
29
+
ingestion/db_writer.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database Writer - Write deals to database using Prisma
3
+ """
4
+
5
+ import os
6
+ import json
7
+ from typing import Optional
8
+ from validator import DealData
9
+ from prisma import Prisma
10
+ from prisma.models import Deal, Provider, Buyer, DealBuyer
11
+
12
+
13
+ class DBWriter:
14
+ """Write deals to database"""
15
+
16
+ def __init__(self, database_url: Optional[str] = None):
17
+ self.database_url = database_url or os.getenv("DATABASE_URL")
18
+ if not self.database_url:
19
+ raise ValueError("DATABASE_URL environment variable required")
20
+ self.prisma = Prisma()
21
+
22
+ async def connect(self):
23
+ """Connect to database"""
24
+ await self.prisma.connect()
25
+
26
+ async def disconnect(self):
27
+ """Disconnect from database"""
28
+ await self.prisma.disconnect()
29
+
30
+ async def upsert_provider(self, name: str) -> Provider:
31
+ """Create or get provider"""
32
+ provider = await self.prisma.provider.upsert(
33
+ where={"name": name},
34
+ data={
35
+ "create": {"name": name},
36
+ "update": {},
37
+ },
38
+ )
39
+ return provider
40
+
41
+ async def upsert_buyer(self, name: str) -> Buyer:
42
+ """Create or get buyer"""
43
+ buyer = await self.prisma.buyer.upsert(
44
+ where={"name": name},
45
+ data={
46
+ "create": {"name": name},
47
+ "update": {},
48
+ },
49
+ )
50
+ return buyer
51
+
52
+ async def upsert_deal(self, deal_data: DealData) -> Deal:
53
+ """
54
+ Create or update deal
55
+
56
+ Uses provider + buyer + date as unique identifier
57
+ """
58
+ # Ensure provider and buyer exist
59
+ provider = await self.upsert_provider(deal_data.provider)
60
+
61
+ # Handle multiple buyers (comma-separated)
62
+ buyer_names = [b.strip() for b in deal_data.buyer.split(",")]
63
+
64
+ # Create deal
65
+ deal_dict = {
66
+ "provider": deal_data.provider,
67
+ "buyer": deal_data.buyer,
68
+ "modality": deal_data.modality,
69
+ "dataType": deal_data.data_type,
70
+ "reportedTerms": deal_data.reported_terms,
71
+ "creatorsCompensated": deal_data.creators_compensated,
72
+ "exclusive": deal_data.exclusive,
73
+ "pricingMechanism": deal_data.pricing_mechanism,
74
+ "dealType": deal_data.deal_type,
75
+ "priceUsd": deal_data.price_usd,
76
+ "priceRangeMinUsd": deal_data.price_range_min_usd,
77
+ "priceRangeMaxUsd": deal_data.price_range_max_usd,
78
+ "priceCurrency": deal_data.price_currency,
79
+ "date": deal_data.date,
80
+ "startDate": deal_data.start_date,
81
+ "endDate": deal_data.end_date,
82
+ "durationYears": deal_data.duration_years,
83
+ "creatorSplitPercentage": deal_data.creator_split_percentage,
84
+ "revenueShare": deal_data.revenue_share,
85
+ "trainingAllowed": deal_data.training_allowed,
86
+ "finetuningAllowed": deal_data.finetuning_allowed,
87
+ "inferenceAllowed": deal_data.inference_allowed,
88
+ "redistributionAllowed": deal_data.redistribution_allowed,
89
+ "deletionRequired": deal_data.deletion_required,
90
+ "sources": json.dumps(deal_data.sources),
91
+ "sourcePrimary": deal_data.source_primary,
92
+ "notes": deal_data.notes,
93
+ "dealStage": deal_data.deal_stage,
94
+ "confidenceScore": deal_data.confidence_score,
95
+ "providerId": provider.id,
96
+ }
97
+
98
+ # Try to find existing deal by provider + buyer + date
99
+ existing = await self.prisma.deal.find_first(
100
+ where={
101
+ "provider": deal_data.provider,
102
+ "buyer": {"contains": buyer_names[0]},
103
+ "date": deal_data.date,
104
+ },
105
+ )
106
+
107
+ if existing:
108
+ # Update existing deal
109
+ deal = await self.prisma.deal.update(
110
+ where={"id": existing.id},
111
+ data=deal_dict,
112
+ )
113
+ else:
114
+ # Create new deal
115
+ deal = await self.prisma.deal.create(data=deal_dict)
116
+
117
+ # Link buyers
118
+ for buyer_name in buyer_names:
119
+ buyer = await self.upsert_buyer(buyer_name)
120
+
121
+ # Check if relationship exists
122
+ existing_relation = await self.prisma.dealbuyer.find_first(
123
+ where={
124
+ "dealId": deal.id,
125
+ "buyerId": buyer.id,
126
+ },
127
+ )
128
+
129
+ if not existing_relation:
130
+ await self.prisma.dealbuyer.create(
131
+ data={
132
+ "dealId": deal.id,
133
+ "buyerId": buyer.id,
134
+ },
135
+ )
136
+
137
+ return deal
138
+
139
+ async def save_deals(self, deals: list[DealData]) -> int:
140
+ """Save multiple deals"""
141
+ saved = 0
142
+ for deal in deals:
143
+ try:
144
+ await self.upsert_deal(deal)
145
+ saved += 1
146
+ except Exception as e:
147
+ print(f"Error saving deal {deal.provider} → {deal.buyer}: {e}")
148
+ return saved
149
+
150
+
151
+ # Async context manager for easy usage
152
+ class DBWriterContext:
153
+ """Context manager for DBWriter"""
154
+
155
+ def __init__(self, database_url: Optional[str] = None):
156
+ self.writer = DBWriter(database_url)
157
+
158
+ async def __aenter__(self):
159
+ await self.writer.connect()
160
+ return self.writer
161
+
162
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
163
+ await self.writer.disconnect()
164
+
165
+
166
+ # Example usage:
167
+ # async def main():
168
+ # async with DBWriterContext() as writer:
169
+ # deal_data = DealData(...)
170
+ # await writer.upsert_deal(deal_data)
171
+
ingestion/deal_parser.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Deal Parser - Extract structured deal information from text
3
+ """
4
+
5
+ from typing import Dict, Any, Optional
6
+ from datetime import datetime
7
+ import re
8
+ from perplexity_client import PerplexityClient
9
+ from validator import DealValidator, DealData
10
+
11
+
12
+ class DealParser:
13
+ """Parse deals from text using LLM + regex validation"""
14
+
15
+ def __init__(self, perplexity_client: Optional[PerplexityClient] = None):
16
+ self.perplexity = perplexity_client or PerplexityClient()
17
+ self.validator = DealValidator()
18
+
19
+ def parse(self, text: str, source_url: str) -> Optional[DealData]:
20
+ """
21
+ Parse deal information from text
22
+
23
+ Args:
24
+ text: Article or filing text
25
+ source_url: URL where text came from
26
+
27
+ Returns:
28
+ DealData object if valid deal found, None otherwise
29
+ """
30
+ # Step 1: LLM extraction
31
+ extracted = self.perplexity.extract_deal_info(text)
32
+
33
+ if not extracted:
34
+ return None
35
+
36
+ # Step 2: Regex validation and normalization
37
+ normalized = self._normalize_extracted(extracted)
38
+
39
+ # Step 3: Add metadata
40
+ normalized["sources"] = [source_url]
41
+ normalized["deal_stage"] = "announced" # Default, can be updated
42
+ normalized["confidence_score"] = self._calculate_confidence(normalized)
43
+
44
+ # Step 4: Validate with Pydantic
45
+ try:
46
+ deal_data = DealData(**normalized)
47
+ return deal_data
48
+ except Exception as e:
49
+ print(f"Validation error: {e}")
50
+ return None
51
+
52
+ def _normalize_extracted(self, extracted: Dict[str, Any]) -> Dict[str, Any]:
53
+ """Normalize and clean extracted data"""
54
+ normalized = {}
55
+
56
+ # Provider and buyer
57
+ normalized["provider"] = self._clean_string(extracted.get("provider"))
58
+ normalized["buyer"] = self._clean_string(extracted.get("buyer"))
59
+
60
+ # Modality - normalize to standard values
61
+ modality = self._normalize_modality(extracted.get("modality"))
62
+ normalized["modality"] = modality
63
+
64
+ # Data type
65
+ normalized["data_type"] = self._clean_string(extracted.get("data_type"))
66
+
67
+ # Pricing
68
+ normalized["price_usd"] = self._parse_price(extracted.get("price_usd"))
69
+ normalized["price_range_min_usd"] = self._parse_price(extracted.get("price_range_min_usd"))
70
+ normalized["price_range_max_usd"] = self._parse_price(extracted.get("price_range_max_usd"))
71
+ normalized["reported_terms"] = self._clean_string(extracted.get("reported_terms"))
72
+
73
+ # Pricing mechanism and deal type
74
+ normalized["pricing_mechanism"] = self._clean_string(extracted.get("pricing_mechanism"))
75
+ normalized["deal_type"] = self._normalize_deal_type(extracted.get("deal_type"))
76
+
77
+ # Dates
78
+ normalized["date"] = self._parse_date(extracted.get("start_date"))
79
+ normalized["start_date"] = self._parse_datetime(extracted.get("start_date"))
80
+ normalized["end_date"] = self._parse_datetime(extracted.get("end_date"))
81
+ normalized["duration_years"] = self._parse_duration(extracted.get("duration_years"))
82
+
83
+ # Boolean fields
84
+ normalized["exclusive"] = self._parse_boolean(extracted.get("exclusivity"))
85
+ normalized["creators_compensated"] = self._parse_boolean(extracted.get("creators_compensated"))
86
+ normalized["revenue_share"] = self._parse_boolean(extracted.get("revenue_share"))
87
+
88
+ # Compensation
89
+ normalized["creator_split_percentage"] = self._parse_float(extracted.get("creator_split_percentage"))
90
+
91
+ # Rights
92
+ rights = extracted.get("rights_granted", "")
93
+ normalized["training_allowed"] = self._parse_rights(rights, ["training", "train"])
94
+ normalized["finetuning_allowed"] = self._parse_rights(rights, ["fine-tuning", "finetuning", "fine-tune"])
95
+ normalized["inference_allowed"] = self._parse_rights(rights, ["inference", "infer"])
96
+ normalized["redistribution_allowed"] = self._parse_rights(rights, ["redistribution", "redistribute"])
97
+
98
+ # Source
99
+ normalized["source_primary"] = self._clean_string(extracted.get("source"))
100
+
101
+ return normalized
102
+
103
+ def _clean_string(self, value: Any) -> Optional[str]:
104
+ """Clean and normalize string values"""
105
+ if not value:
106
+ return None
107
+ if isinstance(value, str):
108
+ return value.strip() or None
109
+ return str(value).strip() or None
110
+
111
+ def _normalize_modality(self, modality: Any) -> str:
112
+ """Normalize modality to standard values"""
113
+ if not modality:
114
+ return "Text" # Default
115
+
116
+ modality_lower = str(modality).lower()
117
+
118
+ modality_map = {
119
+ "text": "Text",
120
+ "image": "Image",
121
+ "audio": "Audio",
122
+ "video": "Video",
123
+ "satellite": "Satellite",
124
+ "biotech": "Health / Biotech",
125
+ "health": "Health / Biotech",
126
+ "corporate": "Corporate / data infra",
127
+ "infra": "Corporate / data infra",
128
+ "legal": "Legal / Books",
129
+ "books": "Legal / Books",
130
+ "commissioning": "Commissioning",
131
+ }
132
+
133
+ for key, value in modality_map.items():
134
+ if key in modality_lower:
135
+ return value
136
+
137
+ return "Text" # Default
138
+
139
+ def _normalize_deal_type(self, deal_type: Any) -> Optional[str]:
140
+ """Normalize deal type"""
141
+ if not deal_type:
142
+ return None
143
+
144
+ deal_type_lower = str(deal_type).lower()
145
+
146
+ type_map = {
147
+ "aggregate": "aggregate",
148
+ "per-unit": "per-unit",
149
+ "per unit": "per-unit",
150
+ "commissioning": "commissioning",
151
+ "settlement": "settlement",
152
+ "acquisition": "acquisition",
153
+ "commons": "commons",
154
+ "open": "commons",
155
+ }
156
+
157
+ for key, value in type_map.items():
158
+ if key in deal_type_lower:
159
+ return value
160
+
161
+ return None
162
+
163
+ def _parse_price(self, value: Any) -> Optional[float]:
164
+ """Parse price from various formats"""
165
+ if value is None:
166
+ return None
167
+
168
+ if isinstance(value, (int, float)):
169
+ return float(value)
170
+
171
+ if isinstance(value, str):
172
+ # Remove currency symbols and commas
173
+ cleaned = re.sub(r'[$,€£¥]', '', value.replace(',', ''))
174
+ # Extract number
175
+ match = re.search(r'(\d+\.?\d*)', cleaned)
176
+ if match:
177
+ return float(match.group(1))
178
+
179
+ return None
180
+
181
+ def _parse_date(self, value: Any) -> Optional[str]:
182
+ """Parse date string to YYYY-MM-DD format"""
183
+ if not value:
184
+ return None
185
+
186
+ # Try common date formats
187
+ formats = [
188
+ "%Y-%m-%d",
189
+ "%Y/%m/%d",
190
+ "%m/%d/%Y",
191
+ "%d/%m/%Y",
192
+ "%B %d, %Y",
193
+ "%b %d, %Y",
194
+ ]
195
+
196
+ for fmt in formats:
197
+ try:
198
+ dt = datetime.strptime(str(value), fmt)
199
+ return dt.strftime("%Y-%m-%d")
200
+ except ValueError:
201
+ continue
202
+
203
+ # Try to extract year if full date fails
204
+ year_match = re.search(r'20\d{2}', str(value))
205
+ if year_match:
206
+ return f"{year_match.group()}-01-01"
207
+
208
+ return None
209
+
210
+ def _parse_datetime(self, value: Any) -> Optional[datetime]:
211
+ """Parse datetime"""
212
+ date_str = self._parse_date(value)
213
+ if date_str:
214
+ try:
215
+ return datetime.strptime(date_str, "%Y-%m-%d")
216
+ except ValueError:
217
+ pass
218
+ return None
219
+
220
+ def _parse_duration(self, value: Any) -> Optional[float]:
221
+ """Parse duration in years"""
222
+ if value is None:
223
+ return None
224
+
225
+ if isinstance(value, (int, float)):
226
+ return float(value)
227
+
228
+ if isinstance(value, str):
229
+ # Extract number
230
+ match = re.search(r'(\d+\.?\d*)', str(value))
231
+ if match:
232
+ return float(match.group(1))
233
+
234
+ return None
235
+
236
+ def _parse_boolean(self, value: Any) -> Optional[bool]:
237
+ """Parse boolean from various formats"""
238
+ if value is None:
239
+ return None
240
+
241
+ if isinstance(value, bool):
242
+ return value
243
+
244
+ if isinstance(value, str):
245
+ lower = value.lower()
246
+ if lower in ["true", "yes", "1"]:
247
+ return True
248
+ if lower in ["false", "no", "0"]:
249
+ return False
250
+
251
+ return None
252
+
253
+ def _parse_float(self, value: Any) -> Optional[float]:
254
+ """Parse float"""
255
+ if value is None:
256
+ return None
257
+
258
+ try:
259
+ return float(value)
260
+ except (ValueError, TypeError):
261
+ return None
262
+
263
+ def _parse_rights(self, rights_text: Any, keywords: list) -> Optional[bool]:
264
+ """Parse rights from text"""
265
+ if not rights_text:
266
+ return None
267
+
268
+ text_lower = str(rights_text).lower()
269
+ for keyword in keywords:
270
+ if keyword in text_lower:
271
+ return True
272
+ return None
273
+
274
+ def _calculate_confidence(self, data: Dict[str, Any]) -> float:
275
+ """Calculate confidence score (0-1)"""
276
+ score = 0.0
277
+
278
+ # Required fields
279
+ if data.get("provider"):
280
+ score += 0.2
281
+ if data.get("buyer"):
282
+ score += 0.2
283
+ if data.get("modality"):
284
+ score += 0.1
285
+
286
+ # Price information
287
+ if data.get("price_usd") or data.get("price_range_min_usd"):
288
+ score += 0.2
289
+
290
+ # Dates
291
+ if data.get("date") or data.get("start_date"):
292
+ score += 0.1
293
+
294
+ # Terms
295
+ if data.get("reported_terms"):
296
+ score += 0.1
297
+
298
+ # Rights/compensation info
299
+ if data.get("exclusive") is not None or data.get("creators_compensated") is not None:
300
+ score += 0.1
301
+
302
+ return min(score, 1.0)
303
+
ingestion/exa_client.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Exa API Client - Intelligent URL retrieval for deal discovery
3
+ """
4
+
5
+ import os
6
+ from typing import List, Dict, Optional
7
+ from dataclasses import dataclass
8
+ import requests
9
+ from source_registry import EXA_SEARCH_QUERIES
10
+
11
+
12
+ @dataclass
13
+ class ExaResult:
14
+ """Represents a result from Exa search"""
15
+ url: str
16
+ title: str
17
+ summary: str
18
+ published_date: Optional[str] = None
19
+ author: Optional[str] = None
20
+ score: float = 0.0 # Relevance score
21
+
22
+
23
+ class ExaClient:
24
+ """Client for Exa API"""
25
+
26
+ def __init__(self, api_key: Optional[str] = None):
27
+ self.api_key = api_key or os.getenv("EXA_API_KEY")
28
+ if not self.api_key:
29
+ raise ValueError("EXA_API_KEY environment variable required")
30
+ self.base_url = "https://api.exa.ai"
31
+ self.headers = {
32
+ "x-api-key": self.api_key,
33
+ "Content-Type": "application/json",
34
+ }
35
+
36
+ def search(
37
+ self,
38
+ query: str,
39
+ num_results: int = 10,
40
+ start_published_date: Optional[str] = None,
41
+ end_published_date: Optional[str] = None,
42
+ category: Optional[str] = None,
43
+ ) -> List[ExaResult]:
44
+ """
45
+ Search for URLs using Exa
46
+
47
+ Args:
48
+ query: Search query
49
+ num_results: Number of results to return
50
+ start_published_date: Start date (YYYY-MM-DD)
51
+ end_published_date: End date (YYYY-MM-DD)
52
+ category: Content category filter
53
+
54
+ Returns:
55
+ List of ExaResult objects
56
+ """
57
+ payload = {
58
+ "query": query,
59
+ "num_results": num_results,
60
+ "use_autoprompt": True, # Let Exa optimize the query
61
+ }
62
+
63
+ if start_published_date:
64
+ payload["start_published_date"] = start_published_date
65
+ if end_published_date:
66
+ payload["end_published_date"] = end_published_date
67
+ if category:
68
+ payload["category"] = category
69
+
70
+ try:
71
+ response = requests.post(
72
+ f"{self.base_url}/search",
73
+ json=payload,
74
+ headers=self.headers,
75
+ timeout=30,
76
+ )
77
+ response.raise_for_status()
78
+ data = response.json()
79
+
80
+ results = []
81
+ for item in data.get("results", []):
82
+ results.append(ExaResult(
83
+ url=item.get("url", ""),
84
+ title=item.get("title", ""),
85
+ summary=item.get("text", ""), # Exa provides summary text
86
+ published_date=item.get("published_date"),
87
+ author=item.get("author"),
88
+ score=item.get("score", 0.0),
89
+ ))
90
+
91
+ return results
92
+
93
+ except requests.exceptions.RequestException as e:
94
+ print(f"Exa API error for query '{query}': {e}")
95
+ return []
96
+
97
+ def get_contents(self, urls: List[str]) -> Dict[str, str]:
98
+ """
99
+ Get full content for URLs (bypasses paywalls via Exa)
100
+
101
+ Args:
102
+ urls: List of URLs to fetch
103
+
104
+ Returns:
105
+ Dict mapping URL to content text
106
+ """
107
+ payload = {
108
+ "urls": urls,
109
+ "text": {"max_characters": 10000}, # Limit content size
110
+ }
111
+
112
+ try:
113
+ response = requests.post(
114
+ f"{self.base_url}/contents",
115
+ json=payload,
116
+ headers=self.headers,
117
+ timeout=60,
118
+ )
119
+ response.raise_for_status()
120
+ data = response.json()
121
+
122
+ contents = {}
123
+ for item in data.get("results", []):
124
+ url = item.get("url", "")
125
+ text = item.get("text", "")
126
+ contents[url] = text
127
+
128
+ return contents
129
+
130
+ except requests.exceptions.RequestException as e:
131
+ print(f"Exa contents API error: {e}")
132
+ return {}
133
+
134
+ def search_deal_candidates(
135
+ self,
136
+ days_back: int = 7,
137
+ num_results_per_query: int = 5,
138
+ ) -> List[ExaResult]:
139
+ """
140
+ Search for deal candidates using all predefined queries
141
+
142
+ Args:
143
+ days_back: How many days back to search
144
+ num_results_per_query: Results per query
145
+
146
+ Returns:
147
+ Deduplicated list of results
148
+ """
149
+ from datetime import datetime, timedelta
150
+
151
+ end_date = datetime.now()
152
+ start_date = end_date - timedelta(days=days_back)
153
+
154
+ start_str = start_date.strftime("%Y-%m-%d")
155
+ end_str = end_date.strftime("%Y-%m-%d")
156
+
157
+ all_results = []
158
+ seen_urls = set()
159
+
160
+ for query in EXA_SEARCH_QUERIES:
161
+ print(f"Searching Exa for: {query}")
162
+ results = self.search(
163
+ query=query,
164
+ num_results=num_results_per_query,
165
+ start_published_date=start_str,
166
+ end_published_date=end_str,
167
+ )
168
+
169
+ for result in results:
170
+ if result.url not in seen_urls:
171
+ seen_urls.add(result.url)
172
+ all_results.append(result)
173
+
174
+ # Sort by score (relevance)
175
+ all_results.sort(key=lambda x: x.score, reverse=True)
176
+
177
+ return all_results
178
+
179
+
180
+ if __name__ == "__main__":
181
+ # Example usage
182
+ client = ExaClient()
183
+ results = client.search_deal_candidates(days_back=30)
184
+ print(f"Found {len(results)} unique URLs")
185
+ for result in results[:5]:
186
+ print(f"\n{result.title}")
187
+ print(f" URL: {result.url}")
188
+ print(f" Score: {result.score:.2f}")
189
+ print(f" Summary: {result.summary[:200]}...")
190
+
ingestion/main.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main Ingestion Pipeline - Orchestrates the full scraping and ingestion process
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ from datetime import datetime
8
+ from typing import List
9
+ from dotenv import load_dotenv
10
+
11
+ # Load environment variables
12
+ load_dotenv()
13
+
14
+ # Import clients and modules
15
+ try:
16
+ from exa_client import ExaClient, ExaResult
17
+ from perplexity_client import PerplexityClient
18
+ from deal_parser import DealParser
19
+ from validator import DealValidator, DealData
20
+ from source_registry import get_all_sources, EXA_SEARCH_QUERIES
21
+ except ImportError as e:
22
+ print(f"Import error: {e}")
23
+ print("Make sure all dependencies are installed: pip install -r requirements.txt")
24
+ sys.exit(1)
25
+
26
+
27
+ class IngestionPipeline:
28
+ """Main pipeline orchestrator"""
29
+
30
+ def __init__(self):
31
+ self.exa = ExaClient() if os.getenv("EXA_API_KEY") else None
32
+ self.perplexity = PerplexityClient() if os.getenv("PERPLEXITY_API_KEY") else None
33
+ self.parser = DealParser(self.perplexity) if self.perplexity else None
34
+ self.validator = DealValidator()
35
+
36
+ def run_exa_search(self, days_back: int = 7) -> List[ExaResult]:
37
+ """Run Exa search for deal candidates"""
38
+ if not self.exa:
39
+ print("Exa API key not configured, skipping Exa search")
40
+ return []
41
+
42
+ print(f"🔍 Searching Exa for deals from last {days_back} days...")
43
+ results = self.exa.search_deal_candidates(
44
+ days_back=days_back,
45
+ num_results_per_query=5,
46
+ )
47
+ print(f"✅ Found {len(results)} unique URLs from Exa")
48
+ return results
49
+
50
+ def process_urls(self, urls: List[str]) -> List[DealData]:
51
+ """Process URLs and extract deals"""
52
+ if not self.parser:
53
+ print("Perplexity API key not configured, cannot parse deals")
54
+ return []
55
+
56
+ deals = []
57
+
58
+ for url in urls:
59
+ print(f"\n📄 Processing: {url}")
60
+
61
+ # Get content (simplified - in production, use html_scraper.py)
62
+ try:
63
+ import requests
64
+ response = requests.get(url, timeout=30, headers={
65
+ "User-Agent": "Mozilla/5.0 (compatible; DealTracker/1.0)"
66
+ })
67
+ text = response.text
68
+
69
+ # Extract deal
70
+ deal = self.parser.parse(text, url)
71
+
72
+ if deal:
73
+ print(f" ✅ Extracted deal: {deal.provider} → {deal.buyer}")
74
+ deals.append(deal)
75
+ else:
76
+ print(f" ⚠️ No deal found in this URL")
77
+
78
+ except Exception as e:
79
+ print(f" ❌ Error processing URL: {e}")
80
+ continue
81
+
82
+ return deals
83
+
84
+ def save_deals(self, deals: List[DealData]):
85
+ """Save deals to database"""
86
+ # This would connect to your database
87
+ # For now, just print
88
+ print(f"\n💾 Saving {len(deals)} deals to database...")
89
+
90
+ # TODO: Implement database writer
91
+ # from db_writer import DBWriter
92
+ # writer = DBWriter()
93
+ # for deal in deals:
94
+ # writer.upsert_deal(deal)
95
+
96
+ print("✅ Deals saved (database writer not yet implemented)")
97
+
98
+
99
+ def main():
100
+ """Main entry point"""
101
+ print("=" * 60)
102
+ print("AI Training Data Deals - Ingestion Pipeline")
103
+ print("=" * 60)
104
+ print(f"Started at: {datetime.now().isoformat()}\n")
105
+
106
+ pipeline = IngestionPipeline()
107
+
108
+ # Step 1: Search for deal candidates
109
+ exa_results = pipeline.run_exa_search(days_back=7)
110
+
111
+ if not exa_results:
112
+ print("\n⚠️ No URLs found. Exiting.")
113
+ return
114
+
115
+ # Step 2: Process URLs
116
+ urls = [r.url for r in exa_results]
117
+ deals = pipeline.process_urls(urls)
118
+
119
+ # Step 3: Save to database
120
+ if deals:
121
+ pipeline.save_deals(deals)
122
+ print(f"\n✅ Pipeline complete: {len(deals)} deals extracted")
123
+ else:
124
+ print("\n⚠️ No deals extracted")
125
+
126
+ print(f"\nFinished at: {datetime.now().isoformat()}")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
131
+
ingestion/perplexity_client.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Perplexity API Client - For content summarization and structured extraction
3
+ """
4
+
5
+ import os
6
+ from typing import Dict, Optional, Any
7
+ import requests
8
+ import json
9
+
10
+
11
+ class PerplexityClient:
12
+ """Client for Perplexity API"""
13
+
14
+ def __init__(self, api_key: Optional[str] = None):
15
+ self.api_key = api_key or os.getenv("PERPLEXITY_API_KEY")
16
+ if not self.api_key:
17
+ raise ValueError("PERPLEXITY_API_KEY environment variable required")
18
+ self.base_url = "https://api.perplexity.ai"
19
+ self.headers = {
20
+ "Authorization": f"Bearer {self.api_key}",
21
+ "Content-Type": "application/json",
22
+ }
23
+
24
+ def chat(
25
+ self,
26
+ messages: list,
27
+ model: str = "llama-3.1-sonar-large-128k-online",
28
+ temperature: float = 0.2,
29
+ ) -> Dict[str, Any]:
30
+ """
31
+ Send chat request to Perplexity
32
+
33
+ Args:
34
+ messages: List of message dicts (role, content)
35
+ model: Model to use
36
+ temperature: Sampling temperature
37
+
38
+ Returns:
39
+ Response dict with content and citations
40
+ """
41
+ payload = {
42
+ "model": model,
43
+ "messages": messages,
44
+ "temperature": temperature,
45
+ }
46
+
47
+ try:
48
+ response = requests.post(
49
+ f"{self.base_url}/chat/completions",
50
+ json=payload,
51
+ headers=self.headers,
52
+ timeout=60,
53
+ )
54
+ response.raise_for_status()
55
+ return response.json()
56
+
57
+ except requests.exceptions.RequestException as e:
58
+ print(f"Perplexity API error: {e}")
59
+ return {}
60
+
61
+ def extract_deal_info(self, text: str) -> Dict[str, Any]:
62
+ """
63
+ Extract structured deal information from text
64
+
65
+ Args:
66
+ text: Article or filing text
67
+
68
+ Returns:
69
+ Dict with extracted fields
70
+ """
71
+ prompt = """Please extract structured information about an AI training data deal from the following text.
72
+
73
+ Extract:
74
+ - provider (company/organization providing data)
75
+ - buyer (company/organization buying/licensing data)
76
+ - modality (text, image, audio, video, satellite, biotech, etc.)
77
+ - data_type (brief description of the data)
78
+ - pricing_mechanism (e.g., "Access / aggregate licensing", "Per-unit licensing", etc.)
79
+ - price_usd (numeric value if mentioned, or null)
80
+ - price_range_min_usd (if range given)
81
+ - price_range_max_usd (if range given)
82
+ - reported_terms (headline terms as mentioned)
83
+ - exclusivity (true/false/null if mentioned)
84
+ - creators_compensated (true/false/null if mentioned)
85
+ - creator_split_percentage (if mentioned)
86
+ - revenue_share (true/false if mentioned)
87
+ - start_date (if mentioned)
88
+ - end_date (if mentioned)
89
+ - duration_years (if mentioned)
90
+ - rights_granted (training, fine-tuning, inference, etc.)
91
+ - deal_type (aggregate, per-unit, commissioning, settlement, etc.)
92
+
93
+ Return ONLY valid JSON, no markdown formatting. If a field cannot be determined, use null."""
94
+
95
+ messages = [
96
+ {
97
+ "role": "system",
98
+ "content": "You are a data extraction assistant. Extract structured information from text and return only valid JSON.",
99
+ },
100
+ {
101
+ "role": "user",
102
+ "content": f"{prompt}\n\nText:\n{text[:8000]}", # Limit text size
103
+ },
104
+ ]
105
+
106
+ response = self.chat(messages, temperature=0.1)
107
+
108
+ try:
109
+ content = response.get("choices", [{}])[0].get("message", {}).get("content", "{}")
110
+ # Remove markdown code blocks if present
111
+ content = content.strip()
112
+ if content.startswith("```json"):
113
+ content = content[7:]
114
+ if content.startswith("```"):
115
+ content = content[3:]
116
+ if content.endswith("```"):
117
+ content = content[:-3]
118
+ content = content.strip()
119
+
120
+ extracted = json.loads(content)
121
+ return extracted
122
+
123
+ except (json.JSONDecodeError, KeyError, IndexError) as e:
124
+ print(f"Error parsing Perplexity response: {e}")
125
+ print(f"Response: {response}")
126
+ return {}
127
+
128
+ def summarize_article(self, text: str) -> str:
129
+ """
130
+ Generate a summary of an article
131
+
132
+ Args:
133
+ text: Article text
134
+
135
+ Returns:
136
+ Summary string
137
+ """
138
+ messages = [
139
+ {
140
+ "role": "system",
141
+ "content": "You are a summarization assistant. Provide concise summaries.",
142
+ },
143
+ {
144
+ "role": "user",
145
+ "content": f"Summarize the following article in 2-3 sentences:\n\n{text[:6000]}",
146
+ },
147
+ ]
148
+
149
+ response = self.chat(messages)
150
+
151
+ try:
152
+ return response.get("choices", [{}])[0].get("message", {}).get("content", "")
153
+ except (KeyError, IndexError):
154
+ return ""
155
+
156
+
157
+ if __name__ == "__main__":
158
+ # Example usage
159
+ client = PerplexityClient()
160
+
161
+ sample_text = """
162
+ OpenAI announced a multi-year licensing agreement with News Corp worth over $250 million.
163
+ The deal grants OpenAI access to content from The Wall Street Journal, The Times, and NY Post
164
+ for training AI models. The agreement is exclusive and spans 5 years.
165
+ """
166
+
167
+ extracted = client.extract_deal_info(sample_text)
168
+ print("Extracted deal info:")
169
+ print(json.dumps(extracted, indent=2))
170
+
ingestion/requirements.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python Ingestion Pipeline Dependencies
2
+
3
+ # Core
4
+ requests>=2.31.0
5
+ beautifulsoup4>=4.12.0
6
+ newspaper3k>=0.2.8
7
+ python-dotenv>=1.0.0
8
+
9
+ # Database
10
+ psycopg2-binary>=2.9.9
11
+ sqlalchemy>=2.0.23
12
+ prisma>=0.11.0
13
+
14
+ # LLM APIs
15
+ openai>=1.12.0
16
+ anthropic>=0.18.0
17
+
18
+ # Validation
19
+ pydantic>=2.6.0
20
+
21
+ # Utilities
22
+ python-dateutil>=2.8.2
23
+ tqdm>=4.66.0
24
+
25
+ # Exa API (when available)
26
+ # exa-py>=1.0.0
27
+
28
+ # Perplexity API (when available)
29
+ # perplexity>=1.0.0
30
+
ingestion/source_registry.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Source Registry - Central registry of data sources to scrape
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional, List
7
+ from enum import Enum
8
+
9
+
10
+ class SourceType(Enum):
11
+ NEWS = "news"
12
+ FILING = "filing"
13
+ PRESS_RELEASE = "press_release"
14
+ RSS = "rss"
15
+ TRACKER = "tracker"
16
+
17
+
18
+ @dataclass
19
+ class Source:
20
+ """Represents a data source configuration"""
21
+ name: str
22
+ base_url: str
23
+ source_type: SourceType
24
+ rss_feed: Optional[str] = None
25
+ css_selectors: Optional[dict] = None # e.g., {"title": ".article-title", "content": ".article-body"}
26
+ api_endpoint: Optional[str] = None
27
+ headers: Optional[dict] = None
28
+ enabled: bool = True
29
+ priority: int = 5 # 1-10, higher = more important
30
+
31
+
32
+ # News Outlets
33
+ NEWS_SOURCES = [
34
+ Source(
35
+ name="Reuters",
36
+ base_url="https://www.reuters.com",
37
+ source_type=SourceType.NEWS,
38
+ rss_feed="https://www.reuters.com/tools/rss",
39
+ priority=9,
40
+ ),
41
+ Source(
42
+ name="Bloomberg",
43
+ base_url="https://www.bloomberg.com",
44
+ source_type=SourceType.NEWS,
45
+ priority=9,
46
+ ),
47
+ Source(
48
+ name="Financial Times",
49
+ base_url="https://www.ft.com",
50
+ source_type=SourceType.NEWS,
51
+ priority=8,
52
+ ),
53
+ Source(
54
+ name="Axios",
55
+ base_url="https://www.axios.com",
56
+ source_type=SourceType.NEWS,
57
+ rss_feed="https://api.axios.com/feed/",
58
+ priority=7,
59
+ ),
60
+ Source(
61
+ name="The Verge",
62
+ base_url="https://www.theverge.com",
63
+ source_type=SourceType.NEWS,
64
+ rss_feed="https://www.theverge.com/rss/index.xml",
65
+ priority=7,
66
+ ),
67
+ Source(
68
+ name="WIRED",
69
+ base_url="https://www.wired.com",
70
+ source_type=SourceType.NEWS,
71
+ rss_feed="https://www.wired.com/feed/rss",
72
+ priority=7,
73
+ ),
74
+ Source(
75
+ name="TechCrunch",
76
+ base_url="https://techcrunch.com",
77
+ source_type=SourceType.NEWS,
78
+ rss_feed="https://techcrunch.com/feed/",
79
+ priority=8,
80
+ ),
81
+ Source(
82
+ name="Music Business Worldwide",
83
+ base_url="https://www.musicbusinessworldwide.com",
84
+ source_type=SourceType.NEWS,
85
+ priority=6,
86
+ ),
87
+ Source(
88
+ name="PetaPixel",
89
+ base_url="https://petapixel.com",
90
+ source_type=SourceType.NEWS,
91
+ rss_feed="https://petapixel.com/feed/",
92
+ priority=5,
93
+ ),
94
+ ]
95
+
96
+ # Press Release Sources
97
+ PRESS_RELEASE_SOURCES = [
98
+ Source(
99
+ name="OpenAI Blog",
100
+ base_url="https://openai.com/blog",
101
+ source_type=SourceType.PRESS_RELEASE,
102
+ rss_feed="https://openai.com/blog/rss.xml",
103
+ priority=9,
104
+ ),
105
+ Source(
106
+ name="Google AI Blog",
107
+ base_url="https://ai.googleblog.com",
108
+ source_type=SourceType.PRESS_RELEASE,
109
+ priority=8,
110
+ ),
111
+ Source(
112
+ name="Meta AI",
113
+ base_url="https://ai.meta.com/blog",
114
+ source_type=SourceType.PRESS_RELEASE,
115
+ priority=8,
116
+ ),
117
+ Source(
118
+ name="Anthropic",
119
+ base_url="https://www.anthropic.com/news",
120
+ source_type=SourceType.PRESS_RELEASE,
121
+ priority=8,
122
+ ),
123
+ Source(
124
+ name="Microsoft AI",
125
+ base_url="https://www.microsoft.com/en-us/research/blog",
126
+ source_type=SourceType.PRESS_RELEASE,
127
+ priority=7,
128
+ ),
129
+ Source(
130
+ name="HarperCollins",
131
+ base_url="https://www.harpercollins.com/news",
132
+ source_type=SourceType.PRESS_RELEASE,
133
+ priority=6,
134
+ ),
135
+ Source(
136
+ name="Wiley",
137
+ base_url="https://www.wiley.com/en-us/news",
138
+ source_type=SourceType.PRESS_RELEASE,
139
+ priority=6,
140
+ ),
141
+ Source(
142
+ name="News Corp",
143
+ base_url="https://newscorp.com/news",
144
+ source_type=SourceType.PRESS_RELEASE,
145
+ priority=7,
146
+ ),
147
+ ]
148
+
149
+ # Filing Sources
150
+ FILING_SOURCES = [
151
+ Source(
152
+ name="SEC EDGAR",
153
+ base_url="https://www.sec.gov/edgar",
154
+ source_type=SourceType.FILING,
155
+ api_endpoint="https://data.sec.gov/api/xbrl",
156
+ priority=9,
157
+ ),
158
+ Source(
159
+ name="SEDAR",
160
+ base_url="https://www.sedar.com",
161
+ source_type=SourceType.FILING,
162
+ priority=7,
163
+ ),
164
+ ]
165
+
166
+ # Industry Trackers
167
+ TRACKER_SOURCES = [
168
+ Source(
169
+ name="CB Insights",
170
+ base_url="https://www.cbinsights.com",
171
+ source_type=SourceType.TRACKER,
172
+ priority=8,
173
+ ),
174
+ Source(
175
+ name="Appen",
176
+ base_url="https://appen.com",
177
+ source_type=SourceType.TRACKER,
178
+ priority=6,
179
+ ),
180
+ ]
181
+
182
+ # Exa Search Queries (for intelligent retrieval)
183
+ EXA_SEARCH_QUERIES = [
184
+ "OpenAI licensing",
185
+ "Anthropic licensing",
186
+ "training data deal",
187
+ "content licensing AI",
188
+ "publisher AI model agreement",
189
+ "data partnership",
190
+ "AI dataset licensing",
191
+ "media archive deal OpenAI",
192
+ "exclusive AI rights",
193
+ "API access licensing",
194
+ "settlement AI training",
195
+ "Microsoft AI data licensing",
196
+ "Google AI content deal",
197
+ "Meta AI training data",
198
+ "music AI licensing",
199
+ "image AI training data",
200
+ "video AI dataset",
201
+ "satellite AI data",
202
+ "biotech AI training",
203
+ ]
204
+
205
+
206
+ def get_all_sources() -> List[Source]:
207
+ """Get all enabled sources"""
208
+ all_sources = (
209
+ NEWS_SOURCES +
210
+ PRESS_RELEASE_SOURCES +
211
+ FILING_SOURCES +
212
+ TRACKER_SOURCES
213
+ )
214
+ return [s for s in all_sources if s.enabled]
215
+
216
+
217
+ def get_sources_by_type(source_type: SourceType) -> List[Source]:
218
+ """Get sources filtered by type"""
219
+ return [s for s in get_all_sources() if s.source_type == source_type]
220
+
221
+
222
+ def get_high_priority_sources(threshold: int = 7) -> List[Source]:
223
+ """Get sources with priority >= threshold"""
224
+ return [s for s in get_all_sources() if s.priority >= threshold]
225
+
ingestion/validator.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Deal Validator - Pydantic models for validation
3
+ """
4
+
5
+ from pydantic import BaseModel, Field, validator
6
+ from typing import Optional, List
7
+ from datetime import datetime
8
+ from enum import Enum
9
+
10
+
11
+ class Modality(str, Enum):
12
+ TEXT = "Text"
13
+ IMAGE = "Image"
14
+ AUDIO = "Audio"
15
+ VIDEO = "Video"
16
+ SATELLITE = "Satellite"
17
+ HEALTH_BIOTECH = "Health / Biotech"
18
+ CORPORATE_INFRA = "Corporate / data infra"
19
+ LEGAL_BOOKS = "Legal / Books"
20
+ COMMISSIONING = "Commissioning"
21
+ MIXED = "Mixed"
22
+
23
+
24
+ class DealType(str, Enum):
25
+ AGGREGATE = "aggregate"
26
+ PER_UNIT = "per-unit"
27
+ COMMISSIONING = "commissioning"
28
+ SETTLEMENT = "settlement"
29
+ ACQUISITION = "acquisition"
30
+ COMMONS = "commons"
31
+ IMPLICIT = "implicit"
32
+ HYBRID = "hybrid"
33
+
34
+
35
+ class DealStage(str, Enum):
36
+ ANNOUNCED = "announced"
37
+ RUMORED = "rumored"
38
+ CONFIRMED = "confirmed"
39
+ SETTLED = "settled"
40
+
41
+
42
+ class DealData(BaseModel):
43
+ """Validated deal data structure"""
44
+
45
+ # Core fields (required)
46
+ provider: str = Field(..., min_length=1)
47
+ buyer: str = Field(..., min_length=1)
48
+ modality: str = Field(default="Text")
49
+ data_type: Optional[str] = None
50
+
51
+ # Pricing
52
+ price_usd: Optional[float] = Field(None, ge=0)
53
+ price_range_min_usd: Optional[float] = Field(None, ge=0)
54
+ price_range_max_usd: Optional[float] = Field(None, ge=0)
55
+ price_currency: str = Field(default="USD")
56
+ reported_terms: Optional[str] = None
57
+ pricing_mechanism: Optional[str] = None
58
+ deal_type: Optional[str] = None
59
+
60
+ # Dates
61
+ date: Optional[str] = None # YYYY-MM-DD or YYYY or YYYY-MM
62
+ start_date: Optional[datetime] = None
63
+ end_date: Optional[datetime] = None
64
+ duration_years: Optional[float] = Field(None, ge=0)
65
+
66
+ # Rights
67
+ exclusive: Optional[bool] = None
68
+ creators_compensated: Optional[bool] = None
69
+ creator_split_percentage: Optional[float] = Field(None, ge=0, le=100)
70
+ revenue_share: Optional[bool] = None
71
+
72
+ # Rights granted
73
+ training_allowed: Optional[bool] = None
74
+ finetuning_allowed: Optional[bool] = None
75
+ inference_allowed: Optional[bool] = None
76
+ redistribution_allowed: Optional[bool] = None
77
+ deletion_required: Optional[bool] = None
78
+
79
+ # Provenance
80
+ sources: List[str] = Field(default_factory=list)
81
+ source_primary: Optional[str] = None
82
+ notes: Optional[str] = None
83
+ deal_stage: str = Field(default="announced")
84
+ confidence_score: float = Field(default=0.5, ge=0, le=1)
85
+
86
+ @validator("modality")
87
+ def validate_modality(cls, v):
88
+ """Normalize modality"""
89
+ if v in [m.value for m in Modality]:
90
+ return v
91
+ return "Text" # Default
92
+
93
+ @validator("deal_type")
94
+ def validate_deal_type(cls, v):
95
+ """Normalize deal type"""
96
+ if not v:
97
+ return None
98
+ if v in [d.value for d in DealType]:
99
+ return v
100
+ return None
101
+
102
+ @validator("deal_stage")
103
+ def validate_deal_stage(cls, v):
104
+ """Normalize deal stage"""
105
+ if v in [s.value for s in DealStage]:
106
+ return v
107
+ return "announced"
108
+
109
+ @validator("price_range_max_usd")
110
+ def validate_price_range(cls, v, values):
111
+ """Ensure max >= min"""
112
+ if v and values.get("price_range_min_usd"):
113
+ if v < values["price_range_min_usd"]:
114
+ raise ValueError("price_range_max_usd must be >= price_range_min_usd")
115
+ return v
116
+
117
+ @validator("end_date")
118
+ def validate_end_date(cls, v, values):
119
+ """Ensure end_date >= start_date"""
120
+ if v and values.get("start_date"):
121
+ if v < values["start_date"]:
122
+ raise ValueError("end_date must be >= start_date")
123
+ return v
124
+
125
+ class Config:
126
+ use_enum_values = True
127
+
128
+
129
+ class DealValidator:
130
+ """Validator for deal data"""
131
+
132
+ @staticmethod
133
+ def validate(deal_dict: dict) -> tuple[bool, Optional[DealData], Optional[str]]:
134
+ """
135
+ Validate deal data
136
+
137
+ Returns:
138
+ (is_valid, DealData object, error_message)
139
+ """
140
+ try:
141
+ deal_data = DealData(**deal_dict)
142
+ return True, deal_data, None
143
+ except Exception as e:
144
+ return False, None, str(e)
145
+
146
+ @staticmethod
147
+ def is_valid_deal(deal_dict: dict) -> bool:
148
+ """Check if deal is valid"""
149
+ is_valid, _, _ = DealValidator.validate(deal_dict)
150
+ return is_valid
151
+
lib/prisma.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const globalForPrisma = globalThis as unknown as {
4
+ prisma: PrismaClient | undefined
5
+ }
6
+
7
+ export const prisma =
8
+ globalForPrisma.prisma ??
9
+ new PrismaClient({
10
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
11
+ })
12
+
13
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
14
+
next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
next.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ }
5
+
6
+ module.exports = nextConfig
7
+
package-lock.json ADDED
@@ -0,0 +1,2246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "odl-training-deals",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "odl-training-deals",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "@prisma/client": "^5.19.0",
12
+ "next": "14.2.5",
13
+ "react": "^18.3.1",
14
+ "react-dom": "^18.3.1"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.14.12",
18
+ "@types/react": "^18.3.3",
19
+ "@types/react-dom": "^18.3.0",
20
+ "autoprefixer": "^10.4.20",
21
+ "postcss": "^8.4.41",
22
+ "prisma": "^5.19.0",
23
+ "tailwindcss": "^3.4.7",
24
+ "tsx": "^4.16.2",
25
+ "typescript": "^5.5.4"
26
+ }
27
+ },
28
+ "node_modules/@alloc/quick-lru": {
29
+ "version": "5.2.0",
30
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
31
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
32
+ "dev": true,
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=10"
36
+ },
37
+ "funding": {
38
+ "url": "https://github.com/sponsors/sindresorhus"
39
+ }
40
+ },
41
+ "node_modules/@esbuild/aix-ppc64": {
42
+ "version": "0.25.12",
43
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
44
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
45
+ "cpu": [
46
+ "ppc64"
47
+ ],
48
+ "dev": true,
49
+ "license": "MIT",
50
+ "optional": true,
51
+ "os": [
52
+ "aix"
53
+ ],
54
+ "engines": {
55
+ "node": ">=18"
56
+ }
57
+ },
58
+ "node_modules/@esbuild/android-arm": {
59
+ "version": "0.25.12",
60
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
61
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
62
+ "cpu": [
63
+ "arm"
64
+ ],
65
+ "dev": true,
66
+ "license": "MIT",
67
+ "optional": true,
68
+ "os": [
69
+ "android"
70
+ ],
71
+ "engines": {
72
+ "node": ">=18"
73
+ }
74
+ },
75
+ "node_modules/@esbuild/android-arm64": {
76
+ "version": "0.25.12",
77
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
78
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
79
+ "cpu": [
80
+ "arm64"
81
+ ],
82
+ "dev": true,
83
+ "license": "MIT",
84
+ "optional": true,
85
+ "os": [
86
+ "android"
87
+ ],
88
+ "engines": {
89
+ "node": ">=18"
90
+ }
91
+ },
92
+ "node_modules/@esbuild/android-x64": {
93
+ "version": "0.25.12",
94
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
95
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
96
+ "cpu": [
97
+ "x64"
98
+ ],
99
+ "dev": true,
100
+ "license": "MIT",
101
+ "optional": true,
102
+ "os": [
103
+ "android"
104
+ ],
105
+ "engines": {
106
+ "node": ">=18"
107
+ }
108
+ },
109
+ "node_modules/@esbuild/darwin-arm64": {
110
+ "version": "0.25.12",
111
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
112
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
113
+ "cpu": [
114
+ "arm64"
115
+ ],
116
+ "dev": true,
117
+ "license": "MIT",
118
+ "optional": true,
119
+ "os": [
120
+ "darwin"
121
+ ],
122
+ "engines": {
123
+ "node": ">=18"
124
+ }
125
+ },
126
+ "node_modules/@esbuild/darwin-x64": {
127
+ "version": "0.25.12",
128
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
129
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
130
+ "cpu": [
131
+ "x64"
132
+ ],
133
+ "dev": true,
134
+ "license": "MIT",
135
+ "optional": true,
136
+ "os": [
137
+ "darwin"
138
+ ],
139
+ "engines": {
140
+ "node": ">=18"
141
+ }
142
+ },
143
+ "node_modules/@esbuild/freebsd-arm64": {
144
+ "version": "0.25.12",
145
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
146
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
147
+ "cpu": [
148
+ "arm64"
149
+ ],
150
+ "dev": true,
151
+ "license": "MIT",
152
+ "optional": true,
153
+ "os": [
154
+ "freebsd"
155
+ ],
156
+ "engines": {
157
+ "node": ">=18"
158
+ }
159
+ },
160
+ "node_modules/@esbuild/freebsd-x64": {
161
+ "version": "0.25.12",
162
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
163
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
164
+ "cpu": [
165
+ "x64"
166
+ ],
167
+ "dev": true,
168
+ "license": "MIT",
169
+ "optional": true,
170
+ "os": [
171
+ "freebsd"
172
+ ],
173
+ "engines": {
174
+ "node": ">=18"
175
+ }
176
+ },
177
+ "node_modules/@esbuild/linux-arm": {
178
+ "version": "0.25.12",
179
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
180
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
181
+ "cpu": [
182
+ "arm"
183
+ ],
184
+ "dev": true,
185
+ "license": "MIT",
186
+ "optional": true,
187
+ "os": [
188
+ "linux"
189
+ ],
190
+ "engines": {
191
+ "node": ">=18"
192
+ }
193
+ },
194
+ "node_modules/@esbuild/linux-arm64": {
195
+ "version": "0.25.12",
196
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
197
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
198
+ "cpu": [
199
+ "arm64"
200
+ ],
201
+ "dev": true,
202
+ "license": "MIT",
203
+ "optional": true,
204
+ "os": [
205
+ "linux"
206
+ ],
207
+ "engines": {
208
+ "node": ">=18"
209
+ }
210
+ },
211
+ "node_modules/@esbuild/linux-ia32": {
212
+ "version": "0.25.12",
213
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
214
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
215
+ "cpu": [
216
+ "ia32"
217
+ ],
218
+ "dev": true,
219
+ "license": "MIT",
220
+ "optional": true,
221
+ "os": [
222
+ "linux"
223
+ ],
224
+ "engines": {
225
+ "node": ">=18"
226
+ }
227
+ },
228
+ "node_modules/@esbuild/linux-loong64": {
229
+ "version": "0.25.12",
230
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
231
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
232
+ "cpu": [
233
+ "loong64"
234
+ ],
235
+ "dev": true,
236
+ "license": "MIT",
237
+ "optional": true,
238
+ "os": [
239
+ "linux"
240
+ ],
241
+ "engines": {
242
+ "node": ">=18"
243
+ }
244
+ },
245
+ "node_modules/@esbuild/linux-mips64el": {
246
+ "version": "0.25.12",
247
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
248
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
249
+ "cpu": [
250
+ "mips64el"
251
+ ],
252
+ "dev": true,
253
+ "license": "MIT",
254
+ "optional": true,
255
+ "os": [
256
+ "linux"
257
+ ],
258
+ "engines": {
259
+ "node": ">=18"
260
+ }
261
+ },
262
+ "node_modules/@esbuild/linux-ppc64": {
263
+ "version": "0.25.12",
264
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
265
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
266
+ "cpu": [
267
+ "ppc64"
268
+ ],
269
+ "dev": true,
270
+ "license": "MIT",
271
+ "optional": true,
272
+ "os": [
273
+ "linux"
274
+ ],
275
+ "engines": {
276
+ "node": ">=18"
277
+ }
278
+ },
279
+ "node_modules/@esbuild/linux-riscv64": {
280
+ "version": "0.25.12",
281
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
282
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
283
+ "cpu": [
284
+ "riscv64"
285
+ ],
286
+ "dev": true,
287
+ "license": "MIT",
288
+ "optional": true,
289
+ "os": [
290
+ "linux"
291
+ ],
292
+ "engines": {
293
+ "node": ">=18"
294
+ }
295
+ },
296
+ "node_modules/@esbuild/linux-s390x": {
297
+ "version": "0.25.12",
298
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
299
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
300
+ "cpu": [
301
+ "s390x"
302
+ ],
303
+ "dev": true,
304
+ "license": "MIT",
305
+ "optional": true,
306
+ "os": [
307
+ "linux"
308
+ ],
309
+ "engines": {
310
+ "node": ">=18"
311
+ }
312
+ },
313
+ "node_modules/@esbuild/linux-x64": {
314
+ "version": "0.25.12",
315
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
316
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
317
+ "cpu": [
318
+ "x64"
319
+ ],
320
+ "dev": true,
321
+ "license": "MIT",
322
+ "optional": true,
323
+ "os": [
324
+ "linux"
325
+ ],
326
+ "engines": {
327
+ "node": ">=18"
328
+ }
329
+ },
330
+ "node_modules/@esbuild/netbsd-arm64": {
331
+ "version": "0.25.12",
332
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
333
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
334
+ "cpu": [
335
+ "arm64"
336
+ ],
337
+ "dev": true,
338
+ "license": "MIT",
339
+ "optional": true,
340
+ "os": [
341
+ "netbsd"
342
+ ],
343
+ "engines": {
344
+ "node": ">=18"
345
+ }
346
+ },
347
+ "node_modules/@esbuild/netbsd-x64": {
348
+ "version": "0.25.12",
349
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
350
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
351
+ "cpu": [
352
+ "x64"
353
+ ],
354
+ "dev": true,
355
+ "license": "MIT",
356
+ "optional": true,
357
+ "os": [
358
+ "netbsd"
359
+ ],
360
+ "engines": {
361
+ "node": ">=18"
362
+ }
363
+ },
364
+ "node_modules/@esbuild/openbsd-arm64": {
365
+ "version": "0.25.12",
366
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
367
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
368
+ "cpu": [
369
+ "arm64"
370
+ ],
371
+ "dev": true,
372
+ "license": "MIT",
373
+ "optional": true,
374
+ "os": [
375
+ "openbsd"
376
+ ],
377
+ "engines": {
378
+ "node": ">=18"
379
+ }
380
+ },
381
+ "node_modules/@esbuild/openbsd-x64": {
382
+ "version": "0.25.12",
383
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
384
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
385
+ "cpu": [
386
+ "x64"
387
+ ],
388
+ "dev": true,
389
+ "license": "MIT",
390
+ "optional": true,
391
+ "os": [
392
+ "openbsd"
393
+ ],
394
+ "engines": {
395
+ "node": ">=18"
396
+ }
397
+ },
398
+ "node_modules/@esbuild/openharmony-arm64": {
399
+ "version": "0.25.12",
400
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
401
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
402
+ "cpu": [
403
+ "arm64"
404
+ ],
405
+ "dev": true,
406
+ "license": "MIT",
407
+ "optional": true,
408
+ "os": [
409
+ "openharmony"
410
+ ],
411
+ "engines": {
412
+ "node": ">=18"
413
+ }
414
+ },
415
+ "node_modules/@esbuild/sunos-x64": {
416
+ "version": "0.25.12",
417
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
418
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
419
+ "cpu": [
420
+ "x64"
421
+ ],
422
+ "dev": true,
423
+ "license": "MIT",
424
+ "optional": true,
425
+ "os": [
426
+ "sunos"
427
+ ],
428
+ "engines": {
429
+ "node": ">=18"
430
+ }
431
+ },
432
+ "node_modules/@esbuild/win32-arm64": {
433
+ "version": "0.25.12",
434
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
435
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
436
+ "cpu": [
437
+ "arm64"
438
+ ],
439
+ "dev": true,
440
+ "license": "MIT",
441
+ "optional": true,
442
+ "os": [
443
+ "win32"
444
+ ],
445
+ "engines": {
446
+ "node": ">=18"
447
+ }
448
+ },
449
+ "node_modules/@esbuild/win32-ia32": {
450
+ "version": "0.25.12",
451
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
452
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
453
+ "cpu": [
454
+ "ia32"
455
+ ],
456
+ "dev": true,
457
+ "license": "MIT",
458
+ "optional": true,
459
+ "os": [
460
+ "win32"
461
+ ],
462
+ "engines": {
463
+ "node": ">=18"
464
+ }
465
+ },
466
+ "node_modules/@esbuild/win32-x64": {
467
+ "version": "0.25.12",
468
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
469
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
470
+ "cpu": [
471
+ "x64"
472
+ ],
473
+ "dev": true,
474
+ "license": "MIT",
475
+ "optional": true,
476
+ "os": [
477
+ "win32"
478
+ ],
479
+ "engines": {
480
+ "node": ">=18"
481
+ }
482
+ },
483
+ "node_modules/@jridgewell/gen-mapping": {
484
+ "version": "0.3.13",
485
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
486
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
487
+ "dev": true,
488
+ "license": "MIT",
489
+ "dependencies": {
490
+ "@jridgewell/sourcemap-codec": "^1.5.0",
491
+ "@jridgewell/trace-mapping": "^0.3.24"
492
+ }
493
+ },
494
+ "node_modules/@jridgewell/resolve-uri": {
495
+ "version": "3.1.2",
496
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
497
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
498
+ "dev": true,
499
+ "license": "MIT",
500
+ "engines": {
501
+ "node": ">=6.0.0"
502
+ }
503
+ },
504
+ "node_modules/@jridgewell/sourcemap-codec": {
505
+ "version": "1.5.5",
506
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
507
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
508
+ "dev": true,
509
+ "license": "MIT"
510
+ },
511
+ "node_modules/@jridgewell/trace-mapping": {
512
+ "version": "0.3.31",
513
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
514
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
515
+ "dev": true,
516
+ "license": "MIT",
517
+ "dependencies": {
518
+ "@jridgewell/resolve-uri": "^3.1.0",
519
+ "@jridgewell/sourcemap-codec": "^1.4.14"
520
+ }
521
+ },
522
+ "node_modules/@next/env": {
523
+ "version": "14.2.5",
524
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
525
+ "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==",
526
+ "license": "MIT"
527
+ },
528
+ "node_modules/@next/swc-darwin-arm64": {
529
+ "version": "14.2.5",
530
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz",
531
+ "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==",
532
+ "cpu": [
533
+ "arm64"
534
+ ],
535
+ "license": "MIT",
536
+ "optional": true,
537
+ "os": [
538
+ "darwin"
539
+ ],
540
+ "engines": {
541
+ "node": ">= 10"
542
+ }
543
+ },
544
+ "node_modules/@next/swc-darwin-x64": {
545
+ "version": "14.2.5",
546
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz",
547
+ "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==",
548
+ "cpu": [
549
+ "x64"
550
+ ],
551
+ "license": "MIT",
552
+ "optional": true,
553
+ "os": [
554
+ "darwin"
555
+ ],
556
+ "engines": {
557
+ "node": ">= 10"
558
+ }
559
+ },
560
+ "node_modules/@next/swc-linux-arm64-gnu": {
561
+ "version": "14.2.5",
562
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz",
563
+ "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==",
564
+ "cpu": [
565
+ "arm64"
566
+ ],
567
+ "license": "MIT",
568
+ "optional": true,
569
+ "os": [
570
+ "linux"
571
+ ],
572
+ "engines": {
573
+ "node": ">= 10"
574
+ }
575
+ },
576
+ "node_modules/@next/swc-linux-arm64-musl": {
577
+ "version": "14.2.5",
578
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz",
579
+ "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==",
580
+ "cpu": [
581
+ "arm64"
582
+ ],
583
+ "license": "MIT",
584
+ "optional": true,
585
+ "os": [
586
+ "linux"
587
+ ],
588
+ "engines": {
589
+ "node": ">= 10"
590
+ }
591
+ },
592
+ "node_modules/@next/swc-linux-x64-gnu": {
593
+ "version": "14.2.5",
594
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz",
595
+ "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==",
596
+ "cpu": [
597
+ "x64"
598
+ ],
599
+ "license": "MIT",
600
+ "optional": true,
601
+ "os": [
602
+ "linux"
603
+ ],
604
+ "engines": {
605
+ "node": ">= 10"
606
+ }
607
+ },
608
+ "node_modules/@next/swc-linux-x64-musl": {
609
+ "version": "14.2.5",
610
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz",
611
+ "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==",
612
+ "cpu": [
613
+ "x64"
614
+ ],
615
+ "license": "MIT",
616
+ "optional": true,
617
+ "os": [
618
+ "linux"
619
+ ],
620
+ "engines": {
621
+ "node": ">= 10"
622
+ }
623
+ },
624
+ "node_modules/@next/swc-win32-arm64-msvc": {
625
+ "version": "14.2.5",
626
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz",
627
+ "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==",
628
+ "cpu": [
629
+ "arm64"
630
+ ],
631
+ "license": "MIT",
632
+ "optional": true,
633
+ "os": [
634
+ "win32"
635
+ ],
636
+ "engines": {
637
+ "node": ">= 10"
638
+ }
639
+ },
640
+ "node_modules/@next/swc-win32-ia32-msvc": {
641
+ "version": "14.2.5",
642
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz",
643
+ "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==",
644
+ "cpu": [
645
+ "ia32"
646
+ ],
647
+ "license": "MIT",
648
+ "optional": true,
649
+ "os": [
650
+ "win32"
651
+ ],
652
+ "engines": {
653
+ "node": ">= 10"
654
+ }
655
+ },
656
+ "node_modules/@next/swc-win32-x64-msvc": {
657
+ "version": "14.2.5",
658
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz",
659
+ "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==",
660
+ "cpu": [
661
+ "x64"
662
+ ],
663
+ "license": "MIT",
664
+ "optional": true,
665
+ "os": [
666
+ "win32"
667
+ ],
668
+ "engines": {
669
+ "node": ">= 10"
670
+ }
671
+ },
672
+ "node_modules/@nodelib/fs.scandir": {
673
+ "version": "2.1.5",
674
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
675
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
676
+ "dev": true,
677
+ "license": "MIT",
678
+ "dependencies": {
679
+ "@nodelib/fs.stat": "2.0.5",
680
+ "run-parallel": "^1.1.9"
681
+ },
682
+ "engines": {
683
+ "node": ">= 8"
684
+ }
685
+ },
686
+ "node_modules/@nodelib/fs.stat": {
687
+ "version": "2.0.5",
688
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
689
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
690
+ "dev": true,
691
+ "license": "MIT",
692
+ "engines": {
693
+ "node": ">= 8"
694
+ }
695
+ },
696
+ "node_modules/@nodelib/fs.walk": {
697
+ "version": "1.2.8",
698
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
699
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
700
+ "dev": true,
701
+ "license": "MIT",
702
+ "dependencies": {
703
+ "@nodelib/fs.scandir": "2.1.5",
704
+ "fastq": "^1.6.0"
705
+ },
706
+ "engines": {
707
+ "node": ">= 8"
708
+ }
709
+ },
710
+ "node_modules/@prisma/client": {
711
+ "version": "5.22.0",
712
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
713
+ "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
714
+ "hasInstallScript": true,
715
+ "license": "Apache-2.0",
716
+ "engines": {
717
+ "node": ">=16.13"
718
+ },
719
+ "peerDependencies": {
720
+ "prisma": "*"
721
+ },
722
+ "peerDependenciesMeta": {
723
+ "prisma": {
724
+ "optional": true
725
+ }
726
+ }
727
+ },
728
+ "node_modules/@prisma/debug": {
729
+ "version": "5.22.0",
730
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
731
+ "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
732
+ "devOptional": true,
733
+ "license": "Apache-2.0"
734
+ },
735
+ "node_modules/@prisma/engines": {
736
+ "version": "5.22.0",
737
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
738
+ "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
739
+ "devOptional": true,
740
+ "hasInstallScript": true,
741
+ "license": "Apache-2.0",
742
+ "dependencies": {
743
+ "@prisma/debug": "5.22.0",
744
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
745
+ "@prisma/fetch-engine": "5.22.0",
746
+ "@prisma/get-platform": "5.22.0"
747
+ }
748
+ },
749
+ "node_modules/@prisma/engines-version": {
750
+ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
751
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
752
+ "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
753
+ "devOptional": true,
754
+ "license": "Apache-2.0"
755
+ },
756
+ "node_modules/@prisma/fetch-engine": {
757
+ "version": "5.22.0",
758
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
759
+ "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
760
+ "devOptional": true,
761
+ "license": "Apache-2.0",
762
+ "dependencies": {
763
+ "@prisma/debug": "5.22.0",
764
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
765
+ "@prisma/get-platform": "5.22.0"
766
+ }
767
+ },
768
+ "node_modules/@prisma/get-platform": {
769
+ "version": "5.22.0",
770
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
771
+ "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
772
+ "devOptional": true,
773
+ "license": "Apache-2.0",
774
+ "dependencies": {
775
+ "@prisma/debug": "5.22.0"
776
+ }
777
+ },
778
+ "node_modules/@swc/counter": {
779
+ "version": "0.1.3",
780
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
781
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
782
+ "license": "Apache-2.0"
783
+ },
784
+ "node_modules/@swc/helpers": {
785
+ "version": "0.5.5",
786
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
787
+ "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
788
+ "license": "Apache-2.0",
789
+ "dependencies": {
790
+ "@swc/counter": "^0.1.3",
791
+ "tslib": "^2.4.0"
792
+ }
793
+ },
794
+ "node_modules/@types/node": {
795
+ "version": "20.19.25",
796
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
797
+ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
798
+ "dev": true,
799
+ "license": "MIT",
800
+ "dependencies": {
801
+ "undici-types": "~6.21.0"
802
+ }
803
+ },
804
+ "node_modules/@types/prop-types": {
805
+ "version": "15.7.15",
806
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
807
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
808
+ "dev": true,
809
+ "license": "MIT"
810
+ },
811
+ "node_modules/@types/react": {
812
+ "version": "18.3.27",
813
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
814
+ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
815
+ "dev": true,
816
+ "license": "MIT",
817
+ "dependencies": {
818
+ "@types/prop-types": "*",
819
+ "csstype": "^3.2.2"
820
+ }
821
+ },
822
+ "node_modules/@types/react-dom": {
823
+ "version": "18.3.7",
824
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
825
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
826
+ "dev": true,
827
+ "license": "MIT",
828
+ "peerDependencies": {
829
+ "@types/react": "^18.0.0"
830
+ }
831
+ },
832
+ "node_modules/any-promise": {
833
+ "version": "1.3.0",
834
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
835
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
836
+ "dev": true,
837
+ "license": "MIT"
838
+ },
839
+ "node_modules/anymatch": {
840
+ "version": "3.1.3",
841
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
842
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
843
+ "dev": true,
844
+ "license": "ISC",
845
+ "dependencies": {
846
+ "normalize-path": "^3.0.0",
847
+ "picomatch": "^2.0.4"
848
+ },
849
+ "engines": {
850
+ "node": ">= 8"
851
+ }
852
+ },
853
+ "node_modules/arg": {
854
+ "version": "5.0.2",
855
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
856
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
857
+ "dev": true,
858
+ "license": "MIT"
859
+ },
860
+ "node_modules/autoprefixer": {
861
+ "version": "10.4.22",
862
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
863
+ "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
864
+ "dev": true,
865
+ "funding": [
866
+ {
867
+ "type": "opencollective",
868
+ "url": "https://opencollective.com/postcss/"
869
+ },
870
+ {
871
+ "type": "tidelift",
872
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
873
+ },
874
+ {
875
+ "type": "github",
876
+ "url": "https://github.com/sponsors/ai"
877
+ }
878
+ ],
879
+ "license": "MIT",
880
+ "dependencies": {
881
+ "browserslist": "^4.27.0",
882
+ "caniuse-lite": "^1.0.30001754",
883
+ "fraction.js": "^5.3.4",
884
+ "normalize-range": "^0.1.2",
885
+ "picocolors": "^1.1.1",
886
+ "postcss-value-parser": "^4.2.0"
887
+ },
888
+ "bin": {
889
+ "autoprefixer": "bin/autoprefixer"
890
+ },
891
+ "engines": {
892
+ "node": "^10 || ^12 || >=14"
893
+ },
894
+ "peerDependencies": {
895
+ "postcss": "^8.1.0"
896
+ }
897
+ },
898
+ "node_modules/baseline-browser-mapping": {
899
+ "version": "2.8.31",
900
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
901
+ "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
902
+ "dev": true,
903
+ "license": "Apache-2.0",
904
+ "bin": {
905
+ "baseline-browser-mapping": "dist/cli.js"
906
+ }
907
+ },
908
+ "node_modules/binary-extensions": {
909
+ "version": "2.3.0",
910
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
911
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
912
+ "dev": true,
913
+ "license": "MIT",
914
+ "engines": {
915
+ "node": ">=8"
916
+ },
917
+ "funding": {
918
+ "url": "https://github.com/sponsors/sindresorhus"
919
+ }
920
+ },
921
+ "node_modules/braces": {
922
+ "version": "3.0.3",
923
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
924
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
925
+ "dev": true,
926
+ "license": "MIT",
927
+ "dependencies": {
928
+ "fill-range": "^7.1.1"
929
+ },
930
+ "engines": {
931
+ "node": ">=8"
932
+ }
933
+ },
934
+ "node_modules/browserslist": {
935
+ "version": "4.28.0",
936
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
937
+ "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
938
+ "dev": true,
939
+ "funding": [
940
+ {
941
+ "type": "opencollective",
942
+ "url": "https://opencollective.com/browserslist"
943
+ },
944
+ {
945
+ "type": "tidelift",
946
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
947
+ },
948
+ {
949
+ "type": "github",
950
+ "url": "https://github.com/sponsors/ai"
951
+ }
952
+ ],
953
+ "license": "MIT",
954
+ "dependencies": {
955
+ "baseline-browser-mapping": "^2.8.25",
956
+ "caniuse-lite": "^1.0.30001754",
957
+ "electron-to-chromium": "^1.5.249",
958
+ "node-releases": "^2.0.27",
959
+ "update-browserslist-db": "^1.1.4"
960
+ },
961
+ "bin": {
962
+ "browserslist": "cli.js"
963
+ },
964
+ "engines": {
965
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
966
+ }
967
+ },
968
+ "node_modules/busboy": {
969
+ "version": "1.6.0",
970
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
971
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
972
+ "dependencies": {
973
+ "streamsearch": "^1.1.0"
974
+ },
975
+ "engines": {
976
+ "node": ">=10.16.0"
977
+ }
978
+ },
979
+ "node_modules/camelcase-css": {
980
+ "version": "2.0.1",
981
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
982
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
983
+ "dev": true,
984
+ "license": "MIT",
985
+ "engines": {
986
+ "node": ">= 6"
987
+ }
988
+ },
989
+ "node_modules/caniuse-lite": {
990
+ "version": "1.0.30001757",
991
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
992
+ "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
993
+ "funding": [
994
+ {
995
+ "type": "opencollective",
996
+ "url": "https://opencollective.com/browserslist"
997
+ },
998
+ {
999
+ "type": "tidelift",
1000
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1001
+ },
1002
+ {
1003
+ "type": "github",
1004
+ "url": "https://github.com/sponsors/ai"
1005
+ }
1006
+ ],
1007
+ "license": "CC-BY-4.0"
1008
+ },
1009
+ "node_modules/chokidar": {
1010
+ "version": "3.6.0",
1011
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
1012
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
1013
+ "dev": true,
1014
+ "license": "MIT",
1015
+ "dependencies": {
1016
+ "anymatch": "~3.1.2",
1017
+ "braces": "~3.0.2",
1018
+ "glob-parent": "~5.1.2",
1019
+ "is-binary-path": "~2.1.0",
1020
+ "is-glob": "~4.0.1",
1021
+ "normalize-path": "~3.0.0",
1022
+ "readdirp": "~3.6.0"
1023
+ },
1024
+ "engines": {
1025
+ "node": ">= 8.10.0"
1026
+ },
1027
+ "funding": {
1028
+ "url": "https://paulmillr.com/funding/"
1029
+ },
1030
+ "optionalDependencies": {
1031
+ "fsevents": "~2.3.2"
1032
+ }
1033
+ },
1034
+ "node_modules/chokidar/node_modules/glob-parent": {
1035
+ "version": "5.1.2",
1036
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1037
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1038
+ "dev": true,
1039
+ "license": "ISC",
1040
+ "dependencies": {
1041
+ "is-glob": "^4.0.1"
1042
+ },
1043
+ "engines": {
1044
+ "node": ">= 6"
1045
+ }
1046
+ },
1047
+ "node_modules/client-only": {
1048
+ "version": "0.0.1",
1049
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
1050
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
1051
+ "license": "MIT"
1052
+ },
1053
+ "node_modules/commander": {
1054
+ "version": "4.1.1",
1055
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
1056
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
1057
+ "dev": true,
1058
+ "license": "MIT",
1059
+ "engines": {
1060
+ "node": ">= 6"
1061
+ }
1062
+ },
1063
+ "node_modules/cssesc": {
1064
+ "version": "3.0.0",
1065
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
1066
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
1067
+ "dev": true,
1068
+ "license": "MIT",
1069
+ "bin": {
1070
+ "cssesc": "bin/cssesc"
1071
+ },
1072
+ "engines": {
1073
+ "node": ">=4"
1074
+ }
1075
+ },
1076
+ "node_modules/csstype": {
1077
+ "version": "3.2.3",
1078
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1079
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1080
+ "dev": true,
1081
+ "license": "MIT"
1082
+ },
1083
+ "node_modules/didyoumean": {
1084
+ "version": "1.2.2",
1085
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
1086
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
1087
+ "dev": true,
1088
+ "license": "Apache-2.0"
1089
+ },
1090
+ "node_modules/dlv": {
1091
+ "version": "1.1.3",
1092
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
1093
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
1094
+ "dev": true,
1095
+ "license": "MIT"
1096
+ },
1097
+ "node_modules/electron-to-chromium": {
1098
+ "version": "1.5.262",
1099
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
1100
+ "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
1101
+ "dev": true,
1102
+ "license": "ISC"
1103
+ },
1104
+ "node_modules/esbuild": {
1105
+ "version": "0.25.12",
1106
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1107
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1108
+ "dev": true,
1109
+ "hasInstallScript": true,
1110
+ "license": "MIT",
1111
+ "bin": {
1112
+ "esbuild": "bin/esbuild"
1113
+ },
1114
+ "engines": {
1115
+ "node": ">=18"
1116
+ },
1117
+ "optionalDependencies": {
1118
+ "@esbuild/aix-ppc64": "0.25.12",
1119
+ "@esbuild/android-arm": "0.25.12",
1120
+ "@esbuild/android-arm64": "0.25.12",
1121
+ "@esbuild/android-x64": "0.25.12",
1122
+ "@esbuild/darwin-arm64": "0.25.12",
1123
+ "@esbuild/darwin-x64": "0.25.12",
1124
+ "@esbuild/freebsd-arm64": "0.25.12",
1125
+ "@esbuild/freebsd-x64": "0.25.12",
1126
+ "@esbuild/linux-arm": "0.25.12",
1127
+ "@esbuild/linux-arm64": "0.25.12",
1128
+ "@esbuild/linux-ia32": "0.25.12",
1129
+ "@esbuild/linux-loong64": "0.25.12",
1130
+ "@esbuild/linux-mips64el": "0.25.12",
1131
+ "@esbuild/linux-ppc64": "0.25.12",
1132
+ "@esbuild/linux-riscv64": "0.25.12",
1133
+ "@esbuild/linux-s390x": "0.25.12",
1134
+ "@esbuild/linux-x64": "0.25.12",
1135
+ "@esbuild/netbsd-arm64": "0.25.12",
1136
+ "@esbuild/netbsd-x64": "0.25.12",
1137
+ "@esbuild/openbsd-arm64": "0.25.12",
1138
+ "@esbuild/openbsd-x64": "0.25.12",
1139
+ "@esbuild/openharmony-arm64": "0.25.12",
1140
+ "@esbuild/sunos-x64": "0.25.12",
1141
+ "@esbuild/win32-arm64": "0.25.12",
1142
+ "@esbuild/win32-ia32": "0.25.12",
1143
+ "@esbuild/win32-x64": "0.25.12"
1144
+ }
1145
+ },
1146
+ "node_modules/escalade": {
1147
+ "version": "3.2.0",
1148
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1149
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1150
+ "dev": true,
1151
+ "license": "MIT",
1152
+ "engines": {
1153
+ "node": ">=6"
1154
+ }
1155
+ },
1156
+ "node_modules/fast-glob": {
1157
+ "version": "3.3.3",
1158
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1159
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1160
+ "dev": true,
1161
+ "license": "MIT",
1162
+ "dependencies": {
1163
+ "@nodelib/fs.stat": "^2.0.2",
1164
+ "@nodelib/fs.walk": "^1.2.3",
1165
+ "glob-parent": "^5.1.2",
1166
+ "merge2": "^1.3.0",
1167
+ "micromatch": "^4.0.8"
1168
+ },
1169
+ "engines": {
1170
+ "node": ">=8.6.0"
1171
+ }
1172
+ },
1173
+ "node_modules/fast-glob/node_modules/glob-parent": {
1174
+ "version": "5.1.2",
1175
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1176
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1177
+ "dev": true,
1178
+ "license": "ISC",
1179
+ "dependencies": {
1180
+ "is-glob": "^4.0.1"
1181
+ },
1182
+ "engines": {
1183
+ "node": ">= 6"
1184
+ }
1185
+ },
1186
+ "node_modules/fastq": {
1187
+ "version": "1.19.1",
1188
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
1189
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
1190
+ "dev": true,
1191
+ "license": "ISC",
1192
+ "dependencies": {
1193
+ "reusify": "^1.0.4"
1194
+ }
1195
+ },
1196
+ "node_modules/fill-range": {
1197
+ "version": "7.1.1",
1198
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1199
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1200
+ "dev": true,
1201
+ "license": "MIT",
1202
+ "dependencies": {
1203
+ "to-regex-range": "^5.0.1"
1204
+ },
1205
+ "engines": {
1206
+ "node": ">=8"
1207
+ }
1208
+ },
1209
+ "node_modules/fraction.js": {
1210
+ "version": "5.3.4",
1211
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
1212
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
1213
+ "dev": true,
1214
+ "license": "MIT",
1215
+ "engines": {
1216
+ "node": "*"
1217
+ },
1218
+ "funding": {
1219
+ "type": "github",
1220
+ "url": "https://github.com/sponsors/rawify"
1221
+ }
1222
+ },
1223
+ "node_modules/fsevents": {
1224
+ "version": "2.3.3",
1225
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1226
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1227
+ "dev": true,
1228
+ "hasInstallScript": true,
1229
+ "license": "MIT",
1230
+ "optional": true,
1231
+ "os": [
1232
+ "darwin"
1233
+ ],
1234
+ "engines": {
1235
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1236
+ }
1237
+ },
1238
+ "node_modules/function-bind": {
1239
+ "version": "1.1.2",
1240
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1241
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1242
+ "dev": true,
1243
+ "license": "MIT",
1244
+ "funding": {
1245
+ "url": "https://github.com/sponsors/ljharb"
1246
+ }
1247
+ },
1248
+ "node_modules/get-tsconfig": {
1249
+ "version": "4.13.0",
1250
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
1251
+ "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
1252
+ "dev": true,
1253
+ "license": "MIT",
1254
+ "dependencies": {
1255
+ "resolve-pkg-maps": "^1.0.0"
1256
+ },
1257
+ "funding": {
1258
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
1259
+ }
1260
+ },
1261
+ "node_modules/glob-parent": {
1262
+ "version": "6.0.2",
1263
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1264
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1265
+ "dev": true,
1266
+ "license": "ISC",
1267
+ "dependencies": {
1268
+ "is-glob": "^4.0.3"
1269
+ },
1270
+ "engines": {
1271
+ "node": ">=10.13.0"
1272
+ }
1273
+ },
1274
+ "node_modules/graceful-fs": {
1275
+ "version": "4.2.11",
1276
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1277
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1278
+ "license": "ISC"
1279
+ },
1280
+ "node_modules/hasown": {
1281
+ "version": "2.0.2",
1282
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1283
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1284
+ "dev": true,
1285
+ "license": "MIT",
1286
+ "dependencies": {
1287
+ "function-bind": "^1.1.2"
1288
+ },
1289
+ "engines": {
1290
+ "node": ">= 0.4"
1291
+ }
1292
+ },
1293
+ "node_modules/is-binary-path": {
1294
+ "version": "2.1.0",
1295
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1296
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1297
+ "dev": true,
1298
+ "license": "MIT",
1299
+ "dependencies": {
1300
+ "binary-extensions": "^2.0.0"
1301
+ },
1302
+ "engines": {
1303
+ "node": ">=8"
1304
+ }
1305
+ },
1306
+ "node_modules/is-core-module": {
1307
+ "version": "2.16.1",
1308
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
1309
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
1310
+ "dev": true,
1311
+ "license": "MIT",
1312
+ "dependencies": {
1313
+ "hasown": "^2.0.2"
1314
+ },
1315
+ "engines": {
1316
+ "node": ">= 0.4"
1317
+ },
1318
+ "funding": {
1319
+ "url": "https://github.com/sponsors/ljharb"
1320
+ }
1321
+ },
1322
+ "node_modules/is-extglob": {
1323
+ "version": "2.1.1",
1324
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1325
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1326
+ "dev": true,
1327
+ "license": "MIT",
1328
+ "engines": {
1329
+ "node": ">=0.10.0"
1330
+ }
1331
+ },
1332
+ "node_modules/is-glob": {
1333
+ "version": "4.0.3",
1334
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1335
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1336
+ "dev": true,
1337
+ "license": "MIT",
1338
+ "dependencies": {
1339
+ "is-extglob": "^2.1.1"
1340
+ },
1341
+ "engines": {
1342
+ "node": ">=0.10.0"
1343
+ }
1344
+ },
1345
+ "node_modules/is-number": {
1346
+ "version": "7.0.0",
1347
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1348
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1349
+ "dev": true,
1350
+ "license": "MIT",
1351
+ "engines": {
1352
+ "node": ">=0.12.0"
1353
+ }
1354
+ },
1355
+ "node_modules/jiti": {
1356
+ "version": "1.21.7",
1357
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1358
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1359
+ "dev": true,
1360
+ "license": "MIT",
1361
+ "bin": {
1362
+ "jiti": "bin/jiti.js"
1363
+ }
1364
+ },
1365
+ "node_modules/js-tokens": {
1366
+ "version": "4.0.0",
1367
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1368
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1369
+ "license": "MIT"
1370
+ },
1371
+ "node_modules/lilconfig": {
1372
+ "version": "3.1.3",
1373
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1374
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1375
+ "dev": true,
1376
+ "license": "MIT",
1377
+ "engines": {
1378
+ "node": ">=14"
1379
+ },
1380
+ "funding": {
1381
+ "url": "https://github.com/sponsors/antonk52"
1382
+ }
1383
+ },
1384
+ "node_modules/lines-and-columns": {
1385
+ "version": "1.2.4",
1386
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1387
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1388
+ "dev": true,
1389
+ "license": "MIT"
1390
+ },
1391
+ "node_modules/loose-envify": {
1392
+ "version": "1.4.0",
1393
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1394
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1395
+ "license": "MIT",
1396
+ "dependencies": {
1397
+ "js-tokens": "^3.0.0 || ^4.0.0"
1398
+ },
1399
+ "bin": {
1400
+ "loose-envify": "cli.js"
1401
+ }
1402
+ },
1403
+ "node_modules/merge2": {
1404
+ "version": "1.4.1",
1405
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1406
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1407
+ "dev": true,
1408
+ "license": "MIT",
1409
+ "engines": {
1410
+ "node": ">= 8"
1411
+ }
1412
+ },
1413
+ "node_modules/micromatch": {
1414
+ "version": "4.0.8",
1415
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1416
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1417
+ "dev": true,
1418
+ "license": "MIT",
1419
+ "dependencies": {
1420
+ "braces": "^3.0.3",
1421
+ "picomatch": "^2.3.1"
1422
+ },
1423
+ "engines": {
1424
+ "node": ">=8.6"
1425
+ }
1426
+ },
1427
+ "node_modules/mz": {
1428
+ "version": "2.7.0",
1429
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1430
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1431
+ "dev": true,
1432
+ "license": "MIT",
1433
+ "dependencies": {
1434
+ "any-promise": "^1.0.0",
1435
+ "object-assign": "^4.0.1",
1436
+ "thenify-all": "^1.0.0"
1437
+ }
1438
+ },
1439
+ "node_modules/nanoid": {
1440
+ "version": "3.3.11",
1441
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1442
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1443
+ "funding": [
1444
+ {
1445
+ "type": "github",
1446
+ "url": "https://github.com/sponsors/ai"
1447
+ }
1448
+ ],
1449
+ "license": "MIT",
1450
+ "bin": {
1451
+ "nanoid": "bin/nanoid.cjs"
1452
+ },
1453
+ "engines": {
1454
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1455
+ }
1456
+ },
1457
+ "node_modules/next": {
1458
+ "version": "14.2.5",
1459
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz",
1460
+ "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==",
1461
+ "license": "MIT",
1462
+ "dependencies": {
1463
+ "@next/env": "14.2.5",
1464
+ "@swc/helpers": "0.5.5",
1465
+ "busboy": "1.6.0",
1466
+ "caniuse-lite": "^1.0.30001579",
1467
+ "graceful-fs": "^4.2.11",
1468
+ "postcss": "8.4.31",
1469
+ "styled-jsx": "5.1.1"
1470
+ },
1471
+ "bin": {
1472
+ "next": "dist/bin/next"
1473
+ },
1474
+ "engines": {
1475
+ "node": ">=18.17.0"
1476
+ },
1477
+ "optionalDependencies": {
1478
+ "@next/swc-darwin-arm64": "14.2.5",
1479
+ "@next/swc-darwin-x64": "14.2.5",
1480
+ "@next/swc-linux-arm64-gnu": "14.2.5",
1481
+ "@next/swc-linux-arm64-musl": "14.2.5",
1482
+ "@next/swc-linux-x64-gnu": "14.2.5",
1483
+ "@next/swc-linux-x64-musl": "14.2.5",
1484
+ "@next/swc-win32-arm64-msvc": "14.2.5",
1485
+ "@next/swc-win32-ia32-msvc": "14.2.5",
1486
+ "@next/swc-win32-x64-msvc": "14.2.5"
1487
+ },
1488
+ "peerDependencies": {
1489
+ "@opentelemetry/api": "^1.1.0",
1490
+ "@playwright/test": "^1.41.2",
1491
+ "react": "^18.2.0",
1492
+ "react-dom": "^18.2.0",
1493
+ "sass": "^1.3.0"
1494
+ },
1495
+ "peerDependenciesMeta": {
1496
+ "@opentelemetry/api": {
1497
+ "optional": true
1498
+ },
1499
+ "@playwright/test": {
1500
+ "optional": true
1501
+ },
1502
+ "sass": {
1503
+ "optional": true
1504
+ }
1505
+ }
1506
+ },
1507
+ "node_modules/next/node_modules/postcss": {
1508
+ "version": "8.4.31",
1509
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
1510
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
1511
+ "funding": [
1512
+ {
1513
+ "type": "opencollective",
1514
+ "url": "https://opencollective.com/postcss/"
1515
+ },
1516
+ {
1517
+ "type": "tidelift",
1518
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1519
+ },
1520
+ {
1521
+ "type": "github",
1522
+ "url": "https://github.com/sponsors/ai"
1523
+ }
1524
+ ],
1525
+ "license": "MIT",
1526
+ "dependencies": {
1527
+ "nanoid": "^3.3.6",
1528
+ "picocolors": "^1.0.0",
1529
+ "source-map-js": "^1.0.2"
1530
+ },
1531
+ "engines": {
1532
+ "node": "^10 || ^12 || >=14"
1533
+ }
1534
+ },
1535
+ "node_modules/node-releases": {
1536
+ "version": "2.0.27",
1537
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
1538
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
1539
+ "dev": true,
1540
+ "license": "MIT"
1541
+ },
1542
+ "node_modules/normalize-path": {
1543
+ "version": "3.0.0",
1544
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1545
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1546
+ "dev": true,
1547
+ "license": "MIT",
1548
+ "engines": {
1549
+ "node": ">=0.10.0"
1550
+ }
1551
+ },
1552
+ "node_modules/normalize-range": {
1553
+ "version": "0.1.2",
1554
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
1555
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
1556
+ "dev": true,
1557
+ "license": "MIT",
1558
+ "engines": {
1559
+ "node": ">=0.10.0"
1560
+ }
1561
+ },
1562
+ "node_modules/object-assign": {
1563
+ "version": "4.1.1",
1564
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1565
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1566
+ "dev": true,
1567
+ "license": "MIT",
1568
+ "engines": {
1569
+ "node": ">=0.10.0"
1570
+ }
1571
+ },
1572
+ "node_modules/object-hash": {
1573
+ "version": "3.0.0",
1574
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1575
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1576
+ "dev": true,
1577
+ "license": "MIT",
1578
+ "engines": {
1579
+ "node": ">= 6"
1580
+ }
1581
+ },
1582
+ "node_modules/path-parse": {
1583
+ "version": "1.0.7",
1584
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1585
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1586
+ "dev": true,
1587
+ "license": "MIT"
1588
+ },
1589
+ "node_modules/picocolors": {
1590
+ "version": "1.1.1",
1591
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1592
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1593
+ "license": "ISC"
1594
+ },
1595
+ "node_modules/picomatch": {
1596
+ "version": "2.3.1",
1597
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1598
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1599
+ "dev": true,
1600
+ "license": "MIT",
1601
+ "engines": {
1602
+ "node": ">=8.6"
1603
+ },
1604
+ "funding": {
1605
+ "url": "https://github.com/sponsors/jonschlinkert"
1606
+ }
1607
+ },
1608
+ "node_modules/pify": {
1609
+ "version": "2.3.0",
1610
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1611
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1612
+ "dev": true,
1613
+ "license": "MIT",
1614
+ "engines": {
1615
+ "node": ">=0.10.0"
1616
+ }
1617
+ },
1618
+ "node_modules/pirates": {
1619
+ "version": "4.0.7",
1620
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1621
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1622
+ "dev": true,
1623
+ "license": "MIT",
1624
+ "engines": {
1625
+ "node": ">= 6"
1626
+ }
1627
+ },
1628
+ "node_modules/postcss": {
1629
+ "version": "8.5.6",
1630
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1631
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1632
+ "dev": true,
1633
+ "funding": [
1634
+ {
1635
+ "type": "opencollective",
1636
+ "url": "https://opencollective.com/postcss/"
1637
+ },
1638
+ {
1639
+ "type": "tidelift",
1640
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1641
+ },
1642
+ {
1643
+ "type": "github",
1644
+ "url": "https://github.com/sponsors/ai"
1645
+ }
1646
+ ],
1647
+ "license": "MIT",
1648
+ "dependencies": {
1649
+ "nanoid": "^3.3.11",
1650
+ "picocolors": "^1.1.1",
1651
+ "source-map-js": "^1.2.1"
1652
+ },
1653
+ "engines": {
1654
+ "node": "^10 || ^12 || >=14"
1655
+ }
1656
+ },
1657
+ "node_modules/postcss-import": {
1658
+ "version": "15.1.0",
1659
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1660
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1661
+ "dev": true,
1662
+ "license": "MIT",
1663
+ "dependencies": {
1664
+ "postcss-value-parser": "^4.0.0",
1665
+ "read-cache": "^1.0.0",
1666
+ "resolve": "^1.1.7"
1667
+ },
1668
+ "engines": {
1669
+ "node": ">=14.0.0"
1670
+ },
1671
+ "peerDependencies": {
1672
+ "postcss": "^8.0.0"
1673
+ }
1674
+ },
1675
+ "node_modules/postcss-js": {
1676
+ "version": "4.1.0",
1677
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
1678
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
1679
+ "dev": true,
1680
+ "funding": [
1681
+ {
1682
+ "type": "opencollective",
1683
+ "url": "https://opencollective.com/postcss/"
1684
+ },
1685
+ {
1686
+ "type": "github",
1687
+ "url": "https://github.com/sponsors/ai"
1688
+ }
1689
+ ],
1690
+ "license": "MIT",
1691
+ "dependencies": {
1692
+ "camelcase-css": "^2.0.1"
1693
+ },
1694
+ "engines": {
1695
+ "node": "^12 || ^14 || >= 16"
1696
+ },
1697
+ "peerDependencies": {
1698
+ "postcss": "^8.4.21"
1699
+ }
1700
+ },
1701
+ "node_modules/postcss-load-config": {
1702
+ "version": "6.0.1",
1703
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
1704
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
1705
+ "dev": true,
1706
+ "funding": [
1707
+ {
1708
+ "type": "opencollective",
1709
+ "url": "https://opencollective.com/postcss/"
1710
+ },
1711
+ {
1712
+ "type": "github",
1713
+ "url": "https://github.com/sponsors/ai"
1714
+ }
1715
+ ],
1716
+ "license": "MIT",
1717
+ "dependencies": {
1718
+ "lilconfig": "^3.1.1"
1719
+ },
1720
+ "engines": {
1721
+ "node": ">= 18"
1722
+ },
1723
+ "peerDependencies": {
1724
+ "jiti": ">=1.21.0",
1725
+ "postcss": ">=8.0.9",
1726
+ "tsx": "^4.8.1",
1727
+ "yaml": "^2.4.2"
1728
+ },
1729
+ "peerDependenciesMeta": {
1730
+ "jiti": {
1731
+ "optional": true
1732
+ },
1733
+ "postcss": {
1734
+ "optional": true
1735
+ },
1736
+ "tsx": {
1737
+ "optional": true
1738
+ },
1739
+ "yaml": {
1740
+ "optional": true
1741
+ }
1742
+ }
1743
+ },
1744
+ "node_modules/postcss-nested": {
1745
+ "version": "6.2.0",
1746
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1747
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1748
+ "dev": true,
1749
+ "funding": [
1750
+ {
1751
+ "type": "opencollective",
1752
+ "url": "https://opencollective.com/postcss/"
1753
+ },
1754
+ {
1755
+ "type": "github",
1756
+ "url": "https://github.com/sponsors/ai"
1757
+ }
1758
+ ],
1759
+ "license": "MIT",
1760
+ "dependencies": {
1761
+ "postcss-selector-parser": "^6.1.1"
1762
+ },
1763
+ "engines": {
1764
+ "node": ">=12.0"
1765
+ },
1766
+ "peerDependencies": {
1767
+ "postcss": "^8.2.14"
1768
+ }
1769
+ },
1770
+ "node_modules/postcss-selector-parser": {
1771
+ "version": "6.1.2",
1772
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1773
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1774
+ "dev": true,
1775
+ "license": "MIT",
1776
+ "dependencies": {
1777
+ "cssesc": "^3.0.0",
1778
+ "util-deprecate": "^1.0.2"
1779
+ },
1780
+ "engines": {
1781
+ "node": ">=4"
1782
+ }
1783
+ },
1784
+ "node_modules/postcss-value-parser": {
1785
+ "version": "4.2.0",
1786
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1787
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1788
+ "dev": true,
1789
+ "license": "MIT"
1790
+ },
1791
+ "node_modules/prisma": {
1792
+ "version": "5.22.0",
1793
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
1794
+ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
1795
+ "devOptional": true,
1796
+ "hasInstallScript": true,
1797
+ "license": "Apache-2.0",
1798
+ "dependencies": {
1799
+ "@prisma/engines": "5.22.0"
1800
+ },
1801
+ "bin": {
1802
+ "prisma": "build/index.js"
1803
+ },
1804
+ "engines": {
1805
+ "node": ">=16.13"
1806
+ },
1807
+ "optionalDependencies": {
1808
+ "fsevents": "2.3.3"
1809
+ }
1810
+ },
1811
+ "node_modules/queue-microtask": {
1812
+ "version": "1.2.3",
1813
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1814
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1815
+ "dev": true,
1816
+ "funding": [
1817
+ {
1818
+ "type": "github",
1819
+ "url": "https://github.com/sponsors/feross"
1820
+ },
1821
+ {
1822
+ "type": "patreon",
1823
+ "url": "https://www.patreon.com/feross"
1824
+ },
1825
+ {
1826
+ "type": "consulting",
1827
+ "url": "https://feross.org/support"
1828
+ }
1829
+ ],
1830
+ "license": "MIT"
1831
+ },
1832
+ "node_modules/react": {
1833
+ "version": "18.3.1",
1834
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1835
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1836
+ "license": "MIT",
1837
+ "dependencies": {
1838
+ "loose-envify": "^1.1.0"
1839
+ },
1840
+ "engines": {
1841
+ "node": ">=0.10.0"
1842
+ }
1843
+ },
1844
+ "node_modules/react-dom": {
1845
+ "version": "18.3.1",
1846
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1847
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1848
+ "license": "MIT",
1849
+ "dependencies": {
1850
+ "loose-envify": "^1.1.0",
1851
+ "scheduler": "^0.23.2"
1852
+ },
1853
+ "peerDependencies": {
1854
+ "react": "^18.3.1"
1855
+ }
1856
+ },
1857
+ "node_modules/read-cache": {
1858
+ "version": "1.0.0",
1859
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1860
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1861
+ "dev": true,
1862
+ "license": "MIT",
1863
+ "dependencies": {
1864
+ "pify": "^2.3.0"
1865
+ }
1866
+ },
1867
+ "node_modules/readdirp": {
1868
+ "version": "3.6.0",
1869
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1870
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1871
+ "dev": true,
1872
+ "license": "MIT",
1873
+ "dependencies": {
1874
+ "picomatch": "^2.2.1"
1875
+ },
1876
+ "engines": {
1877
+ "node": ">=8.10.0"
1878
+ }
1879
+ },
1880
+ "node_modules/resolve": {
1881
+ "version": "1.22.11",
1882
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
1883
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
1884
+ "dev": true,
1885
+ "license": "MIT",
1886
+ "dependencies": {
1887
+ "is-core-module": "^2.16.1",
1888
+ "path-parse": "^1.0.7",
1889
+ "supports-preserve-symlinks-flag": "^1.0.0"
1890
+ },
1891
+ "bin": {
1892
+ "resolve": "bin/resolve"
1893
+ },
1894
+ "engines": {
1895
+ "node": ">= 0.4"
1896
+ },
1897
+ "funding": {
1898
+ "url": "https://github.com/sponsors/ljharb"
1899
+ }
1900
+ },
1901
+ "node_modules/resolve-pkg-maps": {
1902
+ "version": "1.0.0",
1903
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
1904
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
1905
+ "dev": true,
1906
+ "license": "MIT",
1907
+ "funding": {
1908
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
1909
+ }
1910
+ },
1911
+ "node_modules/reusify": {
1912
+ "version": "1.1.0",
1913
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1914
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1915
+ "dev": true,
1916
+ "license": "MIT",
1917
+ "engines": {
1918
+ "iojs": ">=1.0.0",
1919
+ "node": ">=0.10.0"
1920
+ }
1921
+ },
1922
+ "node_modules/run-parallel": {
1923
+ "version": "1.2.0",
1924
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1925
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1926
+ "dev": true,
1927
+ "funding": [
1928
+ {
1929
+ "type": "github",
1930
+ "url": "https://github.com/sponsors/feross"
1931
+ },
1932
+ {
1933
+ "type": "patreon",
1934
+ "url": "https://www.patreon.com/feross"
1935
+ },
1936
+ {
1937
+ "type": "consulting",
1938
+ "url": "https://feross.org/support"
1939
+ }
1940
+ ],
1941
+ "license": "MIT",
1942
+ "dependencies": {
1943
+ "queue-microtask": "^1.2.2"
1944
+ }
1945
+ },
1946
+ "node_modules/scheduler": {
1947
+ "version": "0.23.2",
1948
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1949
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1950
+ "license": "MIT",
1951
+ "dependencies": {
1952
+ "loose-envify": "^1.1.0"
1953
+ }
1954
+ },
1955
+ "node_modules/source-map-js": {
1956
+ "version": "1.2.1",
1957
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1958
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1959
+ "license": "BSD-3-Clause",
1960
+ "engines": {
1961
+ "node": ">=0.10.0"
1962
+ }
1963
+ },
1964
+ "node_modules/streamsearch": {
1965
+ "version": "1.1.0",
1966
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
1967
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1968
+ "engines": {
1969
+ "node": ">=10.0.0"
1970
+ }
1971
+ },
1972
+ "node_modules/styled-jsx": {
1973
+ "version": "5.1.1",
1974
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
1975
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
1976
+ "license": "MIT",
1977
+ "dependencies": {
1978
+ "client-only": "0.0.1"
1979
+ },
1980
+ "engines": {
1981
+ "node": ">= 12.0.0"
1982
+ },
1983
+ "peerDependencies": {
1984
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
1985
+ },
1986
+ "peerDependenciesMeta": {
1987
+ "@babel/core": {
1988
+ "optional": true
1989
+ },
1990
+ "babel-plugin-macros": {
1991
+ "optional": true
1992
+ }
1993
+ }
1994
+ },
1995
+ "node_modules/sucrase": {
1996
+ "version": "3.35.1",
1997
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
1998
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
1999
+ "dev": true,
2000
+ "license": "MIT",
2001
+ "dependencies": {
2002
+ "@jridgewell/gen-mapping": "^0.3.2",
2003
+ "commander": "^4.0.0",
2004
+ "lines-and-columns": "^1.1.6",
2005
+ "mz": "^2.7.0",
2006
+ "pirates": "^4.0.1",
2007
+ "tinyglobby": "^0.2.11",
2008
+ "ts-interface-checker": "^0.1.9"
2009
+ },
2010
+ "bin": {
2011
+ "sucrase": "bin/sucrase",
2012
+ "sucrase-node": "bin/sucrase-node"
2013
+ },
2014
+ "engines": {
2015
+ "node": ">=16 || 14 >=14.17"
2016
+ }
2017
+ },
2018
+ "node_modules/supports-preserve-symlinks-flag": {
2019
+ "version": "1.0.0",
2020
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
2021
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
2022
+ "dev": true,
2023
+ "license": "MIT",
2024
+ "engines": {
2025
+ "node": ">= 0.4"
2026
+ },
2027
+ "funding": {
2028
+ "url": "https://github.com/sponsors/ljharb"
2029
+ }
2030
+ },
2031
+ "node_modules/tailwindcss": {
2032
+ "version": "3.4.18",
2033
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
2034
+ "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
2035
+ "dev": true,
2036
+ "license": "MIT",
2037
+ "dependencies": {
2038
+ "@alloc/quick-lru": "^5.2.0",
2039
+ "arg": "^5.0.2",
2040
+ "chokidar": "^3.6.0",
2041
+ "didyoumean": "^1.2.2",
2042
+ "dlv": "^1.1.3",
2043
+ "fast-glob": "^3.3.2",
2044
+ "glob-parent": "^6.0.2",
2045
+ "is-glob": "^4.0.3",
2046
+ "jiti": "^1.21.7",
2047
+ "lilconfig": "^3.1.3",
2048
+ "micromatch": "^4.0.8",
2049
+ "normalize-path": "^3.0.0",
2050
+ "object-hash": "^3.0.0",
2051
+ "picocolors": "^1.1.1",
2052
+ "postcss": "^8.4.47",
2053
+ "postcss-import": "^15.1.0",
2054
+ "postcss-js": "^4.0.1",
2055
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
2056
+ "postcss-nested": "^6.2.0",
2057
+ "postcss-selector-parser": "^6.1.2",
2058
+ "resolve": "^1.22.8",
2059
+ "sucrase": "^3.35.0"
2060
+ },
2061
+ "bin": {
2062
+ "tailwind": "lib/cli.js",
2063
+ "tailwindcss": "lib/cli.js"
2064
+ },
2065
+ "engines": {
2066
+ "node": ">=14.0.0"
2067
+ }
2068
+ },
2069
+ "node_modules/thenify": {
2070
+ "version": "3.3.1",
2071
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
2072
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
2073
+ "dev": true,
2074
+ "license": "MIT",
2075
+ "dependencies": {
2076
+ "any-promise": "^1.0.0"
2077
+ }
2078
+ },
2079
+ "node_modules/thenify-all": {
2080
+ "version": "1.6.0",
2081
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
2082
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
2083
+ "dev": true,
2084
+ "license": "MIT",
2085
+ "dependencies": {
2086
+ "thenify": ">= 3.1.0 < 4"
2087
+ },
2088
+ "engines": {
2089
+ "node": ">=0.8"
2090
+ }
2091
+ },
2092
+ "node_modules/tinyglobby": {
2093
+ "version": "0.2.15",
2094
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
2095
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2096
+ "dev": true,
2097
+ "license": "MIT",
2098
+ "dependencies": {
2099
+ "fdir": "^6.5.0",
2100
+ "picomatch": "^4.0.3"
2101
+ },
2102
+ "engines": {
2103
+ "node": ">=12.0.0"
2104
+ },
2105
+ "funding": {
2106
+ "url": "https://github.com/sponsors/SuperchupuDev"
2107
+ }
2108
+ },
2109
+ "node_modules/tinyglobby/node_modules/fdir": {
2110
+ "version": "6.5.0",
2111
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2112
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2113
+ "dev": true,
2114
+ "license": "MIT",
2115
+ "engines": {
2116
+ "node": ">=12.0.0"
2117
+ },
2118
+ "peerDependencies": {
2119
+ "picomatch": "^3 || ^4"
2120
+ },
2121
+ "peerDependenciesMeta": {
2122
+ "picomatch": {
2123
+ "optional": true
2124
+ }
2125
+ }
2126
+ },
2127
+ "node_modules/tinyglobby/node_modules/picomatch": {
2128
+ "version": "4.0.3",
2129
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
2130
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
2131
+ "dev": true,
2132
+ "license": "MIT",
2133
+ "engines": {
2134
+ "node": ">=12"
2135
+ },
2136
+ "funding": {
2137
+ "url": "https://github.com/sponsors/jonschlinkert"
2138
+ }
2139
+ },
2140
+ "node_modules/to-regex-range": {
2141
+ "version": "5.0.1",
2142
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2143
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2144
+ "dev": true,
2145
+ "license": "MIT",
2146
+ "dependencies": {
2147
+ "is-number": "^7.0.0"
2148
+ },
2149
+ "engines": {
2150
+ "node": ">=8.0"
2151
+ }
2152
+ },
2153
+ "node_modules/ts-interface-checker": {
2154
+ "version": "0.1.13",
2155
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
2156
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
2157
+ "dev": true,
2158
+ "license": "Apache-2.0"
2159
+ },
2160
+ "node_modules/tslib": {
2161
+ "version": "2.8.1",
2162
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2163
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2164
+ "license": "0BSD"
2165
+ },
2166
+ "node_modules/tsx": {
2167
+ "version": "4.20.6",
2168
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
2169
+ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
2170
+ "dev": true,
2171
+ "license": "MIT",
2172
+ "dependencies": {
2173
+ "esbuild": "~0.25.0",
2174
+ "get-tsconfig": "^4.7.5"
2175
+ },
2176
+ "bin": {
2177
+ "tsx": "dist/cli.mjs"
2178
+ },
2179
+ "engines": {
2180
+ "node": ">=18.0.0"
2181
+ },
2182
+ "optionalDependencies": {
2183
+ "fsevents": "~2.3.3"
2184
+ }
2185
+ },
2186
+ "node_modules/typescript": {
2187
+ "version": "5.9.3",
2188
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
2189
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2190
+ "dev": true,
2191
+ "license": "Apache-2.0",
2192
+ "bin": {
2193
+ "tsc": "bin/tsc",
2194
+ "tsserver": "bin/tsserver"
2195
+ },
2196
+ "engines": {
2197
+ "node": ">=14.17"
2198
+ }
2199
+ },
2200
+ "node_modules/undici-types": {
2201
+ "version": "6.21.0",
2202
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2203
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2204
+ "dev": true,
2205
+ "license": "MIT"
2206
+ },
2207
+ "node_modules/update-browserslist-db": {
2208
+ "version": "1.1.4",
2209
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
2210
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
2211
+ "dev": true,
2212
+ "funding": [
2213
+ {
2214
+ "type": "opencollective",
2215
+ "url": "https://opencollective.com/browserslist"
2216
+ },
2217
+ {
2218
+ "type": "tidelift",
2219
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2220
+ },
2221
+ {
2222
+ "type": "github",
2223
+ "url": "https://github.com/sponsors/ai"
2224
+ }
2225
+ ],
2226
+ "license": "MIT",
2227
+ "dependencies": {
2228
+ "escalade": "^3.2.0",
2229
+ "picocolors": "^1.1.1"
2230
+ },
2231
+ "bin": {
2232
+ "update-browserslist-db": "cli.js"
2233
+ },
2234
+ "peerDependencies": {
2235
+ "browserslist": ">= 4.21.0"
2236
+ }
2237
+ },
2238
+ "node_modules/util-deprecate": {
2239
+ "version": "1.0.2",
2240
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2241
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2242
+ "dev": true,
2243
+ "license": "MIT"
2244
+ }
2245
+ }
2246
+ }
package.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "odl-training-deals",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "prisma generate && next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "db:push": "prisma db push",
11
+ "db:seed": "tsx prisma/seed.ts",
12
+ "db:studio": "prisma studio",
13
+ "db:generate": "prisma generate"
14
+ },
15
+ "dependencies": {
16
+ "@prisma/client": "^5.19.0",
17
+ "next": "14.2.5",
18
+ "react": "^18.3.1",
19
+ "react-dom": "^18.3.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.14.12",
23
+ "@types/react": "^18.3.3",
24
+ "@types/react-dom": "^18.3.0",
25
+ "autoprefixer": "^10.4.20",
26
+ "postcss": "^8.4.41",
27
+ "prisma": "^5.19.0",
28
+ "tailwindcss": "^3.4.7",
29
+ "tsx": "^4.16.2",
30
+ "typescript": "^5.5.4"
31
+ }
32
+ }
33
+
postcss.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
7
+
prisma/schema.prisma ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ provider = "prisma-client-js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "sqlite" // Start with SQLite for MVP, easy migration to Postgres later
10
+ url = env("DATABASE_URL")
11
+ }
12
+
13
+ // Core Deals table - matches your provided table structure
14
+ model Deal {
15
+ id String @id @default(uuid()) // UUID for new deals, custom IDs allowed in seed
16
+ date String? // "2024-05-22" or "2024" or "2021-2024"
17
+ period String? // For ranges like "2021-2024"
18
+ modality String // "Text", "Image", "Audio", "Video", "Satellite", etc.
19
+ provider String
20
+ buyer String // Can be comma-separated for multiple buyers
21
+ dataType String @map("data_type") // Short description
22
+ reportedTerms String? @map("reported_terms") // Headline terms
23
+ creatorsCompensated Boolean? @map("creators_compensated") // Yes/No/Unclear
24
+ exclusive Boolean? // Yes/No/N/A
25
+ pricingMechanism String @map("pricing_mechanism")
26
+
27
+ // Extended fields from your schema plan
28
+ dealType String? @map("deal_type") // aggregate, per-unit, commissioning, etc.
29
+ priceUsd Float? @map("price_usd")
30
+ priceRangeMinUsd Float? @map("price_range_min_usd")
31
+ priceRangeMaxUsd Float? @map("price_range_max_usd")
32
+ priceCurrency String? @map("price_currency") @default("USD")
33
+ durationYears Float? @map("duration_years")
34
+ startDate DateTime? @map("start_date")
35
+ endDate DateTime? @map("end_date")
36
+
37
+ // Rights & restrictions
38
+ trainingAllowed Boolean? @map("training_allowed")
39
+ finetuningAllowed Boolean? @map("finetuning_allowed")
40
+ inferenceAllowed Boolean? @map("inference_allowed")
41
+ redistributionAllowed Boolean? @map("redistribution_allowed")
42
+ deletionRequired Boolean? @map("deletion_required")
43
+
44
+ // Compensation details
45
+ creatorSplitPercentage Float? @map("creator_split_percentage")
46
+ revenueShare Boolean? @map("revenue_share")
47
+
48
+ // Provenance
49
+ sources String // JSON array of URLs
50
+ sourcePrimary String? @map("source_primary") // "Reuters", "SEC filing", etc.
51
+ notes String?
52
+ dealStage String? @map("deal_stage") @default("confirmed") // announced, rumored, confirmed, settled
53
+ confidenceScore Float? @map("confidence_score") @default(1.0) // 0-1
54
+
55
+ // Relations
56
+ providerRelation Provider? @relation(fields: [providerId], references: [id])
57
+ providerId String? @map("provider_id")
58
+
59
+ buyerRelations DealBuyer[]
60
+
61
+ // Pricing normalization
62
+ pricingNormalizations PricingNormalization[]
63
+
64
+ // Timestamps
65
+ createdAt DateTime @default(now()) @map("created_at")
66
+ updatedAt DateTime @updatedAt @map("updated_at")
67
+
68
+ @@index([provider])
69
+ @@index([buyer])
70
+ @@index([modality])
71
+ @@index([date])
72
+ @@index([dealStage])
73
+ @@map("deals")
74
+ }
75
+
76
+ // Provider table (normalized)
77
+ model Provider {
78
+ id String @id @default(uuid())
79
+ name String @unique
80
+ industry String?
81
+ country String?
82
+ annualRevenue Float? @map("annual_revenue")
83
+ contentType String? @map("content_type")
84
+ ownershipStructure String? @map("ownership_structure") // publisher, creator-owned, platform, commons
85
+
86
+ deals Deal[]
87
+
88
+ createdAt DateTime @default(now()) @map("created_at")
89
+ updatedAt DateTime @updatedAt @map("updated_at")
90
+
91
+ @@map("providers")
92
+ }
93
+
94
+ // Buyer table (normalized)
95
+ model Buyer {
96
+ id String @id @default(uuid())
97
+ name String @unique
98
+ orgType String? @map("org_type") // lab, cloud, aggregator, startup, research
99
+ modelsAffected String? @map("models_affected") // JSON array
100
+
101
+ deals DealBuyer[]
102
+
103
+ createdAt DateTime @default(now()) @map("created_at")
104
+ updatedAt DateTime @updatedAt @map("updated_at")
105
+
106
+ @@map("buyers")
107
+ }
108
+
109
+ // Many-to-many relationship (deals can have multiple buyers)
110
+ model DealBuyer {
111
+ id String @id @default(uuid())
112
+ dealId String @map("deal_id")
113
+ buyerId String @map("buyer_id")
114
+
115
+ deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade)
116
+ buyer Buyer @relation(fields: [buyerId], references: [id], onDelete: Cascade)
117
+
118
+ @@unique([dealId, buyerId])
119
+ @@map("deal_buyers")
120
+ }
121
+
122
+ // Pricing normalization table
123
+ model PricingNormalization {
124
+ id String @id @default(uuid())
125
+ dealId String @map("deal_id")
126
+ unitType String @map("unit_type") // token, record, dataset, corpus, stream, minute, image, track
127
+ normalizedCostPerUnit Float @map("normalized_cost_per_unit")
128
+ normalizationMethod String @map("normalization_method") // manual, inferred, approximate
129
+ assumptions String? // JSON for assumptions used
130
+
131
+ deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade)
132
+
133
+ createdAt DateTime @default(now()) @map("created_at")
134
+
135
+ @@index([dealId])
136
+ @@map("pricing_normalizations")
137
+ }
138
+
139
+ // Source tracking for provenance
140
+ model DealSource {
141
+ id String @id @default(uuid())
142
+ dealId String @map("deal_id")
143
+ url String
144
+ sourceType String @map("source_type") // news, filing, press_release, rss
145
+ scrapedAt DateTime? @map("scraped_at")
146
+ htmlArchive String? @map("html_archive") // Path to stored HTML
147
+ extractedFields String? @map("extracted_fields") // JSON of what was extracted
148
+
149
+ createdAt DateTime @default(now()) @map("created_at")
150
+
151
+ @@index([dealId])
152
+ @@map("deal_sources")
153
+ }
154
+
prisma/seed.ts ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ const prisma = new PrismaClient()
4
+
5
+ // Seed data from your provided table (24 deals)
6
+ const dealsData = [
7
+ {
8
+ id: '1',
9
+ date: '2024-05-22',
10
+ modality: 'Text',
11
+ provider: 'News Corp',
12
+ buyer: 'OpenAI',
13
+ dataType: 'News archives & publisher content (WSJ, Times, NY Post)',
14
+ reportedTerms: '>$250M over 5 years',
15
+ creatorsCompensated: false,
16
+ exclusive: true,
17
+ pricingMechanism: 'Access / aggregate licensing',
18
+ dealType: 'aggregate',
19
+ priceUsd: 250000000,
20
+ durationYears: 5,
21
+ sourcePrimary: 'Reuters',
22
+ },
23
+ {
24
+ id: '2',
25
+ date: '2024-02-22',
26
+ modality: 'Text',
27
+ provider: 'Reddit',
28
+ buyer: 'Google',
29
+ dataType: 'Social-media UGC feed (API)',
30
+ reportedTerms: '≈$60M per year',
31
+ creatorsCompensated: false,
32
+ exclusive: false,
33
+ pricingMechanism: 'Volume-based API / access',
34
+ dealType: 'aggregate',
35
+ priceUsd: 60000000,
36
+ durationYears: 1,
37
+ sourcePrimary: 'Axios',
38
+ },
39
+ {
40
+ id: '3',
41
+ date: '2024-05',
42
+ modality: 'Text',
43
+ provider: 'Dotdash Meredith',
44
+ buyer: 'OpenAI',
45
+ dataType: 'Magazine & digital-media archives',
46
+ reportedTerms: '≥$16M per year (fixed component)',
47
+ creatorsCompensated: false,
48
+ exclusive: false,
49
+ pricingMechanism: 'Access / aggregate licensing',
50
+ dealType: 'aggregate',
51
+ priceUsd: 16000000,
52
+ durationYears: 1,
53
+ sourcePrimary: 'TechCrunch',
54
+ },
55
+ {
56
+ id: '4',
57
+ date: '2024-11',
58
+ modality: 'Text',
59
+ provider: 'HarperCollins',
60
+ buyer: 'Microsoft',
61
+ dataType: 'Non-fiction books (AI training rights)',
62
+ reportedTerms: '$5K per title; 50/50 publisher–author split',
63
+ creatorsCompensated: true,
64
+ exclusive: false,
65
+ pricingMechanism: 'Per-unit licensing (per book)',
66
+ dealType: 'per-unit',
67
+ creatorSplitPercentage: 50,
68
+ sourcePrimary: 'Reuters',
69
+ },
70
+ {
71
+ id: '5',
72
+ date: '2023',
73
+ modality: 'Text',
74
+ provider: 'Taylor & Francis',
75
+ buyer: 'Microsoft',
76
+ dataType: 'Academic journals & textbooks',
77
+ reportedTerms: '≈$10M',
78
+ creatorsCompensated: null, // Unclear
79
+ exclusive: false,
80
+ pricingMechanism: 'Restricted access licensing',
81
+ dealType: 'aggregate',
82
+ priceUsd: 10000000,
83
+ sourcePrimary: 'CB Insights',
84
+ },
85
+ {
86
+ id: '6',
87
+ date: '2024',
88
+ modality: 'Text',
89
+ provider: 'Wiley',
90
+ buyer: 'Anthropic, AWS, Perplexity',
91
+ dataType: 'Scientific content + enriched metadata',
92
+ reportedTerms: '$23M (per 2025 proxy filing)',
93
+ creatorsCompensated: false,
94
+ exclusive: false,
95
+ pricingMechanism: 'Limited-term structured license',
96
+ dealType: 'aggregate',
97
+ priceUsd: 23000000,
98
+ sourcePrimary: 'SEC Filing',
99
+ },
100
+ {
101
+ id: '7',
102
+ date: '2021-2024',
103
+ modality: 'Image / Video',
104
+ provider: 'Shutterstock',
105
+ buyer: 'Meta, OpenAI, Google, Apple',
106
+ dataType: 'Stock images & video + metadata',
107
+ reportedTerms: '≈$25–30M per deal (multi-year)',
108
+ creatorsCompensated: true, // Partial (royalty fund)
109
+ exclusive: false,
110
+ pricingMechanism: 'Hybrid per-unit + access',
111
+ dealType: 'hybrid',
112
+ priceRangeMinUsd: 25000000,
113
+ priceRangeMaxUsd: 30000000,
114
+ sourcePrimary: 'CB Insights',
115
+ },
116
+ {
117
+ id: '8',
118
+ date: '2024',
119
+ modality: 'Image',
120
+ provider: 'Freepik',
121
+ buyer: 'Unnamed AI firms',
122
+ dataType: '≈200M stock images',
123
+ reportedTerms: '≈$6M total (≈$0.02–0.04 per image)',
124
+ creatorsCompensated: false,
125
+ exclusive: false,
126
+ pricingMechanism: 'Per-unit micro-licensing',
127
+ dealType: 'per-unit',
128
+ priceUsd: 6000000,
129
+ sourcePrimary: 'TechCrunch',
130
+ },
131
+ {
132
+ id: '9',
133
+ date: '2020-2023',
134
+ modality: 'Image + Text',
135
+ provider: 'LAION / Common Crawl',
136
+ buyer: 'Open model builders',
137
+ dataType: 'Image–caption sets; large-scale web crawls',
138
+ reportedTerms: 'Open data; no direct cash',
139
+ creatorsCompensated: null, // N/A
140
+ exclusive: false,
141
+ pricingMechanism: 'Open commons / open-source',
142
+ dealType: 'commons',
143
+ priceUsd: 0,
144
+ sourcePrimary: 'Open Source',
145
+ },
146
+ {
147
+ id: '10',
148
+ date: '2025-05',
149
+ modality: 'Text',
150
+ provider: 'Le Monde',
151
+ buyer: 'OpenAI, Perplexity',
152
+ dataType: 'News content',
153
+ reportedTerms: 'Undisclosed; 25% AI revenue share to journalists',
154
+ creatorsCompensated: true,
155
+ exclusive: false,
156
+ pricingMechanism: 'Access licensing with rev-share',
157
+ dealType: 'aggregate',
158
+ revenueShare: true,
159
+ creatorSplitPercentage: 25,
160
+ sourcePrimary: 'Reuters',
161
+ },
162
+ {
163
+ id: '11',
164
+ date: '2025-07',
165
+ modality: 'Audio',
166
+ provider: 'SourceAudio',
167
+ buyer: 'ElevenLabs, Music.AI',
168
+ dataType: 'Pre-cleared music tracks for training',
169
+ reportedTerms: '$10M (multi-year)',
170
+ creatorsCompensated: true,
171
+ exclusive: false,
172
+ pricingMechanism: 'Access / catalog licensing',
173
+ dealType: 'aggregate',
174
+ priceUsd: 10000000,
175
+ sourcePrimary: 'MBW',
176
+ },
177
+ {
178
+ id: '12',
179
+ date: '2024',
180
+ modality: 'Audio',
181
+ provider: 'UMG, Warner',
182
+ buyer: 'AI music startups (e.g. Suno, Mubert)',
183
+ dataType: 'Major-label music catalogs (audio + lyrics)',
184
+ reportedTerms: 'Undisclosed',
185
+ creatorsCompensated: null, // Unclear
186
+ exclusive: false,
187
+ pricingMechanism: 'Music/audio access licensing',
188
+ dealType: 'aggregate',
189
+ sourcePrimary: 'MBW',
190
+ },
191
+ {
192
+ id: '13',
193
+ date: '2024',
194
+ modality: 'Audio',
195
+ provider: 'Audius + indie labels',
196
+ buyer: 'EU generative-music firms',
197
+ dataType: 'Independent tracks & stems',
198
+ reportedTerms: '~€0.30–€2.00 per track',
199
+ creatorsCompensated: true,
200
+ exclusive: false,
201
+ pricingMechanism: 'Per-unit micro-licensing',
202
+ dealType: 'per-unit',
203
+ priceRangeMinUsd: 0.30,
204
+ priceRangeMaxUsd: 2.00,
205
+ priceCurrency: 'EUR',
206
+ sourcePrimary: 'TechCrunch',
207
+ },
208
+ {
209
+ id: '14',
210
+ date: '2025-01',
211
+ modality: 'Video',
212
+ provider: 'YouTube creators (individuals)',
213
+ buyer: 'OpenAI, Meta',
214
+ dataType: 'Unpublished creator videos',
215
+ reportedTerms: '≈$5M total; ≈$1–4 per minute of footage',
216
+ creatorsCompensated: true,
217
+ exclusive: false,
218
+ pricingMechanism: 'Per-unit licensing (per video minute)',
219
+ dealType: 'per-unit',
220
+ priceUsd: 5000000,
221
+ priceRangeMinUsd: 1,
222
+ priceRangeMaxUsd: 4,
223
+ sourcePrimary: 'The Verge',
224
+ },
225
+ {
226
+ id: '15',
227
+ date: '2023-2024',
228
+ modality: 'Video',
229
+ provider: 'Independent creators',
230
+ buyer: 'Runway, Pika Labs',
231
+ dataType: 'Professional / unpublished video footage',
232
+ reportedTerms: '≈$1–4 per minute (estimated)',
233
+ creatorsCompensated: true,
234
+ exclusive: false,
235
+ pricingMechanism: 'Per-unit licensing',
236
+ dealType: 'per-unit',
237
+ priceRangeMinUsd: 1,
238
+ priceRangeMaxUsd: 4,
239
+ sourcePrimary: 'TechCrunch',
240
+ },
241
+ {
242
+ id: '16',
243
+ date: '2020-2024',
244
+ modality: 'Satellite',
245
+ provider: 'Planet Labs',
246
+ buyer: 'Agriculture / gov / AI firms',
247
+ dataType: 'High-frequency Earth observation imagery',
248
+ reportedTerms: '≈$180M annual revenue (licensing)',
249
+ creatorsCompensated: false,
250
+ exclusive: false,
251
+ pricingMechanism: 'Subscription-style access licensing',
252
+ dealType: 'aggregate',
253
+ priceUsd: 180000000,
254
+ durationYears: 1,
255
+ sourcePrimary: 'Company Filings',
256
+ },
257
+ {
258
+ id: '17',
259
+ date: '2024',
260
+ modality: 'Health / Biotech',
261
+ provider: 'Tempus',
262
+ buyer: 'Pharma & AI firms',
263
+ dataType: 'Anonymized patient + genomic data',
264
+ reportedTerms: '$200M over 3 years',
265
+ creatorsCompensated: false,
266
+ exclusive: false,
267
+ pricingMechanism: 'Access licensing for medical LLMs',
268
+ dealType: 'aggregate',
269
+ priceUsd: 200000000,
270
+ durationYears: 3,
271
+ sourcePrimary: 'SEC Filing',
272
+ },
273
+ {
274
+ id: '18',
275
+ date: '2025-06',
276
+ modality: 'Corporate / data infra',
277
+ provider: 'Scale AI',
278
+ buyer: 'Meta',
279
+ dataType: 'Data services + infrastructure (equity-linked)',
280
+ reportedTerms: '$14.3B for 49% stake (corporate deal)',
281
+ creatorsCompensated: null, // Variable
282
+ exclusive: true,
283
+ pricingMechanism: 'Strategic acquisition (data services)',
284
+ dealType: 'acquisition',
285
+ priceUsd: 14300000000,
286
+ sourcePrimary: 'Reuters',
287
+ },
288
+ {
289
+ id: '19',
290
+ date: '2025-04',
291
+ modality: 'Corporate / data infra',
292
+ provider: 'Informatica',
293
+ buyer: 'Salesforce',
294
+ dataType: 'Cloud data integration platform',
295
+ reportedTerms: '≈$8B acquisition',
296
+ creatorsCompensated: null, // N/A
297
+ exclusive: true,
298
+ pricingMechanism: 'Full acquisition (data pipeline assets)',
299
+ dealType: 'acquisition',
300
+ priceUsd: 8000000000,
301
+ sourcePrimary: 'Reuters',
302
+ },
303
+ {
304
+ id: '20',
305
+ date: '2025-09-05',
306
+ modality: 'Legal / Books',
307
+ provider: 'Authors & publishers (class action)',
308
+ buyer: 'Anthropic',
309
+ dataType: 'Books previously ingested without license',
310
+ reportedTerms: '$1.5B settlement; ≈$3K per book',
311
+ creatorsCompensated: true,
312
+ exclusive: null, // N/A
313
+ pricingMechanism: 'Legal settlement (with deletion & limits)',
314
+ dealType: 'settlement',
315
+ priceUsd: 1500000000,
316
+ deletionRequired: true,
317
+ sourcePrimary: 'Court Filing',
318
+ },
319
+ {
320
+ id: '21',
321
+ date: '2025-08',
322
+ modality: 'Text',
323
+ provider: 'CuriosityStream',
324
+ buyer: 'AI partners',
325
+ dataType: 'Factual & documentary video library (as text/video data)',
326
+ reportedTerms: '≈$20–30M per year; ≈25% of 2025 revenue',
327
+ creatorsCompensated: false,
328
+ exclusive: false,
329
+ pricingMechanism: 'Access licensing (subscription/API feed)',
330
+ dealType: 'aggregate',
331
+ priceRangeMinUsd: 20000000,
332
+ priceRangeMaxUsd: 30000000,
333
+ durationYears: 1,
334
+ sourcePrimary: 'Company Filings',
335
+ },
336
+ {
337
+ id: '22',
338
+ date: '2025',
339
+ modality: 'Text',
340
+ provider: 'New York Times',
341
+ buyer: 'Amazon',
342
+ dataType: 'Editorial news (incl. NYT Cooking, The Athletic)',
343
+ reportedTerms: '≈$20–25M per year',
344
+ creatorsCompensated: false,
345
+ exclusive: false,
346
+ pricingMechanism: 'Access licensing for Alexa / AI uses',
347
+ dealType: 'aggregate',
348
+ priceRangeMinUsd: 20000000,
349
+ priceRangeMaxUsd: 25000000,
350
+ durationYears: 1,
351
+ sourcePrimary: 'Axios',
352
+ },
353
+ {
354
+ id: '23',
355
+ date: '2025 H1',
356
+ modality: 'Text / Q&A',
357
+ provider: 'Chegg',
358
+ buyer: 'AI partners',
359
+ dataType: 'Expert-written Q&A pairs (homework help DB)',
360
+ reportedTerms: '$11M in H1 2025 (data licensing revenue)',
361
+ creatorsCompensated: null, // Unclear
362
+ exclusive: false,
363
+ pricingMechanism: 'Access licensing (subset of library)',
364
+ dealType: 'aggregate',
365
+ priceUsd: 11000000,
366
+ durationYears: 0.5,
367
+ sourcePrimary: 'SEC Filing',
368
+ },
369
+ {
370
+ id: '24',
371
+ date: '2022-2025',
372
+ modality: 'Commissioning',
373
+ provider: 'Mercor',
374
+ buyer: 'Multiple AI labs',
375
+ dataType: 'Domain-expert data for RLHF & fine-tuning',
376
+ reportedTerms: '≈$450M ARR (services tied to data creation)',
377
+ creatorsCompensated: true,
378
+ exclusive: false,
379
+ pricingMechanism: 'Commissioning / service-based',
380
+ dealType: 'commissioning',
381
+ priceUsd: 450000000,
382
+ durationYears: 1,
383
+ sourcePrimary: 'CB Insights',
384
+ },
385
+ ]
386
+
387
+ async function main() {
388
+ console.log('🌱 Seeding database...')
389
+
390
+ // Create providers and buyers first
391
+ const providers = new Set<string>()
392
+ const buyers = new Set<string>()
393
+
394
+ dealsData.forEach(deal => {
395
+ providers.add(deal.provider)
396
+ deal.buyer.split(',').forEach(b => buyers.add(b.trim()))
397
+ })
398
+
399
+ // Create providers
400
+ for (const providerName of providers) {
401
+ await prisma.provider.upsert({
402
+ where: { name: providerName },
403
+ update: {},
404
+ create: { name: providerName },
405
+ })
406
+ }
407
+
408
+ // Create buyers
409
+ for (const buyerName of buyers) {
410
+ await prisma.buyer.upsert({
411
+ where: { name: buyerName },
412
+ update: {},
413
+ create: { name: buyerName },
414
+ })
415
+ }
416
+
417
+ // Create deals
418
+ for (const dealData of dealsData) {
419
+ const provider = await prisma.provider.findUnique({
420
+ where: { name: dealData.provider },
421
+ })
422
+
423
+ const buyerNames = dealData.buyer.split(',').map(b => b.trim())
424
+
425
+ const deal = await prisma.deal.upsert({
426
+ where: { id: dealData.id },
427
+ update: {},
428
+ create: {
429
+ id: dealData.id,
430
+ date: dealData.date,
431
+ modality: dealData.modality,
432
+ provider: dealData.provider,
433
+ buyer: dealData.buyer,
434
+ dataType: dealData.dataType,
435
+ reportedTerms: dealData.reportedTerms,
436
+ creatorsCompensated: dealData.creatorsCompensated,
437
+ exclusive: dealData.exclusive,
438
+ pricingMechanism: dealData.pricingMechanism,
439
+ dealType: dealData.dealType,
440
+ priceUsd: dealData.priceUsd,
441
+ priceRangeMinUsd: dealData.priceRangeMinUsd,
442
+ priceRangeMaxUsd: dealData.priceRangeMaxUsd,
443
+ priceCurrency: dealData.priceCurrency || 'USD',
444
+ durationYears: dealData.durationYears,
445
+ creatorSplitPercentage: dealData.creatorSplitPercentage,
446
+ revenueShare: dealData.revenueShare,
447
+ deletionRequired: dealData.deletionRequired,
448
+ sourcePrimary: dealData.sourcePrimary,
449
+ sources: JSON.stringify([]),
450
+ dealStage: 'confirmed',
451
+ confidenceScore: 1.0,
452
+ providerId: provider?.id,
453
+ },
454
+ })
455
+
456
+ // Link buyers
457
+ for (const buyerName of buyerNames) {
458
+ const buyer = await prisma.buyer.findUnique({
459
+ where: { name: buyerName },
460
+ })
461
+ if (buyer) {
462
+ await prisma.dealBuyer.upsert({
463
+ where: {
464
+ dealId_buyerId: {
465
+ dealId: deal.id,
466
+ buyerId: buyer.id,
467
+ },
468
+ },
469
+ update: {},
470
+ create: {
471
+ dealId: deal.id,
472
+ buyerId: buyer.id,
473
+ },
474
+ })
475
+ }
476
+ }
477
+ }
478
+
479
+ console.log(`✅ Seeded ${dealsData.length} deals`)
480
+ console.log(`✅ Seeded ${providers.size} providers`)
481
+ console.log(`✅ Seeded ${buyers.size} buyers`)
482
+ }
483
+
484
+ main()
485
+ .catch((e) => {
486
+ console.error(e)
487
+ process.exit(1)
488
+ })
489
+ .finally(async () => {
490
+ await prisma.$disconnect()
491
+ })
492
+
tailwind.config.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from 'tailwindcss'
2
+
3
+ const config: Config = {
4
+ content: [
5
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
7
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ colors: {
12
+ base: 'var(--color-base)',
13
+ text: 'var(--color-text)',
14
+ 'text-muted': 'var(--color-text-muted)',
15
+ accent: 'var(--color-accent)',
16
+ 'accent-hover': 'var(--color-accent-hover)',
17
+ background: 'var(--color-background)',
18
+ surface: 'var(--color-surface)',
19
+ border: 'var(--color-border)',
20
+ 'border-subtle': 'var(--color-border-subtle)',
21
+ },
22
+ fontFamily: {
23
+ sans: ['var(--font-sans)'],
24
+ serif: ['var(--font-serif)'],
25
+ mono: ['var(--font-mono)'],
26
+ },
27
+ maxWidth: {
28
+ content: 'var(--max-width-content)',
29
+ narrow: 'var(--max-width-narrow)',
30
+ },
31
+ },
32
+ },
33
+ plugins: [],
34
+ }
35
+ export default config
36
+
tsconfig.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }
28
+
vercel.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "buildCommand": "npm run build",
3
+ "devCommand": "npm run dev",
4
+ "installCommand": "npm install",
5
+ "framework": "nextjs",
6
+ "regions": ["iad1"],
7
+ "env": {
8
+ "DATABASE_URL": "@database_url"
9
+ }
10
+ }
11
+