@littlebearapps/platform-admin-sdk 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -7
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +206 -4
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/shared/tests/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchResultItem Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a single search result with icon, title, snippet, and content type indicator.
|
|
5
|
+
*
|
|
6
|
+
* @module dashboard/components/search/SearchResultItem
|
|
7
|
+
* @created 2026-02-03
|
|
8
|
+
* @task task-303.3
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SearchResult, SearchContentType } from '../../lib/search/types';
|
|
12
|
+
|
|
13
|
+
/** Icons for each content type */
|
|
14
|
+
const CONTENT_TYPE_ICONS: Record<SearchContentType, JSX.Element> = {
|
|
15
|
+
error: (
|
|
16
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
17
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
18
|
+
</svg>
|
|
19
|
+
),
|
|
20
|
+
pattern: (
|
|
21
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
22
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
23
|
+
</svg>
|
|
24
|
+
),
|
|
25
|
+
setting: (
|
|
26
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
28
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
29
|
+
</svg>
|
|
30
|
+
),
|
|
31
|
+
page: (
|
|
32
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
33
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
34
|
+
</svg>
|
|
35
|
+
),
|
|
36
|
+
service: (
|
|
37
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
38
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
|
39
|
+
</svg>
|
|
40
|
+
),
|
|
41
|
+
opportunity: (
|
|
42
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
43
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
44
|
+
</svg>
|
|
45
|
+
),
|
|
46
|
+
draft: (
|
|
47
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
48
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
49
|
+
</svg>
|
|
50
|
+
),
|
|
51
|
+
project: (
|
|
52
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
53
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
54
|
+
</svg>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Colour classes for each content type */
|
|
59
|
+
const CONTENT_TYPE_COLORS: Record<SearchContentType, string> = {
|
|
60
|
+
error: 'text-red-500 dark:text-red-400 bg-red-100 dark:bg-red-900/30',
|
|
61
|
+
pattern: 'text-purple-500 dark:text-purple-400 bg-purple-100 dark:bg-purple-900/30',
|
|
62
|
+
setting: 'text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700',
|
|
63
|
+
page: 'text-blue-500 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30',
|
|
64
|
+
service: 'text-green-500 dark:text-green-400 bg-green-100 dark:bg-green-900/30',
|
|
65
|
+
opportunity: 'text-orange-500 dark:text-orange-400 bg-orange-100 dark:bg-orange-900/30',
|
|
66
|
+
draft: 'text-cyan-500 dark:text-cyan-400 bg-cyan-100 dark:bg-cyan-900/30',
|
|
67
|
+
project: 'text-indigo-500 dark:text-indigo-400 bg-indigo-100 dark:bg-indigo-900/30',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Labels for each content type */
|
|
71
|
+
const CONTENT_TYPE_LABELS: Record<SearchContentType, string> = {
|
|
72
|
+
error: 'Error',
|
|
73
|
+
pattern: 'Pattern',
|
|
74
|
+
setting: 'Setting',
|
|
75
|
+
page: 'Page',
|
|
76
|
+
service: 'Service',
|
|
77
|
+
opportunity: 'Opportunity',
|
|
78
|
+
draft: 'Draft',
|
|
79
|
+
project: 'Project',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
interface SearchResultItemProps {
|
|
83
|
+
result: SearchResult;
|
|
84
|
+
isSelected: boolean;
|
|
85
|
+
query: string;
|
|
86
|
+
onClick: () => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function SearchResultItem({
|
|
90
|
+
result,
|
|
91
|
+
isSelected,
|
|
92
|
+
query,
|
|
93
|
+
onClick,
|
|
94
|
+
}: SearchResultItemProps): JSX.Element {
|
|
95
|
+
const icon = CONTENT_TYPE_ICONS[result.content_type] || CONTENT_TYPE_ICONS.page;
|
|
96
|
+
const colorClass = CONTENT_TYPE_COLORS[result.content_type] || CONTENT_TYPE_COLORS.page;
|
|
97
|
+
const label = CONTENT_TYPE_LABELS[result.content_type] || result.content_type;
|
|
98
|
+
|
|
99
|
+
// Highlight matching terms in the snippet - sanitized by only allowing alphanumeric matches wrapped in mark tags
|
|
100
|
+
const snippetText = result.snippet || result.content || '';
|
|
101
|
+
const highlightedParts = getHighlightedParts(snippetText, query);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<a
|
|
105
|
+
href={result.url}
|
|
106
|
+
onClick={(e) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
onClick();
|
|
109
|
+
}}
|
|
110
|
+
className={`flex items-start gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors ${
|
|
111
|
+
isSelected
|
|
112
|
+
? 'bg-blue-50 dark:bg-blue-900/30'
|
|
113
|
+
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
114
|
+
}`}
|
|
115
|
+
>
|
|
116
|
+
{/* Icon */}
|
|
117
|
+
<span
|
|
118
|
+
className={`flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center ${colorClass}`}
|
|
119
|
+
>
|
|
120
|
+
{icon}
|
|
121
|
+
</span>
|
|
122
|
+
|
|
123
|
+
{/* Content */}
|
|
124
|
+
<div className="flex-1 min-w-0">
|
|
125
|
+
<div className="flex items-center gap-2">
|
|
126
|
+
<span className="font-medium text-gray-900 dark:text-white truncate">
|
|
127
|
+
{result.title}
|
|
128
|
+
</span>
|
|
129
|
+
<span
|
|
130
|
+
className={`text-xs px-1.5 py-0.5 rounded ${colorClass}`}
|
|
131
|
+
>
|
|
132
|
+
{label}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
{snippetText && (
|
|
136
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 mt-0.5">
|
|
137
|
+
{highlightedParts.map((part, i) =>
|
|
138
|
+
part.isMatch ? (
|
|
139
|
+
<mark key={i} className="bg-yellow-200 dark:bg-yellow-800 rounded px-0.5">
|
|
140
|
+
{part.text}
|
|
141
|
+
</mark>
|
|
142
|
+
) : (
|
|
143
|
+
<span key={i}>{part.text}</span>
|
|
144
|
+
)
|
|
145
|
+
)}
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
{result.project && (
|
|
149
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">
|
|
150
|
+
{result.project}
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Arrow indicator when selected */}
|
|
156
|
+
{isSelected && (
|
|
157
|
+
<kbd className="flex-shrink-0 text-xs text-gray-400 border border-gray-200 dark:border-gray-600 rounded px-1.5 py-0.5">
|
|
158
|
+
↵
|
|
159
|
+
</kbd>
|
|
160
|
+
)}
|
|
161
|
+
</a>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface HighlightPart {
|
|
166
|
+
text: string;
|
|
167
|
+
isMatch: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get highlighted parts of text matching search terms
|
|
172
|
+
* Returns array of parts with isMatch flag for safe rendering
|
|
173
|
+
*/
|
|
174
|
+
function getHighlightedParts(text: string, query: string): HighlightPart[] {
|
|
175
|
+
if (!text || !query.trim()) {
|
|
176
|
+
return [{ text, isMatch: false }];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const terms = query
|
|
180
|
+
.toLowerCase()
|
|
181
|
+
.split(/\s+/)
|
|
182
|
+
.filter((t) => t.length > 2);
|
|
183
|
+
|
|
184
|
+
if (terms.length === 0) {
|
|
185
|
+
return [{ text, isMatch: false }];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Escape special regex characters
|
|
189
|
+
const escaped = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
190
|
+
const pattern = new RegExp(`(${escaped.join('|')})`, 'gi');
|
|
191
|
+
|
|
192
|
+
const parts: HighlightPart[] = [];
|
|
193
|
+
let lastIndex = 0;
|
|
194
|
+
let match: RegExpExecArray | null;
|
|
195
|
+
|
|
196
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
197
|
+
// Add non-matching text before this match
|
|
198
|
+
if (match.index > lastIndex) {
|
|
199
|
+
parts.push({ text: text.slice(lastIndex, match.index), isMatch: false });
|
|
200
|
+
}
|
|
201
|
+
// Add the matching text
|
|
202
|
+
parts.push({ text: match[0], isMatch: true });
|
|
203
|
+
lastIndex = pattern.lastIndex;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add remaining text after last match
|
|
207
|
+
if (lastIndex < text.length) {
|
|
208
|
+
parts.push({ text: text.slice(lastIndex), isMatch: false });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return parts.length > 0 ? parts : [{ text, isMatch: false }];
|
|
212
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Model Breakdown Component
|
|
3
|
+
*
|
|
4
|
+
* Displays per-model usage from Workers AI and AI Gateway.
|
|
5
|
+
* Shows costs, requests, and token counts by model.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect } from 'react';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
interface WorkersAIModel {
|
|
14
|
+
model: string;
|
|
15
|
+
project: string;
|
|
16
|
+
requests: number;
|
|
17
|
+
inputTokens: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
costUsd: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AIGatewayModel {
|
|
23
|
+
provider: string;
|
|
24
|
+
model: string;
|
|
25
|
+
gatewayId: string;
|
|
26
|
+
requests: number;
|
|
27
|
+
cachedRequests: number;
|
|
28
|
+
tokensIn: number;
|
|
29
|
+
tokensOut: number;
|
|
30
|
+
costUsd: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ApiResponse {
|
|
34
|
+
success: boolean;
|
|
35
|
+
workersAI?: {
|
|
36
|
+
models: WorkersAIModel[];
|
|
37
|
+
totalCostUsd: number;
|
|
38
|
+
totalRequests: number;
|
|
39
|
+
};
|
|
40
|
+
aiGateway?: {
|
|
41
|
+
models: AIGatewayModel[];
|
|
42
|
+
totalCostUsd: number;
|
|
43
|
+
totalRequests: number;
|
|
44
|
+
byProvider: Record<string, { requests: number; costUsd: number }>;
|
|
45
|
+
};
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Props {
|
|
50
|
+
period: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// CONSTANTS
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
const PROVIDER_COLORS: Record<string, string> = {
|
|
58
|
+
openai: 'bg-green-500',
|
|
59
|
+
anthropic: 'bg-orange-500',
|
|
60
|
+
'google-ai-studio': 'bg-blue-500',
|
|
61
|
+
'workers-ai': 'bg-amber-500',
|
|
62
|
+
deepseek: 'bg-cyan-500',
|
|
63
|
+
minimax: 'bg-indigo-500',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const PROVIDER_LABELS: Record<string, string> = {
|
|
67
|
+
openai: 'OpenAI',
|
|
68
|
+
anthropic: 'Anthropic',
|
|
69
|
+
'google-ai-studio': 'Google AI',
|
|
70
|
+
'workers-ai': 'Workers AI',
|
|
71
|
+
deepseek: 'DeepSeek',
|
|
72
|
+
minimax: 'MiniMax',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// HELPERS
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
function formatNumber(n: number): string {
|
|
80
|
+
if (n >= 1_000_000) {
|
|
81
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
82
|
+
}
|
|
83
|
+
if (n >= 1_000) {
|
|
84
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
85
|
+
}
|
|
86
|
+
return n.toLocaleString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatModelName(model: string): string {
|
|
90
|
+
// Shorten common model prefixes
|
|
91
|
+
return model
|
|
92
|
+
.replace('@cf/meta/', '')
|
|
93
|
+
.replace('@cf/mistral/', '')
|
|
94
|
+
.replace('@cf/', '')
|
|
95
|
+
.replace('gpt-', 'GPT-')
|
|
96
|
+
.replace('claude-', 'Claude ')
|
|
97
|
+
.replace('gemini-', 'Gemini ');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// COMPONENTS
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
function LoadingState() {
|
|
105
|
+
return (
|
|
106
|
+
<div className="animate-pulse space-y-3">
|
|
107
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3" />
|
|
108
|
+
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
109
|
+
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function EmptyState() {
|
|
115
|
+
return (
|
|
116
|
+
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
117
|
+
<div className="text-3xl mb-2">🤖</div>
|
|
118
|
+
<p className="text-sm">No AI model usage data available</p>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ProviderBar({
|
|
124
|
+
byProvider,
|
|
125
|
+
totalCost,
|
|
126
|
+
}: {
|
|
127
|
+
byProvider: Record<string, { requests: number; costUsd: number }>;
|
|
128
|
+
totalCost: number;
|
|
129
|
+
}) {
|
|
130
|
+
const providers = Object.entries(byProvider).sort((a, b) => b[1].costUsd - a[1].costUsd);
|
|
131
|
+
|
|
132
|
+
if (providers.length === 0 || totalCost === 0) return null;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="mb-4">
|
|
136
|
+
<div className="flex h-3 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700">
|
|
137
|
+
{providers.map(([provider, data]) => {
|
|
138
|
+
const width = (data.costUsd / totalCost) * 100;
|
|
139
|
+
if (width < 1) return null;
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
key={provider}
|
|
143
|
+
className={`${PROVIDER_COLORS[provider] || 'bg-gray-400'} transition-all`}
|
|
144
|
+
style={{ width: `${width}%` }}
|
|
145
|
+
title={`${PROVIDER_LABELS[provider] || provider}: $${data.costUsd.toFixed(2)}`}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex flex-wrap gap-3 mt-2">
|
|
151
|
+
{providers.map(([provider, data]) => (
|
|
152
|
+
<div key={provider} className="flex items-center gap-1.5 text-xs">
|
|
153
|
+
<span className={`w-2 h-2 rounded-full ${PROVIDER_COLORS[provider] || 'bg-gray-400'}`} />
|
|
154
|
+
<span className="text-gray-600 dark:text-gray-400">
|
|
155
|
+
{PROVIDER_LABELS[provider] || provider}
|
|
156
|
+
</span>
|
|
157
|
+
<span className="text-gray-900 dark:text-white font-medium">
|
|
158
|
+
${data.costUsd.toFixed(2)}
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function ModelTable({ models, type }: { models: (WorkersAIModel | AIGatewayModel)[]; type: 'workersai' | 'gateway' }) {
|
|
168
|
+
const [expanded, setExpanded] = useState(false);
|
|
169
|
+
const displayModels = expanded ? models : models.slice(0, 5);
|
|
170
|
+
const hasMore = models.length > 5;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="overflow-x-auto">
|
|
174
|
+
<table className="w-full text-sm">
|
|
175
|
+
<thead>
|
|
176
|
+
<tr className="text-left text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
|
177
|
+
<th className="pb-2 font-medium">Model</th>
|
|
178
|
+
{type === 'gateway' && <th className="pb-2 font-medium">Provider</th>}
|
|
179
|
+
<th className="pb-2 font-medium text-right">Requests</th>
|
|
180
|
+
<th className="pb-2 font-medium text-right">Tokens</th>
|
|
181
|
+
<th className="pb-2 font-medium text-right">Cost</th>
|
|
182
|
+
</tr>
|
|
183
|
+
</thead>
|
|
184
|
+
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
|
185
|
+
{displayModels.map((m, idx) => {
|
|
186
|
+
const isGateway = 'provider' in m;
|
|
187
|
+
const tokens = isGateway
|
|
188
|
+
? (m as AIGatewayModel).tokensIn + (m as AIGatewayModel).tokensOut
|
|
189
|
+
: (m as WorkersAIModel).inputTokens + (m as WorkersAIModel).outputTokens;
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<tr key={idx} className="text-gray-900 dark:text-white">
|
|
193
|
+
<td className="py-2">
|
|
194
|
+
<code className="text-xs bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
|
|
195
|
+
{formatModelName(m.model)}
|
|
196
|
+
</code>
|
|
197
|
+
</td>
|
|
198
|
+
{type === 'gateway' && (
|
|
199
|
+
<td className="py-2 text-gray-600 dark:text-gray-400">
|
|
200
|
+
{PROVIDER_LABELS[(m as AIGatewayModel).provider] || (m as AIGatewayModel).provider}
|
|
201
|
+
</td>
|
|
202
|
+
)}
|
|
203
|
+
<td className="py-2 text-right font-mono">{formatNumber(m.requests)}</td>
|
|
204
|
+
<td className="py-2 text-right font-mono text-gray-600 dark:text-gray-400">
|
|
205
|
+
{formatNumber(tokens)}
|
|
206
|
+
</td>
|
|
207
|
+
<td className="py-2 text-right font-medium">${m.costUsd.toFixed(2)}</td>
|
|
208
|
+
</tr>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
</tbody>
|
|
212
|
+
</table>
|
|
213
|
+
{hasMore && (
|
|
214
|
+
<button
|
|
215
|
+
onClick={() => setExpanded(!expanded)}
|
|
216
|
+
className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
217
|
+
>
|
|
218
|
+
{expanded ? 'Show less' : `Show ${models.length - 5} more models`}
|
|
219
|
+
</button>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// =============================================================================
|
|
226
|
+
// MAIN COMPONENT
|
|
227
|
+
// =============================================================================
|
|
228
|
+
|
|
229
|
+
export default function AIModelBreakdown({ period }: Props) {
|
|
230
|
+
const [loading, setLoading] = useState(true);
|
|
231
|
+
const [error, setError] = useState<string | null>(null);
|
|
232
|
+
const [data, setData] = useState<ApiResponse | null>(null);
|
|
233
|
+
const [activeTab, setActiveTab] = useState<'gateway' | 'workersai'>('gateway');
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
async function fetchData() {
|
|
237
|
+
setLoading(true);
|
|
238
|
+
setError(null);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const response = await fetch(`/api/usage/ai-models?period=${period}`);
|
|
242
|
+
const result: ApiResponse = await response.json();
|
|
243
|
+
|
|
244
|
+
if (!result.success) {
|
|
245
|
+
throw new Error(result.error || 'Failed to load data');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
setData(result);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
251
|
+
} finally {
|
|
252
|
+
setLoading(false);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fetchData();
|
|
257
|
+
}, [period]);
|
|
258
|
+
|
|
259
|
+
if (loading) {
|
|
260
|
+
return (
|
|
261
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
262
|
+
<h3 className="font-medium text-gray-900 dark:text-white mb-4">AI Model Usage</h3>
|
|
263
|
+
<LoadingState />
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (error) {
|
|
269
|
+
return (
|
|
270
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
271
|
+
<h3 className="font-medium text-gray-900 dark:text-white mb-2">AI Model Usage</h3>
|
|
272
|
+
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const hasGatewayData = (data?.aiGateway?.models?.length ?? 0) > 0;
|
|
278
|
+
const hasWorkersAIData = (data?.workersAI?.models?.length ?? 0) > 0;
|
|
279
|
+
|
|
280
|
+
if (!hasGatewayData && !hasWorkersAIData) {
|
|
281
|
+
return (
|
|
282
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
283
|
+
<h3 className="font-medium text-gray-900 dark:text-white mb-2">AI Model Usage</h3>
|
|
284
|
+
<EmptyState />
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
291
|
+
{/* Header with tabs */}
|
|
292
|
+
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
293
|
+
<div className="flex items-center justify-between p-4 pb-0">
|
|
294
|
+
<h3 className="font-medium text-gray-900 dark:text-white">AI Model Usage</h3>
|
|
295
|
+
<div className="text-sm">
|
|
296
|
+
<span className="text-gray-500 dark:text-gray-400">Total: </span>
|
|
297
|
+
<span className="font-semibold text-gray-900 dark:text-white">
|
|
298
|
+
${((data?.aiGateway?.totalCostUsd ?? 0) + (data?.workersAI?.totalCostUsd ?? 0)).toFixed(2)}
|
|
299
|
+
</span>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{/* Tabs */}
|
|
304
|
+
<div className="flex px-4 mt-3">
|
|
305
|
+
<button
|
|
306
|
+
onClick={() => setActiveTab('gateway')}
|
|
307
|
+
className={`px-3 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
|
308
|
+
activeTab === 'gateway'
|
|
309
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
310
|
+
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
311
|
+
}`}
|
|
312
|
+
>
|
|
313
|
+
AI Gateway
|
|
314
|
+
{hasGatewayData && (
|
|
315
|
+
<span className="ml-1.5 text-xs bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded">
|
|
316
|
+
${data?.aiGateway?.totalCostUsd?.toFixed(2)}
|
|
317
|
+
</span>
|
|
318
|
+
)}
|
|
319
|
+
</button>
|
|
320
|
+
<button
|
|
321
|
+
onClick={() => setActiveTab('workersai')}
|
|
322
|
+
className={`px-3 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
|
323
|
+
activeTab === 'workersai'
|
|
324
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
325
|
+
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
326
|
+
}`}
|
|
327
|
+
>
|
|
328
|
+
Workers AI
|
|
329
|
+
{hasWorkersAIData && (
|
|
330
|
+
<span className="ml-1.5 text-xs bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded">
|
|
331
|
+
${data?.workersAI?.totalCostUsd?.toFixed(2)}
|
|
332
|
+
</span>
|
|
333
|
+
)}
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{/* Content */}
|
|
339
|
+
<div className="p-4">
|
|
340
|
+
{activeTab === 'gateway' && data?.aiGateway && (
|
|
341
|
+
<>
|
|
342
|
+
<ProviderBar
|
|
343
|
+
byProvider={data.aiGateway.byProvider}
|
|
344
|
+
totalCost={data.aiGateway.totalCostUsd}
|
|
345
|
+
/>
|
|
346
|
+
{hasGatewayData ? (
|
|
347
|
+
<ModelTable models={data.aiGateway.models} type="gateway" />
|
|
348
|
+
) : (
|
|
349
|
+
<EmptyState />
|
|
350
|
+
)}
|
|
351
|
+
</>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{activeTab === 'workersai' && data?.workersAI && (
|
|
355
|
+
hasWorkersAIData ? (
|
|
356
|
+
<ModelTable models={data.workersAI.models} type="workersai" />
|
|
357
|
+
) : (
|
|
358
|
+
<EmptyState />
|
|
359
|
+
)
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|