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.
229 lines
5.8 KiB
TypeScript
229 lines
5.8 KiB
TypeScript
// 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 } |