mattrguerra/cortexmainPrivate on GitHub, published here in full

UsagePanel.tsx

Language TypeScript4.2 KB104 lines

src/components/UsagePanel.tsx

  1. import { useEffect, useState } from 'react'
  2. import type { UsageReport } from '../ipc/gen/UsageReport'
  3. import { shortModel, thousands, tokens, usd } from '../lib/format'
  4. import { useStore } from '../state/store'
  5. export function UsagePanel() {
  6. const client = useStore((s) => s.client)
  7. const spend = useStore((s) => s.spend)
  8. const setPanel = useStore((s) => s.setPanel)
  9. const [days, setDays] = useState<number | null>(null)
  10. const [report, setReport] = useState<UsageReport | null>(null)
  11. useEffect(() => {
  12. void client?.usageReport(days).then(setReport)
  13. }, [client, days])
  14. const maxModel = Math.max(1, ...(report?.by_model.map((r) => r.cost_nanousd) ?? [1]))
  15. const maxDay = Math.max(1, ...(report?.by_day.map((r) => r.cost_nanousd) ?? [1]))
  16. const total = report?.by_model.reduce((s, r) => s + r.cost_nanousd, 0) ?? 0
  17. const estimated = report?.by_model.some((r) => r.estimated)
  18. return (
  19. <div className="overlay" onMouseDown={(e) => e.target === e.currentTarget && setPanel(null)}>
  20. <div className="sheet glass-strong" role="dialog" aria-label="Usage">
  21. <header className="sheet-head">
  22. <h2>Usage</h2>
  23. <div className="filters">
  24. {(
  25. [
  26. [null, 'This month'],
  27. [7, '7 days'],
  28. [30, '30 days'],
  29. [365, 'Year'],
  30. ] as const
  31. ).map(([d, label]) => (
  32. <button key={label} className={`pill ${days === d ? 'on' : ''}`} onClick={() => setDays(d)}>
  33. {label}
  34. </button>
  35. ))}
  36. </div>
  37. <button className="ghost" onClick={() => setPanel(null)}>
  38. Close
  39. </button>
  40. </header>
  41. <div className="sheet-body">
  42. <div className="stat-row">
  43. <div className="stat glass-tint">
  44. <span className="muted small">Spent</span>
  45. <strong data-testid="usage-total">
  46. {usd(total)}
  47. {estimated ? '*' : ''}
  48. </strong>
  49. </div>
  50. <div className="stat glass-tint">
  51. <span className="muted small">Replies</span>
  52. <strong>{thousands(report?.by_model.reduce((s, r) => s + r.messages, 0) ?? 0)}</strong>
  53. </div>
  54. <div className="stat glass-tint">
  55. <span className="muted small">Tokens in / out</span>
  56. <strong>
  57. {tokens(report?.by_model.reduce((s, r) => s + r.prompt_tokens, 0) ?? 0)} /{' '}
  58. {tokens(report?.by_model.reduce((s, r) => s + r.completion_tokens, 0) ?? 0)}
  59. </strong>
  60. </div>
  61. {spend?.budget_usd != null && (
  62. <div className="stat glass-tint">
  63. <span className="muted small">Monthly budget</span>
  64. <strong>${spend.budget_usd.toFixed(2)}</strong>
  65. </div>
  66. )}
  67. </div>
  68. <h3>By model</h3>
  69. <ul className="bars">
  70. {report?.by_model.map((r) => (
  71. <li key={r.key}>
  72. <span className="bar-label" title={r.key}>
  73. {shortModel(r.key)}
  74. </span>
  75. <span className="bar-track">
  76. <span className="bar-fill" style={{ width: `${(r.cost_nanousd / maxModel) * 100}%` }} />
  77. </span>
  78. <span className="bar-value">
  79. {r.cost_nanousd > 0 ? usd(r.cost_nanousd) : 'free'}
  80. {r.estimated ? '*' : ''}
  81. </span>
  82. </li>
  83. ))}
  84. </ul>
  85. <h3>By day</h3>
  86. <div className="days" role="img" aria-label="Spend per day">
  87. {report?.by_day.map((r) => (
  88. <div key={r.key} className="day" title={`${r.key}: ${usd(r.cost_nanousd)}`}>
  89. <span className="day-fill" style={{ height: `${Math.max(3, (r.cost_nanousd / maxDay) * 100)}%` }} />
  90. <span className="day-label">{r.key.slice(5)}</span>
  91. </div>
  92. ))}
  93. </div>
  94. {estimated && (
  95. <p className="muted small">* includes estimates, from servers that do not report token counts.</p>
  96. )}
  97. </div>
  98. </div>
  99. </div>
  100. )
  101. }