Add PDF translation API, utilities, docs, and config
Introduces core backend and frontend infrastructure for a PDF translation interface. Adds API endpoints for translation, PDF testing, and AI provider testing; implements PDF text extraction, cost tracking, and pricing logic in the lib directory; adds reusable UI components; and provides comprehensive documentation (SDD, environment setup, Claude instructions). Updates Tailwind and global styles, and includes a sample test PDF and configuration files.
This commit is contained in:
229
lib/cost-tracker.ts
Normal file
229
lib/cost-tracker.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
// Cost accumulation and tracking system
|
||||
interface CostSession {
|
||||
id: string
|
||||
timestamp: number
|
||||
model: string
|
||||
provider: string
|
||||
tokenUsage: {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
}
|
||||
cost: {
|
||||
inputCost: number
|
||||
outputCost: number
|
||||
totalCost: number
|
||||
currency: string
|
||||
}
|
||||
}
|
||||
|
||||
interface CostSummary {
|
||||
totalSessions: number
|
||||
totalTokens: number
|
||||
totalCost: number
|
||||
currency: string
|
||||
startDate: number
|
||||
lastUpdated: number
|
||||
byProvider: Record<string, {
|
||||
sessions: number
|
||||
tokens: number
|
||||
cost: number
|
||||
}>
|
||||
byModel: Record<string, {
|
||||
sessions: number
|
||||
tokens: number
|
||||
cost: number
|
||||
}>
|
||||
}
|
||||
|
||||
class CostTracker {
|
||||
private readonly STORAGE_KEY = 'pdf_translation_cost_tracker'
|
||||
|
||||
// Get stored cost summary
|
||||
getCostSummary(): CostSummary {
|
||||
if (typeof window === 'undefined') {
|
||||
// Server-side fallback
|
||||
return this.getDefaultSummary()
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(this.STORAGE_KEY)
|
||||
if (!stored) {
|
||||
return this.getDefaultSummary()
|
||||
}
|
||||
return JSON.parse(stored)
|
||||
} catch (error) {
|
||||
console.error('Error reading cost summary:', error)
|
||||
return this.getDefaultSummary()
|
||||
}
|
||||
}
|
||||
|
||||
// Add new cost session
|
||||
addCostSession(session: Omit<CostSession, 'id' | 'timestamp'>): CostSummary {
|
||||
if (typeof window === 'undefined') {
|
||||
// Server-side fallback
|
||||
return this.getDefaultSummary()
|
||||
}
|
||||
|
||||
const summary = this.getCostSummary()
|
||||
const now = Date.now()
|
||||
|
||||
const newSession: CostSession = {
|
||||
...session,
|
||||
id: `session_${now}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: now
|
||||
}
|
||||
|
||||
// Update summary
|
||||
summary.totalSessions += 1
|
||||
summary.totalTokens += session.tokenUsage.totalTokens
|
||||
summary.totalCost += session.cost.totalCost
|
||||
summary.lastUpdated = now
|
||||
|
||||
if (summary.startDate === 0) {
|
||||
summary.startDate = now
|
||||
}
|
||||
|
||||
// Update by provider
|
||||
if (!summary.byProvider[session.provider]) {
|
||||
summary.byProvider[session.provider] = { sessions: 0, tokens: 0, cost: 0 }
|
||||
}
|
||||
summary.byProvider[session.provider].sessions += 1
|
||||
summary.byProvider[session.provider].tokens += session.tokenUsage.totalTokens
|
||||
summary.byProvider[session.provider].cost += session.cost.totalCost
|
||||
|
||||
// Update by model
|
||||
if (!summary.byModel[session.model]) {
|
||||
summary.byModel[session.model] = { sessions: 0, tokens: 0, cost: 0 }
|
||||
}
|
||||
summary.byModel[session.model].sessions += 1
|
||||
summary.byModel[session.model].tokens += session.tokenUsage.totalTokens
|
||||
summary.byModel[session.model].cost += session.cost.totalCost
|
||||
|
||||
// Save to localStorage
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(summary))
|
||||
} catch (error) {
|
||||
console.error('Error saving cost summary:', error)
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// Reset cost tracking
|
||||
resetCostTracking(): CostSummary {
|
||||
if (typeof window === 'undefined') {
|
||||
return this.getDefaultSummary()
|
||||
}
|
||||
|
||||
const summary = this.getDefaultSummary()
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(summary))
|
||||
} catch (error) {
|
||||
console.error('Error resetting cost tracking:', error)
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// Export cost data
|
||||
exportCostData(): string {
|
||||
const summary = this.getCostSummary()
|
||||
return JSON.stringify(summary, null, 2)
|
||||
}
|
||||
|
||||
// Format cost for display
|
||||
formatCost(amount: number, currency: string = 'USD'): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 4,
|
||||
maximumFractionDigits: 4
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Format large numbers
|
||||
formatNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// Get cost insights
|
||||
getCostInsights(): {
|
||||
averageCostPerSession: number
|
||||
averageTokensPerSession: number
|
||||
mostUsedProvider: string
|
||||
mostUsedModel: string
|
||||
dailyAverage: number
|
||||
} {
|
||||
const summary = this.getCostSummary()
|
||||
|
||||
if (summary.totalSessions === 0) {
|
||||
return {
|
||||
averageCostPerSession: 0,
|
||||
averageTokensPerSession: 0,
|
||||
mostUsedProvider: '',
|
||||
mostUsedModel: '',
|
||||
dailyAverage: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
const averageCostPerSession = summary.totalCost / summary.totalSessions
|
||||
const averageTokensPerSession = summary.totalTokens / summary.totalSessions
|
||||
|
||||
// Find most used provider
|
||||
let mostUsedProvider = ''
|
||||
let maxProviderSessions = 0
|
||||
Object.entries(summary.byProvider).forEach(([provider, stats]) => {
|
||||
if (stats.sessions > maxProviderSessions) {
|
||||
maxProviderSessions = stats.sessions
|
||||
mostUsedProvider = provider
|
||||
}
|
||||
})
|
||||
|
||||
// Find most used model
|
||||
let mostUsedModel = ''
|
||||
let maxModelSessions = 0
|
||||
Object.entries(summary.byModel).forEach(([model, stats]) => {
|
||||
if (stats.sessions > maxModelSessions) {
|
||||
maxModelSessions = stats.sessions
|
||||
mostUsedModel = model
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate daily average
|
||||
const daysSinceStart = summary.startDate > 0
|
||||
? Math.max(1, Math.ceil((Date.now() - summary.startDate) / (24 * 60 * 60 * 1000)))
|
||||
: 1
|
||||
const dailyAverage = summary.totalCost / daysSinceStart
|
||||
|
||||
return {
|
||||
averageCostPerSession,
|
||||
averageTokensPerSession,
|
||||
mostUsedProvider,
|
||||
mostUsedModel,
|
||||
dailyAverage
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultSummary(): CostSummary {
|
||||
return {
|
||||
totalSessions: 0,
|
||||
totalTokens: 0,
|
||||
totalCost: 0,
|
||||
currency: 'USD',
|
||||
startDate: 0,
|
||||
lastUpdated: 0,
|
||||
byProvider: {},
|
||||
byModel: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const costTracker = new CostTracker()
|
||||
export type { CostSession, CostSummary }
|
||||
Reference in New Issue
Block a user