From eecfdca78234bac80b06d9cecfef350f85df4e96 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 9 Feb 2026 21:20:50 -0800 Subject: [PATCH] Wire calculator page to ROI API-backed presets and saves --- data/calculator-presets.json | 46 ++ data/calculator-results.json | 53 +++ src/app/api/calculator/route.ts | 151 ++++++ src/app/calculator/page.tsx | 790 +++++++++++++++----------------- 4 files changed, 621 insertions(+), 419 deletions(-) create mode 100644 data/calculator-presets.json create mode 100644 data/calculator-results.json create mode 100644 src/app/api/calculator/route.ts diff --git a/data/calculator-presets.json b/data/calculator-presets.json new file mode 100644 index 00000000..e60416f7 --- /dev/null +++ b/data/calculator-presets.json @@ -0,0 +1,46 @@ +[ + { + "id": "solo-developer", + "name": "Solo Developer", + "description": "One builder automating repetitive coding and support tasks.", + "inputs": { + "agents": 1, + "hoursSavedPerAgentPerDay": 1.5, + "hourlyRate": 90, + "monthlyToolCosts": 149 + } + }, + { + "id": "small-team", + "name": "Small Team", + "description": "A startup squad using agents across product and operations.", + "inputs": { + "agents": 5, + "hoursSavedPerAgentPerDay": 2, + "hourlyRate": 75, + "monthlyToolCosts": 699 + } + }, + { + "id": "enterprise", + "name": "Enterprise", + "description": "Cross-functional enterprise deployment with strict SLAs.", + "inputs": { + "agents": 40, + "hoursSavedPerAgentPerDay": 1.75, + "hourlyRate": 95, + "monthlyToolCosts": 9500 + } + }, + { + "id": "agency", + "name": "Agency", + "description": "Client services team using agents to speed delivery.", + "inputs": { + "agents": 12, + "hoursSavedPerAgentPerDay": 2.5, + "hourlyRate": 85, + "monthlyToolCosts": 2200 + } + } +] diff --git a/data/calculator-results.json b/data/calculator-results.json new file mode 100644 index 00000000..f680ef57 --- /dev/null +++ b/data/calculator-results.json @@ -0,0 +1,53 @@ +[ + { + "id": "calc_seed_001", + "name": "Pilot - Internal Ops", + "inputs": { + "agents": 3, + "hoursSavedPerAgentPerDay": 1.8, + "hourlyRate": 70, + "monthlyToolCosts": 399 + }, + "results": { + "monthlySavings": 7917, + "annualRoi": 1984.21, + "paybackPeriodMonths": 0.05, + "efficiencyGainPercent": 22.5 + }, + "createdAt": "2026-01-03T14:23:00.000Z" + }, + { + "id": "calc_seed_002", + "name": "Sales Enablement Rollout", + "inputs": { + "agents": 8, + "hoursSavedPerAgentPerDay": 1.25, + "hourlyRate": 65, + "monthlyToolCosts": 1200 + }, + "results": { + "monthlySavings": 13100, + "annualRoi": 1091.67, + "paybackPeriodMonths": 0.09, + "efficiencyGainPercent": 15.63 + }, + "createdAt": "2026-01-10T09:10:00.000Z" + }, + { + "id": "calc_seed_003", + "name": "Agency Client Delivery", + "inputs": { + "agents": 10, + "hoursSavedPerAgentPerDay": 2.4, + "hourlyRate": 80, + "monthlyToolCosts": 2100 + }, + "results": { + "monthlySavings": 40140, + "annualRoi": 1911.43, + "paybackPeriodMonths": 0.05, + "efficiencyGainPercent": 30 + }, + "createdAt": "2026-01-22T18:47:00.000Z" + } +] diff --git a/src/app/api/calculator/route.ts b/src/app/api/calculator/route.ts new file mode 100644 index 00000000..188347da --- /dev/null +++ b/src/app/api/calculator/route.ts @@ -0,0 +1,151 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { NextRequest, NextResponse } from "next/server"; + +type CalculatorInputs = { + agents: number; + hoursSavedPerAgentPerDay: number; + hourlyRate: number; + monthlyToolCosts: number; +}; + +type CalculatorResults = { + monthlySavings: number; + annualRoi: number; + paybackPeriodMonths: number | null; + efficiencyGainPercent: number; +}; + +type SavedCalculation = { + id: string; + name: string; + inputs: CalculatorInputs; + results: CalculatorResults; + createdAt: string; +}; + +type SaveCalculationBody = { + name?: unknown; + inputs?: unknown; + results?: unknown; +}; + +const PRESETS_PATH = path.join(process.cwd(), "data", "calculator-presets.json"); +const RESULTS_PATH = path.join(process.cwd(), "data", "calculator-results.json"); + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +function isInputs(value: unknown): value is CalculatorInputs { + if (!value || typeof value !== "object") { + return false; + } + + const inputs = value as Partial; + + return ( + isFiniteNumber(inputs.agents) && + isFiniteNumber(inputs.hoursSavedPerAgentPerDay) && + isFiniteNumber(inputs.hourlyRate) && + isFiniteNumber(inputs.monthlyToolCosts) + ); +} + +function isResults(value: unknown): value is CalculatorResults { + if (!value || typeof value !== "object") { + return false; + } + + const results = value as Partial; + + const paybackValid = + results.paybackPeriodMonths === null || isFiniteNumber(results.paybackPeriodMonths); + + return ( + isFiniteNumber(results.monthlySavings) && + isFiniteNumber(results.annualRoi) && + paybackValid && + isFiniteNumber(results.efficiencyGainPercent) + ); +} + +async function readJsonFile(filePath: string, fallback: T): Promise { + try { + const raw = await fs.readFile(filePath, "utf8"); + return JSON.parse(raw) as T; + } catch { + return fallback; + } +} + +async function writeResults(results: SavedCalculation[]): Promise { + const dir = path.dirname(RESULTS_PATH); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(RESULTS_PATH, JSON.stringify(results, null, 2), "utf8"); +} + +export async function GET() { + try { + const presets = await readJsonFile(PRESETS_PATH, []); + + if (!Array.isArray(presets)) { + return NextResponse.json({ error: "Invalid presets format" }, { status: 500 }); + } + + return NextResponse.json({ presets }); + } catch (error) { + console.error("Failed to load calculator presets", error); + return NextResponse.json({ error: "Failed to load calculator presets" }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + const body = (await request.json()) as SaveCalculationBody; + + const name = typeof body.name === "string" ? body.name.trim() : ""; + const inputs = body.inputs; + const results = body.results; + + if (!name) { + return NextResponse.json({ error: "name is required" }, { status: 400 }); + } + + if (!isInputs(inputs)) { + return NextResponse.json( + { error: "inputs must include agents, hoursSavedPerAgentPerDay, hourlyRate, and monthlyToolCosts" }, + { status: 400 } + ); + } + + if (!isResults(results)) { + return NextResponse.json( + { + error: + "results must include monthlySavings, annualRoi, paybackPeriodMonths, and efficiencyGainPercent", + }, + { status: 400 } + ); + } + + const existingResults = await readJsonFile(RESULTS_PATH, []); + const safeResults = Array.isArray(existingResults) ? existingResults : []; + + const record: SavedCalculation = { + id: `calc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + name, + inputs, + results, + createdAt: new Date().toISOString(), + }; + + safeResults.unshift(record); + await writeResults(safeResults); + + return NextResponse.json({ calculation: record }, { status: 201 }); + } catch (error) { + console.error("Failed to save calculator result", error); + return NextResponse.json({ error: "Failed to save calculator result" }, { status: 500 }); + } +} diff --git a/src/app/calculator/page.tsx b/src/app/calculator/page.tsx index 329318ed..aad560f6 100644 --- a/src/app/calculator/page.tsx +++ b/src/app/calculator/page.tsx @@ -1,6 +1,11 @@ +/* eslint-disable react/no-unescaped-entities */ "use client"; -import { useState, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { Calculator, Loader2, Save, TrendingUp } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -8,457 +13,343 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; -import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Calculator, Lightbulb, TrendingDown, Zap } from "lucide-react"; - -// Model pricing (per 1K tokens) -interface ModelPricing { +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; + +type CalculatorInputs = { + agents: number; + hoursSavedPerAgentPerDay: number; + hourlyRate: number; + monthlyToolCosts: number; +}; + +type CalculatorPreset = { id: string; name: string; - provider: string; - inputCostPer1K: number; - outputCostPer1K: number; - color: string; + description: string; + inputs: CalculatorInputs; +}; + +type CalculatorResults = { + monthlySavings: number; + annualRoi: number; + paybackPeriodMonths: number | null; + efficiencyGainPercent: number; + monthlyProductivityValue: number; +}; + +const DEFAULT_INPUTS: CalculatorInputs = { + agents: 5, + hoursSavedPerAgentPerDay: 2, + hourlyRate: 75, + monthlyToolCosts: 699, +}; + +const WORK_DAYS_PER_MONTH = 22; +const HOURS_PER_WORK_DAY = 8; + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); } -const models: ModelPricing[] = [ - { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - provider: "OpenAI", - inputCostPer1K: 0.01, - outputCostPer1K: 0.03, - color: "#10a37f", - }, - { - id: "gpt-4o", - name: "GPT-4o", - provider: "OpenAI", - inputCostPer1K: 0.005, - outputCostPer1K: 0.015, - color: "#10a37f", - }, - { - id: "gpt-4o-mini", - name: "GPT-4o Mini", - provider: "OpenAI", - inputCostPer1K: 0.00015, - outputCostPer1K: 0.0006, - color: "#10a37f", - }, - { - id: "claude-3.5-sonnet", - name: "Claude 3.5 Sonnet", - provider: "Anthropic", - inputCostPer1K: 0.003, - outputCostPer1K: 0.015, - color: "#CC9B7A", - }, - { - id: "claude-3-opus", - name: "Claude 3 Opus", - provider: "Anthropic", - inputCostPer1K: 0.015, - outputCostPer1K: 0.075, - color: "#CC9B7A", - }, - { - id: "claude-3-haiku", - name: "Claude 3 Haiku", - provider: "Anthropic", - inputCostPer1K: 0.00025, - outputCostPer1K: 0.00125, - color: "#CC9B7A", - }, - { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro", - provider: "Google", - inputCostPer1K: 0.00125, - outputCostPer1K: 0.005, - color: "#4285F4", - }, - { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash", - provider: "Google", - inputCostPer1K: 0.000075, - outputCostPer1K: 0.0003, - color: "#4285F4", - }, -]; +function calculateResults(inputs: CalculatorInputs): CalculatorResults { + const monthlyProductivityValue = + inputs.agents * inputs.hoursSavedPerAgentPerDay * inputs.hourlyRate * WORK_DAYS_PER_MONTH; + const monthlySavings = monthlyProductivityValue - inputs.monthlyToolCosts; + const annualToolCost = inputs.monthlyToolCosts * 12; + const annualNetSavings = monthlySavings * 12; + + const annualRoi = + annualToolCost > 0 + ? (annualNetSavings / annualToolCost) * 100 + : annualNetSavings > 0 + ? 9999 + : 0; + + const paybackPeriodMonths = monthlySavings > 0 ? inputs.monthlyToolCosts / monthlySavings : null; + + const efficiencyGainPercent = (inputs.hoursSavedPerAgentPerDay / HOURS_PER_WORK_DAY) * 100; + + return { + monthlySavings, + annualRoi, + paybackPeriodMonths, + efficiencyGainPercent, + monthlyProductivityValue, + }; +} export default function CalculatorPage() { - const [selectedModel, setSelectedModel] = useState("gpt-4o"); - const [inputTokens, setInputTokens] = useState(1000); - const [outputTokens, setOutputTokens] = useState(500); - const [requestsPerDay, setRequestsPerDay] = useState(100); - const [agentCount, setAgentCount] = useState(1); - - const currentModel = useMemo( - () => models.find((m) => m.id === selectedModel) || models[0], - [selectedModel] - ); - - // Calculate costs - const costs = useMemo(() => { - const inputCost = (inputTokens / 1000) * currentModel.inputCostPer1K; - const outputCost = (outputTokens / 1000) * currentModel.outputCostPer1K; - const costPerRequest = inputCost + outputCost; - const dailyCost = costPerRequest * requestsPerDay * agentCount; - const monthlyCost = dailyCost * 30; - const yearlyCost = dailyCost * 365; - - return { - perRequest: costPerRequest, - daily: dailyCost, - monthly: monthlyCost, - yearly: yearlyCost, + const [presets, setPresets] = useState([]); + const [activePresetId, setActivePresetId] = useState(null); + const [inputs, setInputs] = useState(DEFAULT_INPUTS); + const [presetLoading, setPresetLoading] = useState(true); + const [presetError, setPresetError] = useState(null); + const [saveLoading, setSaveLoading] = useState(false); + const [saveError, setSaveError] = useState(null); + const [saveSuccess, setSaveSuccess] = useState(null); + + const results = useMemo(() => calculateResults(inputs), [inputs]); + + useEffect(() => { + const loadPresets = async () => { + try { + setPresetLoading(true); + setPresetError(null); + + const response = await fetch("/api/calculator", { method: "GET" }); + if (!response.ok) { + throw new Error("Failed to load presets"); + } + + const data = (await response.json()) as { presets?: CalculatorPreset[] }; + const loadedPresets = Array.isArray(data.presets) ? data.presets : []; + + setPresets(loadedPresets); + + if (loadedPresets.length > 0) { + setActivePresetId(loadedPresets[0].id); + setInputs(loadedPresets[0].inputs); + } + } catch { + setPresetError("We couldn't load presets right now. You can still use manual inputs."); + } finally { + setPresetLoading(false); + } }; - }, [currentModel, inputTokens, outputTokens, requestsPerDay, agentCount]); - - // Calculate comparison costs for all models - const comparisonData = useMemo(() => { - return models.map((model) => { - const inputCost = (inputTokens / 1000) * model.inputCostPer1K; - const outputCost = (outputTokens / 1000) * model.outputCostPer1K; - const costPerRequest = inputCost + outputCost; - const monthlyCost = costPerRequest * requestsPerDay * agentCount * 30; - return { - model, - monthlyCost, - }; - }).sort((a, b) => a.monthlyCost - b.monthlyCost); - }, [inputTokens, outputTokens, requestsPerDay, agentCount]); - - const formatCurrency = (amount: number) => { - if (amount < 0.01) { - return `$${amount.toFixed(4)}`; - } - return `$${amount.toFixed(2)}`; + + void loadPresets(); + }, []); + + const updateInput = (key: keyof CalculatorInputs, value: number, min: number, max: number) => { + setActivePresetId(null); + setInputs((prev) => ({ + ...prev, + [key]: clamp(value, min, max), + })); }; - const optimizationTips = [ - { - icon: , - title: "Use smaller models for simple tasks", - description: "Switch to models like GPT-4o Mini or Claude 3 Haiku for basic queries and save up to 95% on costs.", - }, - { - icon: , - title: "Implement prompt caching", - description: "Cache common system prompts to reduce input token usage by 50-90% on repeated requests.", - }, - { - icon: , - title: "Optimize prompt length", - description: "Review your prompts to remove unnecessary context. Each 1000 tokens saved can reduce costs significantly at scale.", - }, - { - icon: , - title: "Batch requests when possible", - description: "Group similar queries together to reduce overhead and improve efficiency.", - }, - ]; + const formatCurrency = (value: number) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(value); + + const formatPercent = (value: number) => `${value.toFixed(1)}%`; + + const handleSaveCalculation = async () => { + try { + setSaveLoading(true); + setSaveError(null); + setSaveSuccess(null); + + const presetName = presets.find((preset) => preset.id === activePresetId)?.name; + const name = presetName ? `${presetName} Snapshot` : "Custom ROI Snapshot"; + + const response = await fetch("/api/calculator", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + inputs, + results: { + monthlySavings: Number(results.monthlySavings.toFixed(2)), + annualRoi: Number(results.annualRoi.toFixed(2)), + paybackPeriodMonths: + results.paybackPeriodMonths === null + ? null + : Number(results.paybackPeriodMonths.toFixed(2)), + efficiencyGainPercent: Number(results.efficiencyGainPercent.toFixed(2)), + }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to save calculation"); + } + + setSaveSuccess("Calculation saved successfully."); + } catch { + setSaveError("Failed to save calculation. Please try again."); + } finally { + setSaveLoading(false); + } + }; return (
- {/* Hero */}
-
+
-
- - - Cost Estimator +
+ + + ROI Calculator -

- AI Agent Cost Calculator -

-

- Estimate your AI agent operational costs across different models. Compare pricing, optimize your budget, and make informed decisions. +

Agent ROI Calculator

+

+ Model your automation upside in real time. Pick a preset, tune your inputs, and save + your scenario.

- {/* Calculator */} -
-
- {/* Input Panel */} - - - Configuration - Enter your agent usage parameters - - - {/* Model Selection */} -
- - -
- - {/* Input Tokens */} -
- - setInputTokens(Number(e.target.value))} - className="bg-white/5 border-white/10 text-white" - /> -

- Typical: 500-2000 (includes system prompt + user input) -

-
- - {/* Output Tokens */} -
- - setOutputTokens(Number(e.target.value))} - className="bg-white/5 border-white/10 text-white" - /> -

- Typical: 200-1000 (agent response length) -

-
- - {/* Requests Per Day */} -
- - setRequestsPerDay(Number(e.target.value))} - className="bg-white/5 border-white/10 text-white" - /> -
- - {/* Agent Count */} -
- - setAgentCount(Number(e.target.value))} - className="bg-white/5 border-white/10 text-white" - /> -
-
-
- - {/* Results Panel */} - - - Estimated Costs - - Using {currentModel.name} - - - - {/* Per Request Cost */} -
-
Cost per Request
-
{formatCurrency(costs.perRequest)}
-
- - {/* Daily Cost */} -
-
Daily Cost
-
{formatCurrency(costs.daily)}
-
- {requestsPerDay * agentCount} requests/day -
-
- - {/* Monthly Cost */} -
-
Monthly Cost
-
{formatCurrency(costs.monthly)}
-
- ~{(requestsPerDay * agentCount * 30).toLocaleString()} requests/month -
-
- - {/* Yearly Cost */} -
-
Yearly Cost
-
{formatCurrency(costs.yearly)}
-
- ~{(requestsPerDay * agentCount * 365).toLocaleString()} requests/year -
-
- - {/* Breakdown */} -
-
- Input cost: - - {formatCurrency((inputTokens / 1000) * currentModel.inputCostPer1K)}/req - -
-
- Output cost: - - {formatCurrency((outputTokens / 1000) * currentModel.outputCostPer1K)}/req +
+ + + Inputs + Adjust your team assumptions and compare outcomes. + + +
+
+ + {presetLoading && ( + + Loading... -
+ )}
-
-
-
-
- - - - {/* Comparison Table */} -
-
-

Model Comparison

-

- See how different models compare at your usage level -

-
- - - {/* Header */} -
-
Model
-
Monthly Cost
-
vs Selected
+
+ {presets.map((preset) => ( + + ))} +
+ {presetError &&

{presetError}

}
- {/* Rows */} - {comparisonData.map((item, idx) => { - const isSelected = item.model.id === selectedModel; - const diff = item.monthlyCost - costs.monthly; - const diffPercent = costs.monthly > 0 ? (diff / costs.monthly) * 100 : 0; - - return ( -
-
-
-
{item.model.name}
- {isSelected && ( - - Selected - - )} -
-
{item.model.provider}
-
-
- {formatCurrency(item.monthlyCost)} -
-
- {isSelected ? ( - - ) : diff < 0 ? ( - - {formatCurrency(Math.abs(diff))} less ({Math.abs(diffPercent).toFixed(0)}%) - - ) : ( - - {formatCurrency(diff)} more (+{diffPercent.toFixed(0)}%) - - )} -
-
- ); - })} + updateInput("agents", value, 1, 200)} + /> + + updateInput("hoursSavedPerAgentPerDay", value, 0.25, 8)} + /> + + updateInput("hourlyRate", value, 15, 400)} + /> + + updateInput("monthlyToolCosts", value, 0, 100000)} + /> + +
+ + {saveError &&

{saveError}

} + {saveSuccess &&

{saveSuccess}

} +
-
- - - {/* Optimization Tips */} -
-
-

Cost Optimization Tips

-

- Reduce your AI agent costs without sacrificing quality -

-
- -
- {optimizationTips.map((tip) => ( - - -
-
- {tip.icon} -
-
- {tip.title} - - {tip.description} - -
-
-
-
- ))} -
+ + + Computed ROI + Real-time impact from your current assumptions. + + + + + + + + + + +
+

+ Formula used: agents × hours saved × hourly rate × 22 + days − monthly tool costs. +

+
+
+
- - - {/* Disclaimer */} -
- +
+ -

- Disclaimer: Cost estimates are based on current published pricing from model providers as of February 2025 and are subject to change. Actual costs may vary based on your specific usage patterns, volume discounts, enterprise agreements, and provider pricing updates. This calculator is for estimation purposes only and should not be considered financial advice. Always verify current pricing with your provider before making decisions. +

+ Note: This calculator provides directional ROI + estimates. Your actual outcomes depend on utilization, training quality, integration + depth, and process maturity.

@@ -466,3 +357,64 @@ export default function CalculatorPage() {
); } + +type InputRowProps = { + label: string; + value: number; + min: number; + max: number; + step: number; + onChange: (value: number) => void; +}; + +function InputRow({ label, value, min, max, step, onChange }: InputRowProps) { + return ( +
+
+ + {value} +
+ onChange(Number(event.target.value))} + className="w-full accent-[#06D6A0]" + /> + onChange(Number(event.target.value))} + className="border-white/10 bg-white/5 text-white" + /> +
+ ); +} + +type MetricTileProps = { + label: string; + value: string; + accent?: boolean; +}; + +function MetricTile({ label, value, accent }: MetricTileProps) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +}