@omercnet/paseo-context-mode 0.0.0-next.127.2

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.
@@ -0,0 +1,721 @@
1
+ import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { type ReactNode, useMemo, useState } from "react";
4
+ import { ActivityIndicator, Pressable, Text, View } from "react-native";
5
+ import { getContextModeAnalyticsDashboard } from "../shared/analytics";
6
+
7
+ const integerFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
8
+ const compactFormatter = new Intl.NumberFormat(undefined, {
9
+ maximumFractionDigits: 1,
10
+ notation: "compact",
11
+ });
12
+ const percentFormatter = new Intl.NumberFormat(undefined, {
13
+ maximumFractionDigits: 1,
14
+ minimumFractionDigits: 0,
15
+ });
16
+ const dateFormatter = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" });
17
+ const timestampFormatter = new Intl.DateTimeFormat(undefined, {
18
+ dateStyle: "medium",
19
+ timeStyle: "short",
20
+ });
21
+
22
+ function formatInteger(value: number): string {
23
+ return integerFormatter.format(value);
24
+ }
25
+
26
+ function formatCompact(value: number): string {
27
+ return compactFormatter.format(value);
28
+ }
29
+
30
+ function formatBytes(value: number): string {
31
+ if (value < 1_024) return `${formatInteger(value)} B`;
32
+ if (value < 1_048_576) return `${(value / 1_024).toFixed(1)} KB`;
33
+ if (value < 1_073_741_824) return `${(value / 1_048_576).toFixed(1)} MB`;
34
+ return `${(value / 1_073_741_824).toFixed(1)} GB`;
35
+ }
36
+
37
+ function formatDate(value: string, includeTime = false): string {
38
+ const date = new Date(value);
39
+ if (Number.isNaN(date.getTime())) return value;
40
+ return includeTime ? timestampFormatter.format(date) : dateFormatter.format(date);
41
+ }
42
+
43
+ function warningText(warning: unknown): string {
44
+ if (typeof warning === "string") return warning;
45
+ if (!warning || typeof warning !== "object") return String(warning);
46
+ const record = warning as Record<string, unknown>;
47
+ const message = typeof record.message === "string" ? record.message : null;
48
+ const code = typeof record.code === "string" ? record.code : null;
49
+ if (code && message) return `${code}: ${message}`;
50
+ if (message) return message;
51
+ return JSON.stringify(warning) ?? String(warning);
52
+ }
53
+
54
+ function optionalFallback(value: unknown): string | null {
55
+ if (!value || typeof value !== "object") return null;
56
+ const record = value as Record<string, unknown>;
57
+ const fallback =
58
+ record.raw ?? record.fallback ?? record.rawReport ?? record.fallbackReport ?? null;
59
+ if (typeof fallback === "string") return fallback.trim() || null;
60
+ if (fallback === null || fallback === undefined) return null;
61
+ try {
62
+ return JSON.stringify(fallback, null, 2) ?? String(fallback);
63
+ } catch {
64
+ return String(fallback);
65
+ }
66
+ }
67
+ interface AnalyticsDashboardProps extends Pick<PluginSurfaceProps, "theme" | "host" | "layout"> {
68
+ fallback?: string | null;
69
+ health?: ReactNode;
70
+ display?: "full" | "hero" | "details";
71
+ }
72
+ export function AnalyticsDashboard({
73
+ theme,
74
+ host,
75
+ layout,
76
+ fallback,
77
+ health,
78
+ display = "full",
79
+ }: AnalyticsDashboardProps) {
80
+ const showHero = display !== "details";
81
+ const showDetails = display !== "hero";
82
+ const loadDashboard = useRpc(getContextModeAnalyticsDashboard);
83
+ const [rawExpanded, setRawExpanded] = useState(false);
84
+ const [sourceLimit, setSourceLimit] = useState(20);
85
+ const [detailsExpanded, setDetailsExpanded] = useState(false);
86
+ const [warningsExpanded, setWarningsExpanded] = useState(false);
87
+ const [dayLimit, setDayLimit] = useState(14);
88
+ const dashboard = useQuery({
89
+ queryKey: ["context-mode", host.id, "analytics-dashboard"],
90
+ queryFn: () => loadDashboard({}),
91
+ });
92
+ const styles = useMemo(
93
+ () => ({
94
+ root: { gap: layout.compact ? 12 : 16 },
95
+ centered: {
96
+ minHeight: 180,
97
+ alignItems: "center" as const,
98
+ justifyContent: "center" as const,
99
+ gap: 10,
100
+ },
101
+ section: { gap: 8 },
102
+ sectionTitle: {
103
+ color: theme.colors.foreground,
104
+ fontSize: 15,
105
+ fontWeight: "700" as const,
106
+ },
107
+ grow: { flex: 1 },
108
+ card: {
109
+ gap: 10,
110
+ borderWidth: 1,
111
+ borderColor: theme.colors.border,
112
+ borderRadius: 12,
113
+ padding: layout.compact ? 12 : 16,
114
+ backgroundColor: theme.colors.surface1,
115
+ },
116
+ hero: {
117
+ gap: 5,
118
+ borderWidth: 1,
119
+ borderColor: theme.colors.border,
120
+ borderRadius: 14,
121
+ padding: layout.compact ? 14 : 20,
122
+ backgroundColor: theme.colors.surface1,
123
+ },
124
+ eyebrow: {
125
+ color: theme.colors.accent,
126
+ fontSize: 11,
127
+ fontWeight: "700" as const,
128
+ letterSpacing: 0.7,
129
+ textTransform: "uppercase" as const,
130
+ },
131
+ headline: {
132
+ color: theme.colors.foreground,
133
+ fontSize: layout.compact ? 28 : 36,
134
+ fontWeight: "800" as const,
135
+ lineHeight: layout.compact ? 32 : 40,
136
+ },
137
+ subtitle: { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 18 },
138
+ metricGrid: {
139
+ flexDirection: "row" as const,
140
+ flexWrap: "wrap" as const,
141
+ gap: 8,
142
+ },
143
+ metric: {
144
+ minWidth: layout.compact ? 110 : 138,
145
+ flexBasis: layout.compact ? 110 : 138,
146
+ flexGrow: 1,
147
+ gap: 2,
148
+ borderWidth: 1,
149
+ borderColor: theme.colors.border,
150
+ borderRadius: 10,
151
+ paddingHorizontal: 11,
152
+ paddingVertical: 9,
153
+ backgroundColor: theme.colors.surface1,
154
+ },
155
+ metricValue: {
156
+ color: theme.colors.foreground,
157
+ fontSize: 18,
158
+ fontWeight: "700" as const,
159
+ },
160
+ label: { color: theme.colors.foregroundMuted, fontSize: 11 },
161
+ row: {
162
+ flexDirection: "row" as const,
163
+ alignItems: "center" as const,
164
+ gap: 8,
165
+ },
166
+ listRow: {
167
+ flexDirection: "row" as const,
168
+ alignItems: "center" as const,
169
+ gap: 8,
170
+ minHeight: 30,
171
+ },
172
+ rowTitle: {
173
+ color: theme.colors.foreground,
174
+ fontSize: 13,
175
+ fontWeight: "600" as const,
176
+ },
177
+ rowValue: { color: theme.colors.foreground, fontSize: 12 },
178
+ divider: { height: 1, backgroundColor: theme.colors.border },
179
+ badge: {
180
+ borderRadius: 999,
181
+ paddingHorizontal: 8,
182
+ paddingVertical: 3,
183
+ backgroundColor: theme.colors.surface2,
184
+ },
185
+ badgeText: { fontSize: 11, fontWeight: "700" as const },
186
+ success: { color: theme.colors.statusSuccess },
187
+ warning: { color: theme.colors.statusWarning },
188
+ danger: { color: theme.colors.statusDanger },
189
+ warningCard: {
190
+ gap: 6,
191
+ borderWidth: 1,
192
+ borderColor: theme.colors.statusWarning,
193
+ borderRadius: 10,
194
+ padding: 11,
195
+ backgroundColor: theme.colors.surface1,
196
+ },
197
+ timelineRow: {
198
+ flexDirection: "row" as const,
199
+ alignItems: "center" as const,
200
+ gap: 9,
201
+ minHeight: 30,
202
+ },
203
+ timelineDate: { color: theme.colors.foregroundMuted, fontSize: 11, width: 48 },
204
+ timelineTrack: {
205
+ flex: 1,
206
+ height: 7,
207
+ borderRadius: 999,
208
+ overflow: "hidden" as const,
209
+ backgroundColor: theme.colors.surface2,
210
+ },
211
+ timelineFill: {
212
+ height: "100%" as const,
213
+ minWidth: 2,
214
+ borderRadius: 999,
215
+ backgroundColor: theme.colors.accent,
216
+ },
217
+ timelineValue: {
218
+ color: theme.colors.foreground,
219
+ fontSize: 11,
220
+ width: layout.compact ? 58 : 76,
221
+ textAlign: "right" as const,
222
+ },
223
+ sourceMeta: {
224
+ color: theme.colors.foregroundMuted,
225
+ fontSize: 11,
226
+ textAlign: "right" as const,
227
+ },
228
+ button: {
229
+ minHeight: 36,
230
+ alignItems: "center" as const,
231
+ justifyContent: "center" as const,
232
+ borderRadius: 8,
233
+ paddingHorizontal: 12,
234
+ paddingVertical: 8,
235
+ backgroundColor: theme.colors.surface2,
236
+ },
237
+ buttonText: { color: theme.colors.foreground, fontWeight: "600" as const },
238
+ raw: {
239
+ color: theme.colors.foreground,
240
+ fontFamily: "monospace",
241
+ fontSize: layout.compact ? 10 : 11,
242
+ lineHeight: layout.compact ? 15 : 17,
243
+ },
244
+ }),
245
+ [layout.compact, theme],
246
+ );
247
+ if (dashboard.isPending) {
248
+ return (
249
+ <View style={styles.root}>
250
+ {showHero ? health : null}
251
+ <View
252
+ accessible
253
+ accessibilityLabel="Loading lifetime Context Mode analytics"
254
+ style={styles.centered}
255
+ >
256
+ <ActivityIndicator color={theme.colors.accent} />
257
+ <Text style={styles.subtitle}>Loading lifetime impact…</Text>
258
+ </View>
259
+ </View>
260
+ );
261
+ }
262
+
263
+ if (dashboard.error || !dashboard.data) {
264
+ const message = dashboard.error
265
+ ? dashboard.error instanceof Error
266
+ ? dashboard.error.message
267
+ : String(dashboard.error)
268
+ : "Context Mode returned no analytics data.";
269
+ return (
270
+ <View style={styles.root}>
271
+ {showHero ? health : null}
272
+ <View style={styles.card}>
273
+ <Text accessibilityRole="alert" style={styles.danger}>
274
+ {message}
275
+ </Text>
276
+ <Pressable
277
+ accessibilityLabel="Retry loading Context Mode analytics"
278
+ accessibilityRole="button"
279
+ disabled={dashboard.isFetching}
280
+ onPress={() => dashboard.refetch()}
281
+ style={styles.button}
282
+ >
283
+ <Text style={styles.buttonText}>
284
+ {dashboard.isFetching ? "Retrying…" : "Try again"}
285
+ </Text>
286
+ </Pressable>
287
+ </View>
288
+ </View>
289
+ );
290
+ }
291
+
292
+ const data = dashboard.data;
293
+ const visibleDays = data.byDay.slice(-dayLimit).reverse();
294
+ const maxDailySavings = Math.max(1, ...visibleDays.map((day) => day.savedTokens));
295
+ const rawFallback = fallback?.trim() || optionalFallback(data);
296
+ const isEmpty =
297
+ data.totals.sessions === 0 &&
298
+ data.totals.toolCalls === 0 &&
299
+ data.byProvider.length === 0 &&
300
+ data.byCategory.length === 0 &&
301
+ data.byDay.length === 0 &&
302
+ data.sources.length === 0;
303
+ const visibleSources = data.sources.slice(0, sourceLimit);
304
+ const completenessLabel =
305
+ data.completeness === "complete"
306
+ ? "Complete"
307
+ : data.completeness === "partial"
308
+ ? "Partial"
309
+ : "Unsupported";
310
+ const completenessStyle =
311
+ data.completeness === "complete"
312
+ ? styles.success
313
+ : data.completeness === "partial"
314
+ ? styles.warning
315
+ : styles.danger;
316
+
317
+ return (
318
+ <View style={styles.root}>
319
+ {showHero ? (
320
+ <>
321
+ <View
322
+ accessible
323
+ accessibilityLabel={`${formatInteger(data.totals.savedTokens)} lifetime tokens saved, ${percentFormatter.format(data.totals.savingsPercent)} percent reduction`}
324
+ style={styles.hero}
325
+ >
326
+ <View style={styles.row}>
327
+ <Text style={styles.eyebrow}>Lifetime impact</Text>
328
+ <View style={styles.grow} />
329
+ {data.completeness === "complete" ? null : (
330
+ <View style={styles.badge}>
331
+ <Text style={[styles.badgeText, completenessStyle]}>{completenessLabel}</Text>
332
+ </View>
333
+ )}
334
+ </View>
335
+ <Text style={styles.headline}>{formatCompact(data.totals.savedTokens)}</Text>
336
+ <Text style={styles.sectionTitle}>tokens saved</Text>
337
+ <Text style={styles.subtitle}>
338
+ {percentFormatter.format(data.totals.savingsPercent)}% less context returned across{" "}
339
+ {formatInteger(data.totals.sessions)} sessions
340
+ </Text>
341
+ </View>
342
+
343
+ {health}
344
+
345
+ {data.completeness !== "complete" || data.warnings.length > 0 ? (
346
+ <View accessibilityRole="alert" style={styles.warningCard}>
347
+ <View style={styles.row}>
348
+ <Text style={[styles.rowTitle, completenessStyle]}>
349
+ {data.completeness === "unsupported"
350
+ ? "Analytics unavailable"
351
+ : data.completeness === "partial"
352
+ ? "Some analytics are partial"
353
+ : "Analytics notice"}
354
+ </Text>
355
+ <View style={styles.grow} />
356
+ {data.warnings.length > 0 ? (
357
+ <Pressable
358
+ accessibilityLabel={
359
+ warningsExpanded ? "Hide analytics warnings" : "Show analytics warnings"
360
+ }
361
+ accessibilityRole="button"
362
+ accessibilityState={{ expanded: warningsExpanded }}
363
+ onPress={() => setWarningsExpanded((expanded) => !expanded)}
364
+ style={styles.button}
365
+ >
366
+ <Text style={styles.buttonText}>
367
+ {warningsExpanded ? "Hide" : `${data.warnings.length} details`}
368
+ </Text>
369
+ </Pressable>
370
+ ) : null}
371
+ </View>
372
+ {warningsExpanded
373
+ ? data.warnings.map((warning) => {
374
+ const text = warningText(warning);
375
+ return (
376
+ <Text
377
+ key={`${warning.code}:${warning.provider ?? ""}:${warning.database ?? ""}:${warning.message}`}
378
+ style={styles.subtitle}
379
+ >
380
+ {text}
381
+ </Text>
382
+ );
383
+ })
384
+ : null}
385
+ </View>
386
+ ) : null}
387
+ </>
388
+ ) : null}
389
+
390
+ {showDetails ? (
391
+ isEmpty ? (
392
+ <View style={styles.card}>
393
+ <Text style={styles.sectionTitle}>
394
+ {data.completeness === "unsupported" ? "Analytics unavailable" : "No activity yet"}
395
+ </Text>
396
+ <Text style={styles.subtitle}>
397
+ {data.completeness === "unsupported"
398
+ ? "Update Context Mode to a supported analytics schema to populate this dashboard."
399
+ : "Lifetime metrics will appear after Context Mode records tool activity or indexes a source."}
400
+ </Text>
401
+ </View>
402
+ ) : (
403
+ <>
404
+ <View style={styles.section}>
405
+ <Text style={styles.sectionTitle}>At a glance</Text>
406
+ <View style={styles.metricGrid}>
407
+ <Metric
408
+ label="Providers"
409
+ value={formatInteger(data.totals.providers)}
410
+ styles={styles}
411
+ />
412
+ <Metric
413
+ label="Sessions"
414
+ value={formatInteger(data.totals.sessions)}
415
+ styles={styles}
416
+ />
417
+ <Metric
418
+ label="Tool calls"
419
+ value={formatInteger(data.totals.toolCalls)}
420
+ styles={styles}
421
+ />
422
+ <Metric
423
+ label="Indexed sources"
424
+ value={formatInteger(data.totals.indexedSources)}
425
+ styles={styles}
426
+ />
427
+ </View>
428
+ <Pressable
429
+ accessibilityLabel={
430
+ detailsExpanded ? "Hide analytics details" : "Show analytics details"
431
+ }
432
+ accessibilityRole="button"
433
+ accessibilityState={{ expanded: detailsExpanded }}
434
+ onPress={() => setDetailsExpanded((expanded) => !expanded)}
435
+ style={styles.button}
436
+ >
437
+ <Text style={styles.buttonText}>
438
+ {detailsExpanded ? "Hide detail" : "More detail"}
439
+ </Text>
440
+ </Pressable>
441
+ {detailsExpanded ? (
442
+ <>
443
+ <View style={styles.metricGrid}>
444
+ <Metric
445
+ label="Databases"
446
+ value={formatInteger(data.totals.databases)}
447
+ styles={styles}
448
+ />
449
+ <Metric
450
+ label="Projects"
451
+ value={formatInteger(data.totals.projects)}
452
+ styles={styles}
453
+ />
454
+ <Metric
455
+ label="Events"
456
+ value={formatInteger(data.totals.events)}
457
+ styles={styles}
458
+ />
459
+ <Metric
460
+ label="Input tokens"
461
+ value={formatCompact(data.totals.inputTokens)}
462
+ styles={styles}
463
+ />
464
+ <Metric
465
+ label="Output tokens"
466
+ value={formatCompact(data.totals.outputTokens)}
467
+ styles={styles}
468
+ />
469
+ <Metric
470
+ label="Indexed chunks"
471
+ value={formatInteger(data.totals.indexedChunks)}
472
+ styles={styles}
473
+ />
474
+ </View>
475
+ <View style={styles.card}>
476
+ <Text style={styles.rowTitle}>Byte accounting</Text>
477
+ <Text style={styles.subtitle}>
478
+ {formatBytes(data.totals.byteAccounting.savedBytes)} saved ·{" "}
479
+ {formatBytes(data.totals.byteAccounting.returnedBytes)} returned ·{" "}
480
+ {formatBytes(data.totals.byteAccounting.avoidedBytes)} avoided
481
+ </Text>
482
+ <Text style={styles.label}>
483
+ {formatBytes(data.totals.byteAccounting.eventDataBytes)} event data ·{" "}
484
+ {formatBytes(data.totals.byteAccounting.snapshotBytes)} snapshots ·{" "}
485
+ {formatBytes(data.totals.byteAccounting.indexedBytes)} indexed
486
+ </Text>
487
+ </View>
488
+ </>
489
+ ) : null}
490
+ </View>
491
+
492
+ <View style={styles.section}>
493
+ <Text style={styles.sectionTitle}>Savings over time</Text>
494
+ <View style={styles.card}>
495
+ {data.byDay.length === 0 ? (
496
+ <Text style={styles.subtitle}>No dated activity is available.</Text>
497
+ ) : (
498
+ visibleDays.map((day) => {
499
+ const formattedDate = formatDate(day.date);
500
+ const width =
501
+ `${Math.max(2, Math.round((day.savedTokens / maxDailySavings) * 100))}%` as `${number}%`;
502
+ return (
503
+ <View
504
+ accessible
505
+ accessibilityLabel={`${formattedDate}: ${formatInteger(day.calls)} calls, ${formatInteger(day.savedTokens)} tokens saved`}
506
+ key={day.date}
507
+ style={styles.timelineRow}
508
+ >
509
+ <Text style={styles.timelineDate}>{formattedDate}</Text>
510
+ <View style={styles.timelineTrack}>
511
+ <View style={[styles.timelineFill, { width }]} />
512
+ </View>
513
+ <Text style={styles.timelineValue}>{formatCompact(day.savedTokens)}</Text>
514
+ </View>
515
+ );
516
+ })
517
+ )}
518
+ {data.byDay.length > visibleDays.length ? (
519
+ <Pressable
520
+ accessibilityLabel={`Show more savings history, ${data.byDay.length - visibleDays.length} days remaining`}
521
+ accessibilityRole="button"
522
+ onPress={() =>
523
+ setDayLimit((current) => Math.min(data.byDay.length, current + 30))
524
+ }
525
+ style={styles.button}
526
+ >
527
+ <Text style={styles.buttonText}>Show more history</Text>
528
+ </Pressable>
529
+ ) : dayLimit > 14 && data.byDay.length > 14 ? (
530
+ <Pressable
531
+ accessibilityLabel="Show recent savings history only"
532
+ accessibilityRole="button"
533
+ onPress={() => setDayLimit(14)}
534
+ style={styles.button}
535
+ >
536
+ <Text style={styles.buttonText}>Show recent 14 days</Text>
537
+ </Pressable>
538
+ ) : null}
539
+ </View>
540
+ </View>
541
+ <View style={styles.section}>
542
+ <Text style={styles.sectionTitle}>Providers</Text>
543
+ <View style={styles.card}>
544
+ {data.byProvider.length === 0 ? (
545
+ <Text style={styles.subtitle}>No provider-attributed activity is available.</Text>
546
+ ) : (
547
+ data.byProvider.map((provider, index) => (
548
+ <View key={provider.provider}>
549
+ {index > 0 ? <View style={styles.divider} /> : null}
550
+ <View
551
+ accessible
552
+ accessibilityLabel={`${provider.provider}: ${formatInteger(provider.projects)} projects, ${formatInteger(provider.sessions)} sessions, ${formatInteger(provider.toolCalls)} tool calls, ${formatInteger(provider.savedTokens)} tokens saved, ${percentFormatter.format(provider.savingsPercent)} percent reduction`}
553
+ style={styles.listRow}
554
+ >
555
+ <View style={styles.grow}>
556
+ <Text style={styles.rowTitle}>{provider.provider}</Text>
557
+ <Text style={styles.label}>
558
+ {formatInteger(provider.projects)} projects ·{" "}
559
+ {formatInteger(provider.sessions)} sessions ·{" "}
560
+ {formatInteger(provider.toolCalls)} calls
561
+ </Text>
562
+ </View>
563
+ <View>
564
+ <Text style={styles.rowValue}>
565
+ {formatCompact(provider.savedTokens)} saved
566
+ </Text>
567
+ <Text style={styles.sourceMeta}>
568
+ {percentFormatter.format(provider.savingsPercent)}% reduction
569
+ </Text>
570
+ </View>
571
+ </View>
572
+ </View>
573
+ ))
574
+ )}
575
+ </View>
576
+ </View>
577
+
578
+ <View style={styles.section}>
579
+ <Text style={styles.sectionTitle}>Where savings come from</Text>
580
+ <View style={styles.card}>
581
+ {data.byCategory.length === 0 ? (
582
+ <Text style={styles.subtitle}>No category breakdown is available.</Text>
583
+ ) : (
584
+ data.byCategory.map((category, index) => (
585
+ <View key={category.category}>
586
+ {index > 0 ? <View style={styles.divider} /> : null}
587
+ <View
588
+ accessible
589
+ accessibilityLabel={`${category.category}: ${formatInteger(category.count)} events, ${formatInteger(category.savedTokens)} tokens saved`}
590
+ style={styles.listRow}
591
+ >
592
+ <Text style={[styles.rowTitle, styles.grow]}>{category.category}</Text>
593
+ <Text style={styles.label}>{formatInteger(category.count)} events</Text>
594
+ <Text style={styles.rowValue}>
595
+ {formatCompact(category.savedTokens)} saved
596
+ </Text>
597
+ </View>
598
+ </View>
599
+ ))
600
+ )}
601
+ </View>
602
+ </View>
603
+
604
+ <View style={styles.section}>
605
+ <Text style={styles.sectionTitle}>Knowledge sources</Text>
606
+ <View style={styles.card}>
607
+ {data.sources.length === 0 ? (
608
+ <Text style={styles.subtitle}>No indexed sources were found.</Text>
609
+ ) : (
610
+ visibleSources.map((source, index) => {
611
+ const stateStyle =
612
+ source.state === "ready"
613
+ ? styles.success
614
+ : source.state === "empty"
615
+ ? styles.warning
616
+ : styles.danger;
617
+ return (
618
+ <View key={`${source.provider}:${source.source}`}>
619
+ {index > 0 ? <View style={styles.divider} /> : null}
620
+ <View
621
+ accessible
622
+ accessibilityLabel={`${source.source} from ${source.provider}: ${formatInteger(source.chunks)} chunks, ${formatInteger(source.codeChunks)} code chunks, ${source.state}`}
623
+ style={styles.listRow}
624
+ >
625
+ <View style={styles.grow}>
626
+ <Text style={styles.rowTitle}>{source.source}</Text>
627
+ <Text style={styles.label}>
628
+ {source.provider} · {formatBytes(source.indexedBytes)}
629
+ {source.indexedAt ? ` · ${formatDate(source.indexedAt, true)}` : ""}
630
+ </Text>
631
+ {source.detail ? (
632
+ <Text style={styles.label}>{source.detail}</Text>
633
+ ) : null}
634
+ </View>
635
+ <View>
636
+ <Text style={[styles.badgeText, stateStyle]}>{source.state}</Text>
637
+ <Text style={styles.sourceMeta}>
638
+ {formatInteger(source.chunks)} chunks ·{" "}
639
+ {formatInteger(source.codeChunks)} code
640
+ </Text>
641
+ </View>
642
+ </View>
643
+ </View>
644
+ );
645
+ })
646
+ )}
647
+ {data.sources.length > visibleSources.length ? (
648
+ <Pressable
649
+ accessibilityLabel={`Show more indexed sources, ${data.sources.length - visibleSources.length} remaining`}
650
+ accessibilityRole="button"
651
+ onPress={() =>
652
+ setSourceLimit((current) => Math.min(data.sources.length, current + 100))
653
+ }
654
+ style={styles.button}
655
+ >
656
+ <Text style={styles.buttonText}>
657
+ Show next {Math.min(100, data.sources.length - visibleSources.length)} sources
658
+ </Text>
659
+ </Pressable>
660
+ ) : sourceLimit > 20 && data.sources.length > 20 ? (
661
+ <Pressable
662
+ accessibilityLabel="Collapse indexed sources"
663
+ accessibilityRole="button"
664
+ onPress={() => setSourceLimit(20)}
665
+ style={styles.button}
666
+ >
667
+ <Text style={styles.buttonText}>Show first 20 sources</Text>
668
+ </Pressable>
669
+ ) : null}
670
+ </View>
671
+ </View>
672
+ </>
673
+ )
674
+ ) : null}
675
+
676
+ {showDetails && rawFallback ? (
677
+ <View style={styles.section}>
678
+ <Pressable
679
+ accessibilityLabel={`${rawExpanded ? "Hide" : "Show"} raw analytics fallback`}
680
+ accessibilityRole="button"
681
+ accessibilityState={{ expanded: rawExpanded }}
682
+ onPress={() => setRawExpanded((expanded) => !expanded)}
683
+ style={styles.button}
684
+ >
685
+ <Text style={styles.buttonText}>
686
+ {rawExpanded ? "Hide raw fallback" : "Show raw fallback"}
687
+ </Text>
688
+ </Pressable>
689
+ {rawExpanded ? (
690
+ <View style={styles.card}>
691
+ <Text selectable style={styles.raw}>
692
+ {rawFallback}
693
+ </Text>
694
+ </View>
695
+ ) : null}
696
+ </View>
697
+ ) : null}
698
+ </View>
699
+ );
700
+ }
701
+
702
+ function Metric({
703
+ label,
704
+ value,
705
+ styles,
706
+ }: {
707
+ label: string;
708
+ value: string;
709
+ styles: {
710
+ metric: object;
711
+ metricValue: object;
712
+ label: object;
713
+ };
714
+ }) {
715
+ return (
716
+ <View accessible accessibilityLabel={`${label}: ${value}`} style={styles.metric}>
717
+ <Text style={styles.metricValue}>{value}</Text>
718
+ <Text style={styles.label}>{label}</Text>
719
+ </View>
720
+ );
721
+ }