feat(stats): add share button to usage panes (#874)

This commit is contained in:
Jinwoo Hong 2026-04-20 21:54:16 -04:00 committed by GitHub
parent 25971cd32d
commit ff16ab1565
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 728 additions and 0 deletions

View file

@ -80,6 +80,7 @@
"electron-updater": "^6.8.3",
"github-slugger": "^2.0.0",
"hosted-git-info": "^9.0.2",
"html-to-image": "^1.11.13",
"lowlight": "^3.3.0",
"lucide-react": "^0.577.0",
"mermaid": "^11.14.0",

View file

@ -127,6 +127,9 @@ importers:
hosted-git-info:
specifier: ^9.0.2
version: 9.0.2
html-to-image:
specifier: ^1.11.13
version: 1.11.13
lowlight:
specifier: ^3.3.0
version: 3.3.0
@ -4050,6 +4053,9 @@ packages:
resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==}
engines: {node: ^20.17.0 || >=22.9.0}
html-to-image@1.11.13:
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@ -9997,6 +10003,8 @@ snapshots:
dependencies:
lru-cache: 11.2.7
html-to-image@1.11.13: {}
html-url-attributes@3.0.1: {}
http-cache-semantics@4.2.0: {}

View file

@ -25,6 +25,7 @@ import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageDailyChart } from './ClaudeUsageDailyChart'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
const RANGE_OPTIONS: ClaudeUsageRange[] = ['7d', '30d', '90d', 'all']
@ -137,6 +138,9 @@ export function ClaudeUsagePane(): React.JSX.Element {
</p>
</div>
<div className="flex shrink-0 items-center gap-2 self-start">
{summary && daily.length > 0 && (
<ShareUsageButton provider="claude" summary={summary} daily={daily} range={range} />
)}
<DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>

View file

@ -24,6 +24,7 @@ import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { CodexUsageDailyChart } from './CodexUsageDailyChart'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
const RANGE_OPTIONS: CodexUsageRange[] = ['7d', '30d', '90d', 'all']
@ -142,6 +143,9 @@ export function CodexUsagePane(): React.JSX.Element {
</p>
</div>
<div className="flex shrink-0 items-center gap-2 self-start">
{summary && daily.length > 0 && (
<ShareUsageButton provider="codex" summary={summary} daily={daily} range={range} />
)}
<DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>

View file

@ -0,0 +1,143 @@
import { useCallback, useRef, useState } from 'react'
import { toPng } from 'html-to-image'
import { Check, Copy, Share2 } from 'lucide-react'
import { Button } from '../ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ShareUsageCard, type ShareUsageCardProps } from './ShareUsageCard'
type ShareUsageButtonProps = ShareUsageCardProps
function XIcon(): React.JSX.Element {
return (
<svg width={16} height={16} viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
)
}
export function ShareUsageButton(props: ShareUsageButtonProps): React.JSX.Element {
const cardRef = useRef<HTMLDivElement>(null)
const [copied, setCopied] = useState(false)
const [capturing, setCapturing] = useState(false)
const captureToClipboard = useCallback(async () => {
if (!cardRef.current || capturing) {
return
}
setCapturing(true)
try {
const dataUrl = await toPng(cardRef.current, {
pixelRatio: 2,
backgroundColor: undefined
})
await window.api.ui.writeClipboardImage(dataUrl)
return true
} finally {
setCapturing(false)
}
}, [capturing])
const handleCopy = useCallback(async () => {
const ok = await captureToClipboard()
if (ok) {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}, [captureToClipboard])
const handleShareToX = useCallback(async () => {
const { provider, summary, range } = props
const providerName = provider === 'claude' ? 'Claude' : 'Codex'
const rangeLabel =
range === '7d'
? 'last 7 days'
: range === '30d'
? 'last 30 days'
: range === '90d'
? 'last 90 days'
: 'all-time'
const totalTokens =
provider === 'claude'
? summary.inputTokens + summary.outputTokens
: (summary as unknown as { totalTokens: number }).totalTokens
const cost = summary.estimatedCostUsd
const costStr =
cost === null ? 'n/a' : cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`
const fmtTokens = (v: number): string => {
if (v >= 1_000_000) {
return `${(v / 1_000_000).toFixed(1)}M`
}
if (v >= 1_000) {
return `${(v / 1_000).toFixed(1)}k`
}
return v.toLocaleString()
}
const lines = [
`My ${rangeLabel} ${providerName} usage via @orca_build`,
'',
`${fmtTokens(totalTokens)} tokens · ${costStr} est. cost`,
'',
'github.com/stablyai/orca'
]
const url = `https://x.com/intent/post?text=${encodeURIComponent(lines.join('\n'))}`
await window.api.shell.openUrl(url)
}, [props])
return (
<Dialog>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button variant="ghost" size="icon-xs" aria-label="Share usage">
<Share2 className="size-3.5" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
Share
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DialogContent className="max-w-fit" showCloseButton>
<DialogHeader>
<DialogTitle>Share usage</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center gap-3 py-2">
<ShareUsageCard ref={cardRef} {...props} />
<div className="flex w-full max-w-[480px] gap-2">
<Button onClick={() => void handleCopy()} disabled={capturing} className="flex-1">
{copied ? (
<>
<Check className="mr-2 size-4" />
Copied
</>
) : (
<>
<Copy className="mr-2 size-4" />
Copy image
</>
)}
</Button>
<Button
variant="outline"
onClick={() => void handleShareToX()}
disabled={capturing}
className="flex-1"
>
<span className="mr-2">
<XIcon />
</span>
Share on X
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -0,0 +1,359 @@
import { forwardRef } from 'react'
import type { ClaudeUsageSummary } from '../../../../shared/claude-usage-types'
import type { CodexUsageSummary } from '../../../../shared/codex-usage-types'
import {
BackgroundGlows,
CardFooter,
formatCost,
formatDateRange,
formatTokens,
getDailySegments,
getDailyTotal,
getLegendItems,
OrcaLogo,
RANGE_LABELS
} from './share-card-utils'
import type { ClaudeShareData, CodexShareData } from './share-card-utils'
export type ShareUsageCardProps = (ClaudeShareData | CodexShareData) & {
range: string
}
// Why: html-to-image uses SVG foreignObject which handles modern CSS fine,
// but inline styles are kept for portability and to avoid Tailwind class stripping.
export const ShareUsageCard = forwardRef<HTMLDivElement, ShareUsageCardProps>(
function ShareUsageCard(props, ref) {
const { provider, summary, daily, range } = props
const slicedDaily = daily.slice(-10)
const totalTokens =
provider === 'claude'
? summary.inputTokens + summary.outputTokens
: (summary as CodexUsageSummary).totalTokens
const topModel =
provider === 'claude'
? ((summary as ClaudeUsageSummary).topModel ?? 'n/a')
: ((summary as CodexUsageSummary).topModel ?? 'n/a')
const sessions =
provider === 'claude'
? (summary as ClaudeUsageSummary).sessions
: (summary as CodexUsageSummary).sessions
const turnsOrEvents =
provider === 'claude'
? { label: 'turns', count: (summary as ClaudeUsageSummary).turns }
: { label: 'events', count: (summary as CodexUsageSummary).events }
const providerLabel = provider === 'claude' ? 'Claude' : 'Codex'
return (
<div
ref={ref}
style={{
width: 480,
padding: '28px 28px 24px',
background: 'linear-gradient(145deg, #111111 0%, #0a0a0a 50%, #0d0d1a 100%)',
borderRadius: 16,
border: '1px solid rgba(255, 255, 255, 0.08)',
color: '#fafafa',
fontFamily: "'Helvetica Neue', Arial, sans-serif",
WebkitFontSmoothing: 'antialiased',
position: 'relative',
overflow: 'hidden'
}}
>
<BackgroundGlows />
<CardHeader providerLabel={providerLabel} range={range} />
<div
style={{ fontSize: 11, color: '#555', position: 'relative', zIndex: 1, marginBottom: 16 }}
>
{formatDateRange(range)}
</div>
<StatsGrid summary={summary} totalTokens={totalTokens} topModel={topModel} />
<div style={{ position: 'relative', zIndex: 1 }}>
<ChartHeader sessions={sessions} turnsOrEvents={turnsOrEvents} />
<DailyChart slicedDaily={slicedDaily} />
<DayLabels slicedDaily={slicedDaily} />
<Legend provider={provider} />
</div>
<CardFooter summary={summary} />
</div>
)
}
)
function CardHeader(props: { providerLabel: string; range: string }): React.JSX.Element {
return (
<div
style={{
display: 'table',
width: '100%',
marginBottom: 6,
position: 'relative',
zIndex: 1
}}
>
<div style={{ display: 'table-cell', verticalAlign: 'middle' }}>
<div style={{ display: 'inline-block', verticalAlign: 'middle' }}>
<OrcaLogo />
</div>
<div style={{ display: 'inline-block', verticalAlign: 'middle', marginLeft: 10 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#fafafa', lineHeight: 1.2 }}>
Orca IDE
</div>
<div style={{ fontSize: 10, color: '#555', letterSpacing: 0.3 }}>
{props.providerLabel} Usage
</div>
</div>
</div>
<div style={{ display: 'table-cell', verticalAlign: 'middle', textAlign: 'right' }}>
<span
style={{
fontSize: 11,
fontWeight: 500,
color: '#a1a1a1',
background: 'rgba(255, 255, 255, 0.06)',
padding: '3px 8px',
borderRadius: 6,
letterSpacing: 0.3
}}
>
{RANGE_LABELS[props.range] ?? props.range}
</span>
</div>
</div>
)
}
function StatsGrid(props: {
summary: { estimatedCostUsd: number | null }
totalTokens: number
topModel: string
}): React.JSX.Element {
const cards = [
{
value: formatCost(props.summary.estimatedCostUsd ?? null),
label: 'Est. cost',
bg: 'rgba(20, 71, 230, 0.1)',
border: '1px solid rgba(20, 71, 230, 0.2)',
valueColor: '#93b4ff',
valueFontSize: 16
},
{
value: formatTokens(props.totalTokens),
label: 'Total tokens',
bg: 'rgba(255, 255, 255, 0.04)',
border: '1px solid rgba(255, 255, 255, 0.06)',
valueColor: '#fafafa',
valueFontSize: 16
},
{
value: props.topModel,
label: 'Top model',
bg: 'rgba(255, 255, 255, 0.04)',
border: '1px solid rgba(255, 255, 255, 0.06)',
valueColor: '#fafafa',
valueFontSize: 14
}
]
return (
<div style={{ position: 'relative', zIndex: 1, marginBottom: 20 }}>
{cards.map((card, i) => (
<div
key={card.label}
style={{
display: 'inline-block',
verticalAlign: 'top',
width: 'calc(33.33% - 6px)',
marginLeft: i > 0 ? 8 : 0,
background: card.bg,
border: card.border,
borderRadius: 10,
padding: '10px 12px',
height: 52,
overflow: 'hidden',
boxSizing: 'border-box'
}}
>
<div
style={{
fontSize: card.valueFontSize,
fontWeight: 600,
color: card.valueColor,
lineHeight: 1.2,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{card.value}
</div>
<div style={{ fontSize: 10, color: '#666', marginTop: 2, letterSpacing: 0.2 }}>
{card.label}
</div>
</div>
))}
</div>
)
}
function ChartHeader(props: {
sessions: number
turnsOrEvents: { label: string; count: number }
}): React.JSX.Element {
return (
<div style={{ display: 'table', width: '100%', marginBottom: 10 }}>
<div style={{ display: 'table-cell', verticalAlign: 'bottom' }}>
<span
style={{
fontSize: 11,
fontWeight: 500,
color: '#555',
letterSpacing: 0.3,
textTransform: 'uppercase' as const
}}
>
Daily tokens
</span>
</div>
<div style={{ display: 'table-cell', verticalAlign: 'bottom', textAlign: 'right' }}>
<span style={{ fontSize: 10, color: '#444' }}>
{props.sessions} sessions · {props.turnsOrEvents.count} {props.turnsOrEvents.label}
</span>
</div>
</div>
)
}
function DailyChart(props: {
slicedDaily: Parameters<typeof getDailySegments>[0][]
}): React.JSX.Element {
const CHART_H = 120
const maxSegSum = Math.max(
1,
...props.slicedDaily.map((entry) => {
const segs = getDailySegments(entry)
return segs.reduce((sum, s) => sum + s.value, 0)
})
)
return (
<>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
tableLayout: 'fixed',
marginBottom: 6
}}
>
<tbody>
<tr>
{props.slicedDaily.map((entry) => (
<td
key={entry.day}
style={{ textAlign: 'center', padding: '0 3px', fontSize: 8, color: '#444' }}
>
{formatTokens(getDailyTotal(entry))}
</td>
))}
</tr>
</tbody>
</table>
<div style={{ height: CHART_H, overflow: 'hidden', marginBottom: 8 }}>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
tableLayout: 'fixed',
height: '100%'
}}
>
<tbody>
<tr>
{props.slicedDaily.map((entry) => {
const segments = getDailySegments(entry)
return (
<td
key={entry.day}
style={{ verticalAlign: 'bottom', textAlign: 'center', padding: '0 3px' }}
>
{segments.map((seg) =>
seg.value > 0 ? (
<div
key={seg.key}
style={{
height: Math.max(1, Math.round((seg.value / maxSegSum) * CHART_H)),
background: seg.color,
marginLeft: '15%',
marginRight: '15%'
}}
/>
) : null
)}
</td>
)
})}
</tr>
</tbody>
</table>
</div>
</>
)
}
function DayLabels(props: { slicedDaily: { day: string }[] }): React.JSX.Element {
return (
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<tbody>
<tr>
{props.slicedDaily.map((entry) => (
<td
key={entry.day}
style={{ textAlign: 'center', fontSize: 9, color: '#555', padding: '0 3px' }}
>
{entry.day.slice(5)}
</td>
))}
</tr>
</tbody>
</table>
)
}
function Legend(props: { provider: 'claude' | 'codex' }): React.JSX.Element {
return (
<div style={{ marginTop: 10 }}>
{getLegendItems(props.provider).map((item, i) => (
<span
key={item.label}
style={{
display: 'inline-block',
marginRight: i < 3 ? 12 : 0,
fontSize: 9,
color: '#555',
lineHeight: '14px'
}}
>
<span
style={{
display: 'inline-block',
width: 6,
height: 6,
borderRadius: '50%',
background: item.color,
verticalAlign: 'middle',
marginRight: 5
}}
/>
<span style={{ verticalAlign: 'middle' }}>{item.label}</span>
</span>
))}
</div>
)
}

View file

@ -0,0 +1,209 @@
import type {
ClaudeUsageDailyPoint,
ClaudeUsageSummary
} from '../../../../shared/claude-usage-types'
import type { CodexUsageDailyPoint, CodexUsageSummary } from '../../../../shared/codex-usage-types'
export type ClaudeShareData = {
provider: 'claude'
summary: ClaudeUsageSummary
daily: ClaudeUsageDailyPoint[]
}
export type CodexShareData = {
provider: 'codex'
summary: CodexUsageSummary
daily: CodexUsageDailyPoint[]
}
export function formatTokens(value: number): string {
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(1)}M`
}
if (value >= 1_000) {
return `${(value / 1_000).toFixed(1)}k`
}
return value.toLocaleString()
}
export function formatCost(value: number | null): string {
if (value === null) {
return 'n/a'
}
return value < 0.01 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`
}
export function formatDateRange(range: string): string {
const now = new Date()
const end = now.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
if (range === 'all') {
return `Through ${end}`
}
const days = parseInt(range)
if (Number.isNaN(days)) {
return end
}
const start = new Date(now.getTime() - days * 86_400_000)
const startStr = start.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
return `${startStr} ${end}`
}
export const RANGE_LABELS: Record<string, string> = {
'7d': 'Last 7 days',
'30d': 'Last 30 days',
'90d': 'Last 90 days',
all: 'All time'
}
export function getDailyTotal(entry: ClaudeUsageDailyPoint | CodexUsageDailyPoint): number {
if ('cacheReadTokens' in entry) {
return entry.inputTokens + entry.outputTokens + entry.cacheReadTokens + entry.cacheWriteTokens
}
return entry.totalTokens
}
export type DailySegment = { key: string; value: number; color: string }
export function getDailySegments(
entry: ClaudeUsageDailyPoint | CodexUsageDailyPoint
): DailySegment[] {
// Why: segment order matches the original charts exactly (top-to-bottom).
// Segments render as stacked block divs in a table cell with vertical-align: bottom.
if ('cacheReadTokens' in entry) {
return [
{ key: 'cache-write', value: entry.cacheWriteTokens, color: 'rgba(217, 70, 239, 0.7)' },
{ key: 'cache-read', value: entry.cacheReadTokens, color: 'rgba(251, 191, 36, 0.7)' },
{ key: 'output', value: entry.outputTokens, color: 'rgba(52, 211, 153, 0.8)' },
{ key: 'input', value: entry.inputTokens, color: 'rgba(56, 189, 248, 0.8)' }
]
}
return [
{ key: 'input', value: entry.inputTokens, color: 'rgba(56, 189, 248, 0.8)' },
{ key: 'output', value: entry.outputTokens, color: 'rgba(52, 211, 153, 0.8)' },
{ key: 'cached', value: entry.cachedInputTokens, color: 'rgba(251, 191, 36, 0.7)' },
{ key: 'reasoning', value: entry.reasoningOutputTokens, color: 'rgba(217, 70, 239, 0.7)' }
]
}
export function getLegendItems(provider: 'claude' | 'codex') {
if (provider === 'claude') {
return [
{ label: 'Input', color: 'rgba(56, 189, 248, 0.8)' },
{ label: 'Output', color: 'rgba(52, 211, 153, 0.8)' },
{ label: 'Cache read', color: 'rgba(251, 191, 36, 0.7)' },
{ label: 'Cache write', color: 'rgba(217, 70, 239, 0.7)' }
]
}
return [
{ label: 'Input', color: 'rgba(56, 189, 248, 0.8)' },
{ label: 'Output', color: 'rgba(52, 211, 153, 0.8)' },
{ label: 'Cached input', color: 'rgba(251, 191, 36, 0.7)' },
{ label: 'Reasoning', color: 'rgba(217, 70, 239, 0.7)' }
]
}
export function OrcaLogo(): React.JSX.Element {
return (
<svg
width={26}
height={26}
viewBox="0 0 318.60232 202.66667"
xmlns="http://www.w3.org/2000/svg"
style={{ opacity: 0.9, verticalAlign: 'middle' }}
>
<g style={{ display: 'inline' }} transform="translate(-6.6666669,-70.666669)">
<path
style={{ display: 'inline', fill: '#ffffff' }}
d="m 177.81311,248.33334 c 23.82304,-41.29793 40.54045,-66.84626 49.51207,-75.66667 6.81685,-6.70196 10.07373,-8.7374 20.07265,-12.54475 34.57822,-13.16655 61.04674,-26.78733 72.37222,-37.24295 9.62924,-8.88966 9.34286,-9.01142 -23.43671,-9.964 -35.71756,-1.03796 -43.72989,0.42119 -62.17546,11.323 -16.72118,9.88265 -34.20103,30.11225 -42.74704,49.47157 -2.57353,5.82985 -14.81294,44.3056 -27.96399,87.90747 -2.86036,9.48343 -3.02466,11.71633 -0.86213,11.71633 0.44382,0 7.29659,-11.25 15.22839,-25 z m -65.14644,-8.32267 C 120,239.3326 130.5,237.50979 136,235.95998 c 5.5,-1.5498 12.25,-3.13783 15,-3.52895 2.75,-0.39111 5,-0.95485 5,-1.25275 0,-0.29789 2.15135,-7.58487 4.78078,-16.19328 8.49209,-27.80201 12.21334,-40.41629 21.13747,-71.65166 4.81891,-16.86667 11.23502,-39.185 14.25802,-49.596301 5.12803,-17.66103 5.74763,-23.07037 2.64253,-23.07037 -1.84887,0 -4.07048,6.908293 -16.72243,52.000001 -21.78975,77.65896 -20.80806,74.74393 -26.84794,79.72251 -7.5925,6.25838 -25.03916,14.82524 -36.10856,17.73044 -17.0947,4.48656 -33.410599,3.86724 -53.116765,-2.01622 -18.569242,-5.54403 -23.142662,-5.80284 -33.639754,-1.9037 -5.875424,2.18242 -9.864152,5.04363 -16.716684,11.99127 -4.95,5.0187 -9.0000001,10.02884 -9.0000001,11.13364 0,1.75174 5.9276921,2.00299 46.3333351,1.96383 25.483334,-0.0247 52.333338,-0.59969 59.666668,-1.27777 z M 252.69513,104.63708 c 12.18267,-3.48651 15.77304,-7.895503 9.63821,-11.835773 -10.19296,-6.546726 -36.19849,-1.77301 -41.19436,7.561863 -1.2556,2.3461 -0.98698,3.2037 1.68353,5.375 2.69471,2.19098 4.59991,2.47691 12.53928,1.88189 5.14899,-0.3859 12.94899,-1.72824 17.33334,-2.98298 z"
/>
</g>
</svg>
)
}
export function BackgroundGlows(): React.JSX.Element {
return (
<>
<div
style={{
position: 'absolute',
top: '-60%',
right: '-20%',
width: 300,
height: 300,
background: 'radial-gradient(circle, rgba(20, 71, 230, 0.08) 0%, transparent 70%)',
pointerEvents: 'none'
}}
/>
<div
style={{
position: 'absolute',
bottom: '-40%',
left: '-10%',
width: 250,
height: 250,
background: 'radial-gradient(circle, rgba(139, 92, 246, 0.05) 0%, transparent 70%)',
pointerEvents: 'none'
}}
/>
</>
)
}
export function CardFooter(props: {
summary: { inputTokens: number; outputTokens: number }
}): React.JSX.Element {
return (
<div
style={{
display: 'table',
width: '100%',
marginTop: 16,
paddingTop: 12,
borderTop: '1px solid rgba(255, 255, 255, 0.05)',
position: 'relative',
zIndex: 1
}}
>
<div style={{ display: 'table-cell', verticalAlign: 'middle' }}>
<span style={{ fontSize: 12, color: '#888' }}>
<strong style={{ color: '#ccc' }}>{formatTokens(props.summary.inputTokens)}</strong> input
</span>
<span style={{ fontSize: 12, color: '#888', marginLeft: 16 }}>
<strong style={{ color: '#ccc' }}>{formatTokens(props.summary.outputTokens)}</strong>{' '}
output
</span>
</div>
<div style={{ display: 'table-cell', verticalAlign: 'middle', textAlign: 'right' }}>
<span style={{ display: 'inline-block', verticalAlign: 'middle' }}>
<GitHubIcon />
</span>
<span
style={{
fontSize: 11,
color: '#888',
letterSpacing: 0.2,
verticalAlign: 'middle',
marginLeft: 5
}}
>
github.com/stablyai/orca
</span>
</div>
</div>
)
}
function GitHubIcon(): React.JSX.Element {
return (
<svg
width={13}
height={13}
viewBox="0 0 16 16"
fill="#888"
style={{ opacity: 0.6, verticalAlign: 'middle' }}
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
)
}