@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.
- package/LICENSE +21 -0
- package/NOTICE +7 -0
- package/README.md +74 -0
- package/client/actions-section.tsx +621 -0
- package/client/analytics-dashboard.tsx +721 -0
- package/client/context-mode-pill.tsx +106 -0
- package/client/context-mode-surface.tsx +414 -0
- package/client/knowledge-section.tsx +780 -0
- package/client/settings-screen.tsx +226 -0
- package/index.client.tsx +43 -0
- package/index.server.ts +54 -0
- package/package.json +72 -0
- package/paseo-plugin.json +5 -0
- package/server/actions.ts +146 -0
- package/server/analytics.ts +424 -0
- package/server/binary.ts +182 -0
- package/server/context-mode-db.ts +481 -0
- package/server/handlers.ts +200 -0
- package/server/integration.ts +441 -0
- package/server/knowledge.ts +187 -0
- package/server/mcp-process.ts +357 -0
- package/server/storage-discovery.ts +225 -0
- package/shared/actions.ts +57 -0
- package/shared/analytics.ts +134 -0
- package/shared/contracts.ts +95 -0
- package/shared/index.ts +5 -0
- package/shared/knowledge.ts +150 -0
- package/shared/settings.ts +51 -0
|
@@ -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
|
+
}
|