@pyric/ui 0.1.0-alpha.11 → 0.1.0-alpha.13
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/package.json +5 -4
- package/src/agents/ContextWindowUsage.tsx +514 -0
- package/src/agents/EmptyState.tsx +27 -0
- package/src/agents/Fold.tsx +64 -0
- package/src/agents/Modal.tsx +65 -0
- package/src/agents/PulsingDot.tsx +28 -0
- package/src/agents/inbrowser-agent-usage.d.ts +141 -0
- package/src/agents/index.ts +28 -0
- package/src/auth/authApi.ts +72 -0
- package/src/auth/claims.ts +63 -0
- package/src/auth/components/AuthProviderToggles.tsx +147 -0
- package/src/auth/components/AuthSignInHelper.tsx +197 -0
- package/src/auth/components/AuthUserForm.tsx +328 -0
- package/src/auth/components/AuthUserList.tsx +219 -0
- package/src/auth/components/ClaimsField.tsx +50 -0
- package/src/auth/components/confirmActions.tsx +114 -0
- package/src/auth/controller.ts +173 -0
- package/src/auth/hooks/index.ts +36 -0
- package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
- package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
- package/src/auth/hooks/useAuthUserEditor.ts +76 -0
- package/src/auth/hooks/useAuthUsers.ts +154 -0
- package/src/auth/index.ts +42 -0
- package/src/auth/providers.ts +28 -0
- package/src/auth/reducers/userEditor.ts +186 -0
- package/src/events/components/ActivityActionItems.tsx +136 -0
- package/src/events/components/ActivityGrid.tsx +197 -0
- package/src/events/components/ActivityGridRow.tsx +65 -0
- package/src/events/components/ProposedChangeDiff.tsx +175 -0
- package/src/events/components/format.ts +21 -0
- package/src/events/components/index.ts +17 -0
- package/src/events/digest.ts +630 -0
- package/src/events/hooks/index.ts +9 -0
- package/src/events/hooks/useActivityDigest.ts +42 -0
- package/src/events/hooks/useActivityStream.ts +86 -0
- package/src/events/index.ts +39 -0
- package/src/events/types.ts +152 -0
- package/src/firestore/components/CollectionList.tsx +78 -0
- package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
- package/src/firestore/components/DocumentEditor.tsx +350 -0
- package/src/firestore/components/DocumentList.tsx +217 -0
- package/src/firestore/components/DocumentPreview.tsx +265 -0
- package/src/firestore/components/FieldRenderer.tsx +25 -0
- package/src/firestore/components/QueryBuilder.tsx +181 -0
- package/src/firestore/components/ReferencePicker.tsx +212 -0
- package/src/firestore/components/TreeEntry.tsx +58 -0
- package/src/firestore/components/context.ts +25 -0
- package/src/firestore/fieldEditors/array.tsx +30 -0
- package/src/firestore/fieldEditors/boolean.tsx +39 -0
- package/src/firestore/fieldEditors/bytes.tsx +53 -0
- package/src/firestore/fieldEditors/geopoint.tsx +71 -0
- package/src/firestore/fieldEditors/map.tsx +33 -0
- package/src/firestore/fieldEditors/null.tsx +27 -0
- package/src/firestore/fieldEditors/number.tsx +43 -0
- package/src/firestore/fieldEditors/reference.tsx +87 -0
- package/src/firestore/fieldEditors/registry.ts +45 -0
- package/src/firestore/fieldEditors/string.tsx +33 -0
- package/src/firestore/fieldEditors/timestamp.tsx +75 -0
- package/src/firestore/fieldEditors/types.ts +68 -0
- package/src/firestore/fieldEditors/vector.tsx +142 -0
- package/src/firestore/firestoreApi.ts +86 -0
- package/src/firestore/hooks/coerceError.ts +34 -0
- package/src/firestore/hooks/index.ts +46 -0
- package/src/firestore/hooks/useCollectionList.ts +102 -0
- package/src/firestore/hooks/useDocumentEditor.ts +161 -0
- package/src/firestore/hooks/useDocumentList.ts +242 -0
- package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
- package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
- package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
- package/src/firestore/hooks/useQueryBuilder.ts +188 -0
- package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
- package/src/firestore/hooks/useReferencePicker.ts +228 -0
- package/src/firestore/import/parseImport.ts +137 -0
- package/src/firestore/index.ts +87 -0
- package/src/firestore/reducers/defaults.ts +38 -0
- package/src/firestore/reducers/documentEditor.ts +239 -0
- package/src/firestore/reducers/tree.ts +140 -0
- package/src/firestore/reducers/types.ts +92 -0
- package/src/firestore/reducers/validation.ts +129 -0
- package/src/firestore/types.ts +231 -0
- package/src/firestore/validation/ids.ts +45 -0
- package/src/firestore/valueEquality.ts +43 -0
- package/src/index.ts +10 -0
- package/src/primitives/Badge.tsx +40 -0
- package/src/primitives/ConfirmDialog.tsx +137 -0
- package/src/primitives/CopyButton.tsx +62 -0
- package/src/primitives/JsonView.tsx +151 -0
- package/src/primitives/SegmentedControl.tsx +72 -0
- package/src/primitives/Toast.tsx +161 -0
- package/src/primitives/VirtualList.tsx +104 -0
- package/src/primitives/hooks/useContainerSize.ts +53 -0
- package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
- package/src/primitives/index.ts +39 -0
- package/src/primitives/useConfirm.tsx +113 -0
- package/src/rtdb/components/RtdbPathBar.tsx +135 -0
- package/src/rtdb/components/RtdbTree.tsx +409 -0
- package/src/rtdb/editor.ts +79 -0
- package/src/rtdb/hooks/useRtdbTree.ts +137 -0
- package/src/rtdb/index.ts +66 -0
- package/src/rtdb/pathInput.ts +47 -0
- package/src/rtdb/reducers/tree.ts +191 -0
- package/src/rtdb/rtdbApi.ts +23 -0
- package/src/rtdb/values.ts +188 -0
- package/src/rules/components/DenialInspector.tsx +227 -0
- package/src/rules/components/format.ts +171 -0
- package/src/rules/components/index.ts +12 -0
- package/src/rules/components/scope.ts +75 -0
- package/src/rules/hooks/index.ts +5 -0
- package/src/rules/hooks/useDenialTrace.ts +100 -0
- package/src/rules/index.ts +24 -0
- package/src/rules/types.ts +91 -0
- package/src/storage/collisionRename.ts +114 -0
- package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
- package/src/storage/components/ObjectBrowser.tsx +219 -0
- package/src/storage/components/ObjectInspector.tsx +169 -0
- package/src/storage/components/PathBreadcrumb.tsx +84 -0
- package/src/storage/components/UploadDropzone.tsx +182 -0
- package/src/storage/folderPlaceholder.ts +40 -0
- package/src/storage/hooks/index.ts +59 -0
- package/src/storage/hooks/useMetadataEditor.ts +329 -0
- package/src/storage/hooks/useObjectUpload.ts +262 -0
- package/src/storage/hooks/usePathState.ts +94 -0
- package/src/storage/hooks/useStorageDelete.ts +195 -0
- package/src/storage/hooks/useStorageList.ts +261 -0
- package/src/storage/hooks/useStorageObject.ts +162 -0
- package/src/storage/hooks/useStorageRulesGate.ts +270 -0
- package/src/storage/hooks/useStorageSelection.ts +90 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/pendingPrefixes.ts +125 -0
- package/src/storage/previews.tsx +120 -0
- package/src/storage/storageApi.ts +54 -0
- package/src/traffic/components/RuleHeatmap.tsx +97 -0
- package/src/traffic/components/TrafficDetail.tsx +160 -0
- package/src/traffic/components/TrafficGroupRow.tsx +91 -0
- package/src/traffic/components/TrafficLineChart.tsx +139 -0
- package/src/traffic/components/TrafficLog.tsx +175 -0
- package/src/traffic/components/TrafficMetricCards.tsx +77 -0
- package/src/traffic/components/TrafficRow.tsx +69 -0
- package/src/traffic/components/TrafficStats.tsx +73 -0
- package/src/traffic/components/TrafficTimeline.tsx +289 -0
- package/src/traffic/components/format.ts +22 -0
- package/src/traffic/components/index.ts +22 -0
- package/src/traffic/hooks/index.ts +60 -0
- package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
- package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
- package/src/traffic/hooks/useTrafficFilter.ts +74 -0
- package/src/traffic/hooks/useTrafficGroups.ts +126 -0
- package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
- package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
- package/src/traffic/hooks/useTrafficStats.ts +77 -0
- package/src/traffic/index.ts +13 -0
- package/src/traffic/types.ts +85 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyric/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.13",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://pyric.dev",
|
|
6
6
|
"repository": {
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
},
|
|
78
78
|
"files": [
|
|
79
79
|
"dist",
|
|
80
|
+
"src",
|
|
80
81
|
"README.md",
|
|
81
82
|
"LICENSE"
|
|
82
83
|
],
|
|
@@ -92,7 +93,7 @@
|
|
|
92
93
|
"react": ">=19",
|
|
93
94
|
"react-dom": ">=19",
|
|
94
95
|
"firebase": ">=12",
|
|
95
|
-
"pyric": "^0.1.0-alpha.
|
|
96
|
+
"pyric": "^0.1.0-alpha.13"
|
|
96
97
|
},
|
|
97
98
|
"peerDependenciesMeta": {
|
|
98
99
|
"@inbrowser/agent": {
|
|
@@ -108,8 +109,8 @@
|
|
|
108
109
|
"fake-indexeddb": "^6.0.0",
|
|
109
110
|
"firebase": "^12.12.0",
|
|
110
111
|
"jsdom": "^29.1.1",
|
|
111
|
-
"pyric": "^0.1.0-alpha.
|
|
112
|
-
"pyric-admin": "^0.1.0-alpha.
|
|
112
|
+
"pyric": "^0.1.0-alpha.13",
|
|
113
|
+
"pyric-admin": "^0.1.0-alpha.13",
|
|
113
114
|
"react": "^19.0.0",
|
|
114
115
|
"react-dom": "^19.0.0",
|
|
115
116
|
"react-test-renderer": "^19.2.6",
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
ContextWindowSnapshot,
|
|
4
|
+
SessionRequestUsage,
|
|
5
|
+
SessionTokenUsage,
|
|
6
|
+
} from '@inbrowser/agent/usage';
|
|
7
|
+
|
|
8
|
+
type ClassSlots = {
|
|
9
|
+
root?: string;
|
|
10
|
+
header?: string;
|
|
11
|
+
body?: string;
|
|
12
|
+
row?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
bar?: string;
|
|
16
|
+
segment?: string;
|
|
17
|
+
empty?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface ContextWindowRingProps {
|
|
21
|
+
snapshot: ContextWindowSnapshot;
|
|
22
|
+
size?: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
innerClassName?: string;
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ContextWindowMeterProps {
|
|
29
|
+
snapshot: ContextWindowSnapshot;
|
|
30
|
+
onOpen?: () => void;
|
|
31
|
+
className?: string;
|
|
32
|
+
buttonClassName?: string;
|
|
33
|
+
tooltipClassName?: string;
|
|
34
|
+
ringClassName?: string;
|
|
35
|
+
ringInnerClassName?: string;
|
|
36
|
+
formatTokens?: (tokens: number) => string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TokenUsageInlineProps {
|
|
40
|
+
usage?: SessionTokenUsage;
|
|
41
|
+
className?: string;
|
|
42
|
+
labelClassName?: string;
|
|
43
|
+
valueClassName?: string;
|
|
44
|
+
formatTokens?: (tokens: number) => string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SessionSpendSummaryProps {
|
|
48
|
+
usage?: SessionTokenUsage;
|
|
49
|
+
currentContextTokens?: number;
|
|
50
|
+
className?: string;
|
|
51
|
+
slots?: ClassSlots;
|
|
52
|
+
formatTokens?: (tokens: number) => string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RequestUsageTimelineProps {
|
|
56
|
+
requests: readonly SessionRequestUsage[];
|
|
57
|
+
onOpenTool?: (messageId: string, callId: string) => void;
|
|
58
|
+
className?: string;
|
|
59
|
+
slots?: ClassSlots;
|
|
60
|
+
formatTokens?: (tokens: number) => string;
|
|
61
|
+
empty?: ReactNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ContextWindowPanelProps {
|
|
65
|
+
snapshot: ContextWindowSnapshot;
|
|
66
|
+
onCompactNow?: () => void;
|
|
67
|
+
className?: string;
|
|
68
|
+
slots?: ClassSlots;
|
|
69
|
+
formatTokens?: (tokens: number) => string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const STATUS_COLORS: Record<ContextWindowSnapshot['status'], string> = {
|
|
73
|
+
unknown: '#8b8b95',
|
|
74
|
+
low: '#a4d4a8',
|
|
75
|
+
medium: '#f0c36a',
|
|
76
|
+
high: '#f08a8a',
|
|
77
|
+
critical: '#ff5c7a',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function ContextWindowRing({
|
|
81
|
+
snapshot,
|
|
82
|
+
size = 20,
|
|
83
|
+
className,
|
|
84
|
+
innerClassName,
|
|
85
|
+
style,
|
|
86
|
+
}: ContextWindowRingProps) {
|
|
87
|
+
const pct = snapshot.percentFull === undefined
|
|
88
|
+
? 0.28
|
|
89
|
+
: Math.max(0, Math.min(1, snapshot.percentFull));
|
|
90
|
+
const degrees = Math.max(10, Math.round(pct * 360));
|
|
91
|
+
const color = `var(--pyric-context-window-status-color, ${STATUS_COLORS[snapshot.status]})`;
|
|
92
|
+
const track = 'var(--pyric-context-window-track-color, #3a3a45)';
|
|
93
|
+
const innerSize = Math.max(8, size - Math.max(6, Math.round(size * 0.34)));
|
|
94
|
+
return (
|
|
95
|
+
<span
|
|
96
|
+
data-pyric-ui="context-window-ring"
|
|
97
|
+
data-status={snapshot.status}
|
|
98
|
+
data-basis={snapshot.basis}
|
|
99
|
+
className={className}
|
|
100
|
+
style={{
|
|
101
|
+
display: 'inline-grid',
|
|
102
|
+
placeItems: 'center',
|
|
103
|
+
borderRadius: '999px',
|
|
104
|
+
width: size,
|
|
105
|
+
height: size,
|
|
106
|
+
background: `conic-gradient(${color} ${degrees}deg, ${track} ${degrees}deg)`,
|
|
107
|
+
...style,
|
|
108
|
+
}}
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
>
|
|
111
|
+
<span
|
|
112
|
+
data-pyric-ui="context-window-ring-inner"
|
|
113
|
+
className={innerClassName}
|
|
114
|
+
style={{
|
|
115
|
+
display: 'block',
|
|
116
|
+
borderRadius: '999px',
|
|
117
|
+
width: innerSize,
|
|
118
|
+
height: innerSize,
|
|
119
|
+
background: 'var(--pyric-context-window-inner-color, currentColor)',
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
</span>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function ContextWindowMeter({
|
|
127
|
+
snapshot,
|
|
128
|
+
onOpen,
|
|
129
|
+
className,
|
|
130
|
+
buttonClassName,
|
|
131
|
+
tooltipClassName,
|
|
132
|
+
ringClassName,
|
|
133
|
+
ringInnerClassName,
|
|
134
|
+
formatTokens = formatCompactTokens,
|
|
135
|
+
}: ContextWindowMeterProps) {
|
|
136
|
+
const title = `Context window: ${formatPercent(snapshot)} · ${formatRatio(snapshot, formatTokens)}`;
|
|
137
|
+
return (
|
|
138
|
+
<span
|
|
139
|
+
data-pyric-ui="context-window-meter"
|
|
140
|
+
data-status={snapshot.status}
|
|
141
|
+
data-basis={snapshot.basis}
|
|
142
|
+
className={className}
|
|
143
|
+
>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={onOpen}
|
|
147
|
+
className={buttonClassName}
|
|
148
|
+
aria-label="Open context window details"
|
|
149
|
+
title={title}
|
|
150
|
+
>
|
|
151
|
+
<ContextWindowRing
|
|
152
|
+
snapshot={snapshot}
|
|
153
|
+
size={20}
|
|
154
|
+
className={ringClassName}
|
|
155
|
+
innerClassName={ringInnerClassName}
|
|
156
|
+
/>
|
|
157
|
+
</button>
|
|
158
|
+
<span
|
|
159
|
+
data-pyric-ui="context-window-meter-tooltip"
|
|
160
|
+
className={tooltipClassName}
|
|
161
|
+
>
|
|
162
|
+
<span data-pyric-ui="context-window-meter-percent">
|
|
163
|
+
{formatPercent(snapshot)}
|
|
164
|
+
</span>
|
|
165
|
+
<span data-pyric-ui="context-window-meter-ratio">
|
|
166
|
+
{formatRatio(snapshot, formatTokens)}
|
|
167
|
+
</span>
|
|
168
|
+
</span>
|
|
169
|
+
</span>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function TokenUsageInline({
|
|
174
|
+
usage,
|
|
175
|
+
className,
|
|
176
|
+
labelClassName,
|
|
177
|
+
valueClassName,
|
|
178
|
+
formatTokens = formatCompactTokens,
|
|
179
|
+
}: TokenUsageInlineProps) {
|
|
180
|
+
if (!usage) return null;
|
|
181
|
+
return (
|
|
182
|
+
<dl data-pyric-ui="token-usage-inline" className={className}>
|
|
183
|
+
<MetricTerm
|
|
184
|
+
label="input"
|
|
185
|
+
value={formatTokens(usage.inputTokens)}
|
|
186
|
+
labelClassName={labelClassName}
|
|
187
|
+
valueClassName={valueClassName}
|
|
188
|
+
/>
|
|
189
|
+
<MetricTerm
|
|
190
|
+
label="output"
|
|
191
|
+
value={formatTokens(usage.outputTokens)}
|
|
192
|
+
labelClassName={labelClassName}
|
|
193
|
+
valueClassName={valueClassName}
|
|
194
|
+
/>
|
|
195
|
+
{usage.cachedInputTokens > 0 ? (
|
|
196
|
+
<MetricTerm
|
|
197
|
+
label="cached"
|
|
198
|
+
value={formatTokens(usage.cachedInputTokens)}
|
|
199
|
+
labelClassName={labelClassName}
|
|
200
|
+
valueClassName={valueClassName}
|
|
201
|
+
/>
|
|
202
|
+
) : null}
|
|
203
|
+
{usage.reasoningTokens > 0 ? (
|
|
204
|
+
<MetricTerm
|
|
205
|
+
label="reasoning"
|
|
206
|
+
value={formatTokens(usage.reasoningTokens)}
|
|
207
|
+
labelClassName={labelClassName}
|
|
208
|
+
valueClassName={valueClassName}
|
|
209
|
+
/>
|
|
210
|
+
) : null}
|
|
211
|
+
</dl>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function SessionSpendSummary({
|
|
216
|
+
usage,
|
|
217
|
+
currentContextTokens,
|
|
218
|
+
className,
|
|
219
|
+
slots,
|
|
220
|
+
formatTokens = formatCompactTokens,
|
|
221
|
+
}: SessionSpendSummaryProps) {
|
|
222
|
+
if (!usage || usage.tokensTotal <= 0) return null;
|
|
223
|
+
const contextCopies =
|
|
224
|
+
usage.workMultiplier ??
|
|
225
|
+
(currentContextTokens && currentContextTokens > 0
|
|
226
|
+
? usage.tokensTotal / currentContextTokens
|
|
227
|
+
: undefined);
|
|
228
|
+
return (
|
|
229
|
+
<section
|
|
230
|
+
data-pyric-ui="session-spend-summary"
|
|
231
|
+
className={className ?? slots?.root}
|
|
232
|
+
>
|
|
233
|
+
<div data-pyric-ui="session-spend-summary-header" className={slots?.header}>
|
|
234
|
+
<span data-pyric-ui="session-spend-summary-title">Overall session spend</span>
|
|
235
|
+
<span data-pyric-ui="session-spend-summary-total">
|
|
236
|
+
{formatTokens(usage.tokensTotal)}
|
|
237
|
+
</span>
|
|
238
|
+
</div>
|
|
239
|
+
<dl data-pyric-ui="session-spend-summary-body" className={slots?.body}>
|
|
240
|
+
<MetricTerm
|
|
241
|
+
label="turns"
|
|
242
|
+
value={usage.turns === null ? 'unknown' : usage.turns.toLocaleString()}
|
|
243
|
+
labelClassName={slots?.label}
|
|
244
|
+
valueClassName={slots?.value}
|
|
245
|
+
/>
|
|
246
|
+
<MetricTerm
|
|
247
|
+
label="requests"
|
|
248
|
+
value={usage.requests === null ? 'unknown' : usage.requests.toLocaleString()}
|
|
249
|
+
labelClassName={slots?.label}
|
|
250
|
+
valueClassName={slots?.value}
|
|
251
|
+
/>
|
|
252
|
+
<MetricTerm
|
|
253
|
+
label="average request"
|
|
254
|
+
value={usage.averageRequestTokens ? formatTokens(usage.averageRequestTokens) : 'n/a'}
|
|
255
|
+
labelClassName={slots?.label}
|
|
256
|
+
valueClassName={slots?.value}
|
|
257
|
+
/>
|
|
258
|
+
<MetricTerm
|
|
259
|
+
label="context multiplier"
|
|
260
|
+
value={contextCopies ? `${formatMultiplier(contextCopies)}x` : 'n/a'}
|
|
261
|
+
labelClassName={slots?.label}
|
|
262
|
+
valueClassName={slots?.value}
|
|
263
|
+
/>
|
|
264
|
+
</dl>
|
|
265
|
+
<TokenUsageInline
|
|
266
|
+
usage={usage}
|
|
267
|
+
className={slots?.row}
|
|
268
|
+
labelClassName={slots?.label}
|
|
269
|
+
valueClassName={slots?.value}
|
|
270
|
+
formatTokens={formatTokens}
|
|
271
|
+
/>
|
|
272
|
+
</section>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function RequestUsageTimeline({
|
|
277
|
+
requests,
|
|
278
|
+
onOpenTool,
|
|
279
|
+
className,
|
|
280
|
+
slots,
|
|
281
|
+
formatTokens = formatCompactTokens,
|
|
282
|
+
empty,
|
|
283
|
+
}: RequestUsageTimelineProps) {
|
|
284
|
+
if (requests.length === 0) {
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
data-pyric-ui="request-usage-timeline-empty"
|
|
288
|
+
className={slots?.empty ?? className}
|
|
289
|
+
>
|
|
290
|
+
{empty ?? 'No request usage rows available.'}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return (
|
|
295
|
+
<ol data-pyric-ui="request-usage-timeline" className={className ?? slots?.root}>
|
|
296
|
+
{requests.map((request) => (
|
|
297
|
+
<li
|
|
298
|
+
key={request.id}
|
|
299
|
+
data-pyric-ui="request-usage-row"
|
|
300
|
+
data-usage-source={request.usageSource}
|
|
301
|
+
data-provider={request.providerId}
|
|
302
|
+
className={slots?.row}
|
|
303
|
+
>
|
|
304
|
+
<div data-pyric-ui="request-usage-row-header" className={slots?.header}>
|
|
305
|
+
<span data-pyric-ui="request-usage-row-title">
|
|
306
|
+
Request {request.iteration + 1}
|
|
307
|
+
</span>
|
|
308
|
+
<span data-pyric-ui="request-usage-row-total">
|
|
309
|
+
{formatTokens(request.tokensTotal)}
|
|
310
|
+
</span>
|
|
311
|
+
</div>
|
|
312
|
+
<SegmentBar
|
|
313
|
+
rows={[
|
|
314
|
+
['fresh-input', request.freshInputTokens, '#8bb7ff'],
|
|
315
|
+
['cached-input', request.cachedInputTokens, '#a4d4a8'],
|
|
316
|
+
['visible-output', request.visibleOutputTokens, '#f0c36a'],
|
|
317
|
+
['reasoning-output', request.reasoningTokens, '#c9a7ff'],
|
|
318
|
+
]}
|
|
319
|
+
total={Math.max(1, request.tokensTotal)}
|
|
320
|
+
barClassName={slots?.bar}
|
|
321
|
+
segmentClassName={slots?.segment}
|
|
322
|
+
/>
|
|
323
|
+
<dl data-pyric-ui="request-usage-row-metrics" className={slots?.body}>
|
|
324
|
+
<MetricTerm
|
|
325
|
+
label="input"
|
|
326
|
+
value={formatTokens(request.inputTokens)}
|
|
327
|
+
labelClassName={slots?.label}
|
|
328
|
+
valueClassName={slots?.value}
|
|
329
|
+
/>
|
|
330
|
+
<MetricTerm
|
|
331
|
+
label="output"
|
|
332
|
+
value={formatTokens(request.outputTokens)}
|
|
333
|
+
labelClassName={slots?.label}
|
|
334
|
+
valueClassName={slots?.value}
|
|
335
|
+
/>
|
|
336
|
+
<MetricTerm
|
|
337
|
+
label="messages"
|
|
338
|
+
value={request.messageCount.toLocaleString()}
|
|
339
|
+
labelClassName={slots?.label}
|
|
340
|
+
valueClassName={slots?.value}
|
|
341
|
+
/>
|
|
342
|
+
</dl>
|
|
343
|
+
{onOpenTool && request.emittedToolCalls.length > 0 ? (
|
|
344
|
+
<div data-pyric-ui="request-usage-tools">
|
|
345
|
+
{request.emittedToolCalls.map((tool) => (
|
|
346
|
+
<button
|
|
347
|
+
key={`${tool.messageId ?? 'unknown'}:${tool.callId ?? tool.name}`}
|
|
348
|
+
type="button"
|
|
349
|
+
data-pyric-ui="request-usage-tool"
|
|
350
|
+
disabled={!tool.messageId || !tool.callId}
|
|
351
|
+
onClick={() => {
|
|
352
|
+
if (tool.messageId && tool.callId) onOpenTool(tool.messageId, tool.callId);
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
{tool.name}
|
|
356
|
+
</button>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
) : null}
|
|
360
|
+
</li>
|
|
361
|
+
))}
|
|
362
|
+
</ol>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function ContextWindowPanel({
|
|
367
|
+
snapshot,
|
|
368
|
+
onCompactNow,
|
|
369
|
+
className,
|
|
370
|
+
slots,
|
|
371
|
+
formatTokens = formatCompactTokens,
|
|
372
|
+
}: ContextWindowPanelProps) {
|
|
373
|
+
return (
|
|
374
|
+
<section
|
|
375
|
+
data-pyric-ui="context-window-panel"
|
|
376
|
+
data-status={snapshot.status}
|
|
377
|
+
data-basis={snapshot.basis}
|
|
378
|
+
className={className ?? slots?.root}
|
|
379
|
+
>
|
|
380
|
+
<header data-pyric-ui="context-window-panel-header" className={slots?.header}>
|
|
381
|
+
<ContextWindowRing snapshot={snapshot} size={56} />
|
|
382
|
+
<div>
|
|
383
|
+
<div data-pyric-ui="context-window-panel-percent">
|
|
384
|
+
{formatPercent(snapshot)}
|
|
385
|
+
</div>
|
|
386
|
+
<div data-pyric-ui="context-window-panel-ratio">
|
|
387
|
+
{formatRatio(snapshot, formatTokens)}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</header>
|
|
391
|
+
<SegmentBar
|
|
392
|
+
rows={snapshot.breakdown.map((row) => [row.id, row.tokens, row.color])}
|
|
393
|
+
total={Math.max(1, snapshot.usedTokens)}
|
|
394
|
+
barClassName={slots?.bar}
|
|
395
|
+
segmentClassName={slots?.segment}
|
|
396
|
+
/>
|
|
397
|
+
<dl data-pyric-ui="context-window-breakdown" className={slots?.body}>
|
|
398
|
+
{snapshot.breakdown.map((row) => (
|
|
399
|
+
<MetricTerm
|
|
400
|
+
key={row.id}
|
|
401
|
+
label={row.label}
|
|
402
|
+
value={formatTokens(row.tokens)}
|
|
403
|
+
labelClassName={slots?.label}
|
|
404
|
+
valueClassName={slots?.value}
|
|
405
|
+
/>
|
|
406
|
+
))}
|
|
407
|
+
</dl>
|
|
408
|
+
<SessionSpendSummary
|
|
409
|
+
usage={snapshot.sessionUsage}
|
|
410
|
+
currentContextTokens={snapshot.usedTokens}
|
|
411
|
+
slots={slots}
|
|
412
|
+
formatTokens={formatTokens}
|
|
413
|
+
/>
|
|
414
|
+
{onCompactNow ? (
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
data-pyric-ui="context-window-compact-action"
|
|
418
|
+
onClick={onCompactNow}
|
|
419
|
+
>
|
|
420
|
+
Compact now
|
|
421
|
+
</button>
|
|
422
|
+
) : null}
|
|
423
|
+
</section>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function MetricTerm({
|
|
428
|
+
label,
|
|
429
|
+
value,
|
|
430
|
+
labelClassName,
|
|
431
|
+
valueClassName,
|
|
432
|
+
}: {
|
|
433
|
+
label: string;
|
|
434
|
+
value: string;
|
|
435
|
+
labelClassName?: string;
|
|
436
|
+
valueClassName?: string;
|
|
437
|
+
}) {
|
|
438
|
+
return (
|
|
439
|
+
<div data-pyric-ui="metric-term">
|
|
440
|
+
<dt data-pyric-ui="metric-label" className={labelClassName}>
|
|
441
|
+
{label}
|
|
442
|
+
</dt>
|
|
443
|
+
<dd data-pyric-ui="metric-value" className={valueClassName}>
|
|
444
|
+
{value}
|
|
445
|
+
</dd>
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function SegmentBar({
|
|
451
|
+
rows,
|
|
452
|
+
total,
|
|
453
|
+
barClassName,
|
|
454
|
+
segmentClassName,
|
|
455
|
+
}: {
|
|
456
|
+
rows: readonly (readonly [id: string, tokens: number, color: string])[];
|
|
457
|
+
total: number;
|
|
458
|
+
barClassName?: string;
|
|
459
|
+
segmentClassName?: string;
|
|
460
|
+
}) {
|
|
461
|
+
const visibleRows = rows.filter(([, tokens]) => tokens > 0);
|
|
462
|
+
if (visibleRows.length === 0) return null;
|
|
463
|
+
return (
|
|
464
|
+
<div data-pyric-ui="token-segment-bar" className={barClassName}>
|
|
465
|
+
{visibleRows.map(([id, tokens, color]) => (
|
|
466
|
+
<span
|
|
467
|
+
key={id}
|
|
468
|
+
data-pyric-ui="token-segment"
|
|
469
|
+
data-segment={id}
|
|
470
|
+
className={segmentClassName}
|
|
471
|
+
style={{
|
|
472
|
+
display: 'inline-block',
|
|
473
|
+
width: `${Math.max(1, (tokens / total) * 100)}%`,
|
|
474
|
+
background: `var(--pyric-token-segment-${id}, ${color})`,
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
477
|
+
))}
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function formatRatio(
|
|
483
|
+
snapshot: ContextWindowSnapshot,
|
|
484
|
+
formatTokens: (tokens: number) => string,
|
|
485
|
+
): string {
|
|
486
|
+
const used = formatTokens(snapshot.usedTokens);
|
|
487
|
+
if (!snapshot.limitTokens) return `${used} tokens used`;
|
|
488
|
+
return `${used} / ${formatTokens(snapshot.limitTokens)} tokens used`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function formatPercent(snapshot: ContextWindowSnapshot): string {
|
|
492
|
+
if (snapshot.percentFull === undefined) return 'limit unknown';
|
|
493
|
+
return `${Math.round(snapshot.percentFull * 100)}% full`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function formatCompactTokens(tokens: number): string {
|
|
497
|
+
if (tokens < 1000) return String(Math.max(0, Math.round(tokens)));
|
|
498
|
+
if (tokens < 100_000) {
|
|
499
|
+
const k = tokens / 1000;
|
|
500
|
+
const value = k.toFixed(1);
|
|
501
|
+
return `${value.endsWith('.0') ? value.slice(0, -2) : value}k`;
|
|
502
|
+
}
|
|
503
|
+
if (tokens < 1_000_000) return `${Math.round(tokens / 1000)}k`;
|
|
504
|
+
// Millions tier — "14.59M", never "14586k".
|
|
505
|
+
const m = (tokens / 1_000_000).toFixed(2);
|
|
506
|
+
return `${m.endsWith('.00') ? m.slice(0, -3) : m}M`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function formatMultiplier(value: number): string {
|
|
510
|
+
if (!Number.isFinite(value)) return 'n/a';
|
|
511
|
+
if (value < 10) return value.toFixed(1);
|
|
512
|
+
if (value < 100) return Math.round(value).toString();
|
|
513
|
+
return value.toLocaleString(undefined, { maximumFractionDigits: 0 });
|
|
514
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface EmptyStateProps {
|
|
4
|
+
/** Optional pre-rendered icon node — consumers pass whatever icon
|
|
5
|
+
* primitive their app uses (Material Symbols span, lucide-react
|
|
6
|
+
* component, plain SVG, …). */
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
body?: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Headless zero-state. Structural only: icon slot, title, optional
|
|
15
|
+
* body, all wrapped in a flex container marked
|
|
16
|
+
* `[data-pyric-ui="empty-state"]`. Consumers attach all visual
|
|
17
|
+
* styling.
|
|
18
|
+
*/
|
|
19
|
+
export function EmptyState({ icon, title, body, className }: EmptyStateProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div data-pyric-ui="empty-state" className={className}>
|
|
22
|
+
{icon ? <span data-pyric-empty-icon>{icon}</span> : null}
|
|
23
|
+
<p data-pyric-empty-title>{title}</p>
|
|
24
|
+
{body ? <p data-pyric-empty-body>{body}</p> : null}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type FoldTone = 'normal' | 'error' | 'thought';
|
|
4
|
+
|
|
5
|
+
export interface FoldProps {
|
|
6
|
+
/** Visible summary line, always rendered. */
|
|
7
|
+
header: ReactNode;
|
|
8
|
+
/** Optional right-aligned action (e.g. a copy button). Children
|
|
9
|
+
* should `stopPropagation` so clicks don't toggle the fold. */
|
|
10
|
+
headerAction?: ReactNode;
|
|
11
|
+
/** Revealed when the user expands. */
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
defaultOpen?: boolean;
|
|
14
|
+
/** Surfaced via `[data-pyric-fold-tone]` so consumers can tint the
|
|
15
|
+
* summary or border based on semantic context (error / thought).
|
|
16
|
+
* Defaults to `normal`. */
|
|
17
|
+
tone?: FoldTone;
|
|
18
|
+
/** Forwarded to the root `<details>` element. */
|
|
19
|
+
className?: string;
|
|
20
|
+
/** Forwarded to the `<summary>` element. */
|
|
21
|
+
summaryClassName?: string;
|
|
22
|
+
/** Forwarded to the body wrapper revealed when open. */
|
|
23
|
+
bodyClassName?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Headless disclosure container — native `<details>` for keyboard /
|
|
28
|
+
* a11y. Ships zero visual styling. Consumers style via the three
|
|
29
|
+
* `className` slots or by selecting on:
|
|
30
|
+
*
|
|
31
|
+
* [data-pyric-ui="fold"] — root `<details>`
|
|
32
|
+
* [data-pyric-ui="fold"][open] — expanded state
|
|
33
|
+
* [data-pyric-fold-tone="error"] — error tint
|
|
34
|
+
* [data-pyric-fold-tone="thought"] — thought-stream tint
|
|
35
|
+
* [data-pyric-fold-summary] — clickable summary row
|
|
36
|
+
* [data-pyric-fold-chevron] — disclosure indicator slot
|
|
37
|
+
* [data-pyric-fold-body] — revealed body wrapper
|
|
38
|
+
*/
|
|
39
|
+
export function Fold({
|
|
40
|
+
header,
|
|
41
|
+
headerAction,
|
|
42
|
+
children,
|
|
43
|
+
defaultOpen = false,
|
|
44
|
+
tone = 'normal',
|
|
45
|
+
className,
|
|
46
|
+
summaryClassName,
|
|
47
|
+
bodyClassName,
|
|
48
|
+
}: FoldProps) {
|
|
49
|
+
return (
|
|
50
|
+
<details
|
|
51
|
+
data-pyric-ui="fold"
|
|
52
|
+
data-pyric-fold-tone={tone}
|
|
53
|
+
open={defaultOpen}
|
|
54
|
+
className={className}
|
|
55
|
+
>
|
|
56
|
+
<summary data-pyric-fold-summary className={summaryClassName}>
|
|
57
|
+
<span data-pyric-fold-chevron aria-hidden="true" />
|
|
58
|
+
<span data-pyric-fold-header>{header}</span>
|
|
59
|
+
{headerAction ? <span data-pyric-fold-action>{headerAction}</span> : null}
|
|
60
|
+
</summary>
|
|
61
|
+
<div data-pyric-fold-body className={bodyClassName}>{children}</div>
|
|
62
|
+
</details>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ModalProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
ariaLabel?: string;
|
|
8
|
+
/** Forwarded to the root wrapper (covers the full viewport). */
|
|
9
|
+
className?: string;
|
|
10
|
+
/** Forwarded to the backdrop element. */
|
|
11
|
+
backdropClassName?: string;
|
|
12
|
+
/** Forwarded to the inner panel. */
|
|
13
|
+
panelClassName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Headless modal — provides behavior only (Escape-to-close, backdrop
|
|
18
|
+
* click, `aria-modal`). Consumers attach all visual styling via the
|
|
19
|
+
* three `className` slots or by selecting on the emitted data
|
|
20
|
+
* attributes:
|
|
21
|
+
*
|
|
22
|
+
* [data-pyric-ui="modal"] — root viewport wrapper
|
|
23
|
+
* [data-pyric-modal-backdrop] — clickable backdrop
|
|
24
|
+
* [data-pyric-modal-panel] — inner panel containing children
|
|
25
|
+
*/
|
|
26
|
+
export function Modal({
|
|
27
|
+
open,
|
|
28
|
+
onClose,
|
|
29
|
+
children,
|
|
30
|
+
ariaLabel,
|
|
31
|
+
className,
|
|
32
|
+
backdropClassName,
|
|
33
|
+
panelClassName,
|
|
34
|
+
}: ModalProps) {
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!open) return;
|
|
37
|
+
const handler = (e: KeyboardEvent) => {
|
|
38
|
+
if (e.key === 'Escape') onClose();
|
|
39
|
+
};
|
|
40
|
+
window.addEventListener('keydown', handler);
|
|
41
|
+
return () => window.removeEventListener('keydown', handler);
|
|
42
|
+
}, [open, onClose]);
|
|
43
|
+
|
|
44
|
+
if (!open) return null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
role="dialog"
|
|
49
|
+
aria-modal="true"
|
|
50
|
+
aria-label={ariaLabel}
|
|
51
|
+
data-pyric-ui="modal"
|
|
52
|
+
className={className}
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
data-pyric-modal-backdrop
|
|
56
|
+
className={backdropClassName}
|
|
57
|
+
onClick={onClose}
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
/>
|
|
60
|
+
<div data-pyric-modal-panel className={panelClassName}>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|