@tonyclaw/agent-inspector 3.0.23 → 3.0.25
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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-CFedvZHU.js → CompareDrawer-BnvOPwpe.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DcqFqo1m.js +127 -0
- package/.output/public/assets/{ReplayDialog-X-fRSKAu.js → ReplayDialog-BJgoQWdW.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-S8qB5nVg.js → RequestAnatomy-CpBZEHnM.js} +1 -1
- package/.output/public/assets/{ResponseView-_rcOWSwJ.js → ResponseView-CWmyRb2M.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-Bc15AVpN.js → StreamingChunkSequence-BWhC8bwx.js} +1 -1
- package/.output/public/assets/{_sessionId-Cz-HW2Wf.js → _sessionId-Cq-2OqUU.js} +1 -1
- package/.output/public/assets/{index-CEgwWY3E.js → index-BKi9IRtV.js} +71 -7
- package/.output/public/assets/index-BMYuDL5X.css +1 -0
- package/.output/public/assets/{index-CXYBU0j8.js → index-Cf8O0Oos.js} +1 -1
- package/.output/public/assets/{index-ClKXooQd.js → index-DdgTJ4QJ.js} +1 -1
- package/.output/public/assets/{json-viewer-QfU4qGdd.js → json-viewer-Bqu_4hmm.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +306 -246
- package/.output/server/{_sessionId-9FAmYZVf.mjs → _sessionId-Ba7H3Gjw.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BLKjOzwD.mjs → CompareDrawer-aH59Ab6x.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-Dcs2Mq8l.mjs → ProxyViewerContainer-By-I0tJv.mjs} +1791 -1259
- package/.output/server/_ssr/{ReplayDialog-oyiF4LIo.mjs → ReplayDialog-BO4Vy0et.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-DkyQXSuj.mjs → RequestAnatomy-FtOr7vSS.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-3NTDEiDh.mjs → ResponseView-cvw6ajue.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DRq-5Bgn.mjs → StreamingChunkSequence-Oc8S2kD6.mjs} +3 -3
- package/.output/server/_ssr/{index-dtMiNUa1.mjs → index-BDRlQheR.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-BhUwicvp.mjs → json-viewer-Cd6trhQr.mjs} +3 -3
- package/.output/server/_ssr/{router-DO0OntUK.mjs → router-bofP-Ekl.mjs} +296 -91
- package/.output/server/{_tanstack-start-manifest_v-DO5v3BHY.mjs → _tanstack-start-manifest_v-BPuf86qB.mjs} +1 -1
- package/.output/server/index.mjs +76 -76
- package/package.json +3 -2
- package/src/components/ProxyViewer.tsx +138 -740
- package/src/components/ProxyViewerContainer.tsx +5 -47
- package/src/components/alerts/AlertsDialog.tsx +6 -2
- package/src/components/inspector-pet/InspectorPet.tsx +390 -0
- package/src/components/inspector-pet/inspectorPetLogic.ts +273 -0
- package/src/components/providers/ProviderCard.tsx +20 -579
- package/src/components/providers/ProviderTestResultsView.tsx +732 -0
- package/src/components/proxy-viewer/LogEntry.tsx +5 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +7 -0
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +33 -36
- package/src/components/proxy-viewer/TurnGroup.tsx +1 -5
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +46 -0
- package/src/components/proxy-viewer/proxyViewerLogic.ts +469 -0
- package/src/lib/themeMode.ts +45 -0
- package/src/lib/uiScale.ts +112 -0
- package/src/lib/useThemeMode.ts +40 -0
- package/src/lib/useUiScale.ts +52 -0
- package/src/routes/__root.tsx +78 -1
- package/styles/globals.css +403 -24
- package/.output/public/assets/ProxyViewerContainer-BlCrBvdm.js +0 -127
- package/.output/public/assets/index-B_SIQ47i.css +0 -1
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
type SessionLoadProgress,
|
|
17
17
|
type SessionMembershipEvidence,
|
|
18
18
|
} from "./ProxyViewer";
|
|
19
|
+
import {
|
|
20
|
+
filterLogs,
|
|
21
|
+
mergeLogsById,
|
|
22
|
+
mergeSessionIds,
|
|
23
|
+
} from "./proxy-viewer/proxyViewerContainerLogic";
|
|
19
24
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
20
25
|
import { TransientToast, useTransientToast } from "./ui/transient-toast";
|
|
21
26
|
|
|
@@ -112,24 +117,6 @@ function extractModels(logs: CapturedLog[]): string[] {
|
|
|
112
117
|
return [...set];
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
/**
|
|
116
|
-
* Filter logs by selected session/model. Both `__all__` and missing values
|
|
117
|
-
* pass through. Returns the input array reference when no log matches the
|
|
118
|
-
* filter, so referential equality is preserved for empty results.
|
|
119
|
-
*/
|
|
120
|
-
export function filterLogs(
|
|
121
|
-
logs: CapturedLog[],
|
|
122
|
-
selectedSession: string,
|
|
123
|
-
selectedModel: string,
|
|
124
|
-
): CapturedLog[] {
|
|
125
|
-
if (selectedSession === "__all__" && selectedModel === "__all__") return logs;
|
|
126
|
-
return logs.filter((l) => {
|
|
127
|
-
if (selectedSession !== "__all__" && l.sessionId !== selectedSession) return false;
|
|
128
|
-
if (selectedModel !== "__all__" && l.model !== selectedModel) return false;
|
|
129
|
-
return true;
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
120
|
const DEBOUNCE_MS = 100;
|
|
134
121
|
const HASH_SCROLL_ATTEMPTS = 12;
|
|
135
122
|
const HASH_HIGHLIGHT_MS = 1800;
|
|
@@ -243,35 +230,6 @@ async function deleteAllGroupsForFullHistoryDelete(): Promise<GroupCleanupResult
|
|
|
243
230
|
}
|
|
244
231
|
}
|
|
245
232
|
|
|
246
|
-
export function mergeLogsById(
|
|
247
|
-
previousLogs: readonly CapturedLog[],
|
|
248
|
-
incomingLogs: readonly CapturedLog[],
|
|
249
|
-
): CapturedLog[] {
|
|
250
|
-
if (incomingLogs.length === 0) return [...previousLogs];
|
|
251
|
-
const byId = new Map<number, CapturedLog>();
|
|
252
|
-
for (const log of previousLogs) {
|
|
253
|
-
byId.set(log.id, log);
|
|
254
|
-
}
|
|
255
|
-
for (const log of incomingLogs) {
|
|
256
|
-
byId.set(log.id, log);
|
|
257
|
-
}
|
|
258
|
-
return [...byId.values()].sort((left, right) => left.id - right.id);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function mergeSessionIds(
|
|
262
|
-
previousSessions: readonly string[],
|
|
263
|
-
incomingSessions: readonly string[],
|
|
264
|
-
): string[] {
|
|
265
|
-
const sessions = new Set<string>();
|
|
266
|
-
for (const session of previousSessions) {
|
|
267
|
-
if (session !== "") sessions.add(session);
|
|
268
|
-
}
|
|
269
|
-
for (const session of incomingSessions) {
|
|
270
|
-
if (session !== "") sessions.add(session);
|
|
271
|
-
}
|
|
272
|
-
return [...sessions].sort();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
233
|
export function ProxyViewerContainer({
|
|
276
234
|
initialSessionId,
|
|
277
235
|
}: {
|
|
@@ -469,7 +469,11 @@ export function AlertsDialog(): JSX.Element {
|
|
|
469
469
|
const warningCount = summary?.warning ?? 0;
|
|
470
470
|
const noticeCount = summary?.notice ?? 0;
|
|
471
471
|
const triggerTone =
|
|
472
|
-
criticalCount > 0
|
|
472
|
+
criticalCount > 0
|
|
473
|
+
? "bg-red-600 text-white"
|
|
474
|
+
: warningCount > 0
|
|
475
|
+
? "bg-amber-400 text-zinc-950"
|
|
476
|
+
: "bg-sky-400 text-zinc-950";
|
|
473
477
|
|
|
474
478
|
return (
|
|
475
479
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
@@ -488,7 +492,7 @@ export function AlertsDialog(): JSX.Element {
|
|
|
488
492
|
) : totalCount > 0 ? (
|
|
489
493
|
<span
|
|
490
494
|
className={cn(
|
|
491
|
-
"absolute -right-1 -top-1 min-w-4 rounded-full px-1 text-[10px] font-semibold leading-4
|
|
495
|
+
"absolute -right-1 -top-1 min-w-4 rounded-full px-1 text-[10px] font-semibold leading-4",
|
|
492
496
|
triggerTone,
|
|
493
497
|
)}
|
|
494
498
|
>
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type JSX,
|
|
4
|
+
type PointerEvent as ReactPointerEvent,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Heart, Pause, Play } from "lucide-react";
|
|
12
|
+
import type { CapturedLog } from "../../contracts";
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
import { CrabLogo } from "../ui/crab-logo";
|
|
15
|
+
import {
|
|
16
|
+
INSPECTOR_PET_MARGIN,
|
|
17
|
+
type InspectorPetMood,
|
|
18
|
+
type InspectorPetPosition,
|
|
19
|
+
type InspectorPetViewport,
|
|
20
|
+
clampInspectorPetPosition,
|
|
21
|
+
inspectorPetPanelPosition,
|
|
22
|
+
nextInspectorPetWalkingPosition,
|
|
23
|
+
randomInspectorPetEdgePosition,
|
|
24
|
+
resolveInspectorPetStatus,
|
|
25
|
+
snapInspectorPetToEdge,
|
|
26
|
+
} from "./inspectorPetLogic";
|
|
27
|
+
|
|
28
|
+
type InspectorPetProps = {
|
|
29
|
+
logs: readonly CapturedLog[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type InspectorPetDragState = {
|
|
33
|
+
pointerId: number;
|
|
34
|
+
offsetX: number;
|
|
35
|
+
offsetY: number;
|
|
36
|
+
startX: number;
|
|
37
|
+
startY: number;
|
|
38
|
+
moved: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const INSPECTOR_PET_ROAMING_STORAGE_KEY = "agent-inspector.pet.roaming";
|
|
42
|
+
const INSPECTOR_PET_MIN_DRIFT_MS = 5200;
|
|
43
|
+
const INSPECTOR_PET_DRIFT_WINDOW_MS = 2400;
|
|
44
|
+
const INSPECTOR_PET_PANEL_DISMISS_MS = 6000;
|
|
45
|
+
const INSPECTOR_PET_REACTION_MS = 760;
|
|
46
|
+
const INSPECTOR_PET_DRAG_THRESHOLD = 4;
|
|
47
|
+
|
|
48
|
+
function readViewport(): InspectorPetViewport {
|
|
49
|
+
if (typeof window === "undefined") return { width: 1280, height: 720, scale: 1 };
|
|
50
|
+
const rawScale = window
|
|
51
|
+
.getComputedStyle(document.documentElement)
|
|
52
|
+
.getPropertyValue("--inspector-ui-scale");
|
|
53
|
+
const parsedScale = Number.parseFloat(rawScale);
|
|
54
|
+
const scale = Number.isFinite(parsedScale) && parsedScale > 0 ? parsedScale : 1;
|
|
55
|
+
return {
|
|
56
|
+
width: Math.max(window.innerWidth, INSPECTOR_PET_MARGIN * 2),
|
|
57
|
+
height: Math.max(window.innerHeight, INSPECTOR_PET_MARGIN * 2),
|
|
58
|
+
scale,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readStoredRoaming(): boolean {
|
|
63
|
+
if (typeof window === "undefined") return true;
|
|
64
|
+
try {
|
|
65
|
+
return window.localStorage.getItem(INSPECTOR_PET_ROAMING_STORAGE_KEY) !== "0";
|
|
66
|
+
} catch {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function moodDotClass(mood: InspectorPetMood): string {
|
|
72
|
+
switch (mood) {
|
|
73
|
+
case "active":
|
|
74
|
+
return "bg-cyan-400 shadow-[0_0_9px_rgba(34,211,238,0.65)]";
|
|
75
|
+
case "attention":
|
|
76
|
+
return "bg-red-500 shadow-[0_0_9px_rgba(239,68,68,0.55)]";
|
|
77
|
+
case "idle":
|
|
78
|
+
return "bg-amber-400 shadow-[0_0_8px_rgba(251,191,36,0.45)]";
|
|
79
|
+
case "sleeping":
|
|
80
|
+
return "bg-muted-foreground/45";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function moodMotionClass(mood: InspectorPetMood): string {
|
|
85
|
+
switch (mood) {
|
|
86
|
+
case "active":
|
|
87
|
+
return "inspector-pet-active";
|
|
88
|
+
case "attention":
|
|
89
|
+
return "inspector-pet-attention";
|
|
90
|
+
case "idle":
|
|
91
|
+
return "";
|
|
92
|
+
case "sleeping":
|
|
93
|
+
return "inspector-pet-sleeping";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function InspectorPet({ logs }: InspectorPetProps): JSX.Element {
|
|
98
|
+
const [viewport, setViewport] = useState<InspectorPetViewport>(readViewport);
|
|
99
|
+
const [position, setPosition] = useState<InspectorPetPosition>({
|
|
100
|
+
x: INSPECTOR_PET_MARGIN,
|
|
101
|
+
y: INSPECTOR_PET_MARGIN,
|
|
102
|
+
});
|
|
103
|
+
const [clockMs, setClockMs] = useState(() => Date.now());
|
|
104
|
+
const [motionTick, setMotionTick] = useState(0);
|
|
105
|
+
const [panelTick, setPanelTick] = useState(0);
|
|
106
|
+
const [panelVisible, setPanelVisible] = useState(false);
|
|
107
|
+
const [reactionTick, setReactionTick] = useState(0);
|
|
108
|
+
const [reactionVisible, setReactionVisible] = useState(false);
|
|
109
|
+
const [dragging, setDragging] = useState(false);
|
|
110
|
+
const [roaming, setRoaming] = useState(readStoredRoaming);
|
|
111
|
+
const [reduceMotion, setReduceMotion] = useState(false);
|
|
112
|
+
const dragStateRef = useRef<InspectorPetDragState | null>(null);
|
|
113
|
+
const dragOccurredRef = useRef(false);
|
|
114
|
+
|
|
115
|
+
const status = useMemo(() => resolveInspectorPetStatus(logs, clockMs), [clockMs, logs]);
|
|
116
|
+
const panelPosition = useMemo(
|
|
117
|
+
() => inspectorPetPanelPosition(position, viewport),
|
|
118
|
+
[position, viewport],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const nextViewport = readViewport();
|
|
123
|
+
setViewport(nextViewport);
|
|
124
|
+
setPosition(randomInspectorPetEdgePosition(nextViewport, Math.random(), Math.random()));
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
129
|
+
const handleChange = (event: MediaQueryListEvent): void => setReduceMotion(event.matches);
|
|
130
|
+
setReduceMotion(mediaQuery.matches);
|
|
131
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
132
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const intervalId = window.setInterval(() => setClockMs(Date.now()), 15_000);
|
|
137
|
+
return () => window.clearInterval(intervalId);
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
try {
|
|
142
|
+
window.localStorage.setItem(INSPECTOR_PET_ROAMING_STORAGE_KEY, roaming ? "1" : "0");
|
|
143
|
+
} catch {
|
|
144
|
+
// Local storage can be unavailable in hardened browser profiles.
|
|
145
|
+
}
|
|
146
|
+
}, [roaming]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!roaming || dragging || reduceMotion) return undefined;
|
|
150
|
+
let timeoutId: number | null = null;
|
|
151
|
+
|
|
152
|
+
const scheduleDrift = (): void => {
|
|
153
|
+
const delay = INSPECTOR_PET_MIN_DRIFT_MS + Math.random() * INSPECTOR_PET_DRIFT_WINDOW_MS;
|
|
154
|
+
timeoutId = window.setTimeout(() => {
|
|
155
|
+
setMotionTick((current) => current + 1);
|
|
156
|
+
setPosition((current) =>
|
|
157
|
+
nextInspectorPetWalkingPosition(current, viewport, Math.random(), Math.random()),
|
|
158
|
+
);
|
|
159
|
+
scheduleDrift();
|
|
160
|
+
}, delay);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
scheduleDrift();
|
|
164
|
+
return () => {
|
|
165
|
+
if (timeoutId !== null) window.clearTimeout(timeoutId);
|
|
166
|
+
};
|
|
167
|
+
}, [dragging, reduceMotion, roaming, viewport]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const handleResize = (): void => {
|
|
171
|
+
const nextViewport = readViewport();
|
|
172
|
+
setViewport(nextViewport);
|
|
173
|
+
setPosition((current) => snapInspectorPetToEdge(current, nextViewport));
|
|
174
|
+
};
|
|
175
|
+
window.addEventListener("resize", handleResize);
|
|
176
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!panelVisible) return undefined;
|
|
181
|
+
const timeoutId = window.setTimeout(
|
|
182
|
+
() => setPanelVisible(false),
|
|
183
|
+
INSPECTOR_PET_PANEL_DISMISS_MS,
|
|
184
|
+
);
|
|
185
|
+
return () => window.clearTimeout(timeoutId);
|
|
186
|
+
}, [panelTick, panelVisible]);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!reactionVisible) return undefined;
|
|
190
|
+
const timeoutId = window.setTimeout(() => setReactionVisible(false), INSPECTOR_PET_REACTION_MS);
|
|
191
|
+
return () => window.clearTimeout(timeoutId);
|
|
192
|
+
}, [reactionTick, reactionVisible]);
|
|
193
|
+
|
|
194
|
+
const handlePet = useCallback(() => {
|
|
195
|
+
if (dragOccurredRef.current) {
|
|
196
|
+
dragOccurredRef.current = false;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setClockMs(Date.now());
|
|
200
|
+
setPanelTick((current) => current + 1);
|
|
201
|
+
setPanelVisible(true);
|
|
202
|
+
setReactionTick((current) => current + 1);
|
|
203
|
+
setReactionVisible(true);
|
|
204
|
+
}, []);
|
|
205
|
+
|
|
206
|
+
const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
207
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
208
|
+
dragOccurredRef.current = false;
|
|
209
|
+
dragStateRef.current = {
|
|
210
|
+
pointerId: event.pointerId,
|
|
211
|
+
offsetX: event.clientX - rect.left,
|
|
212
|
+
offsetY: event.clientY - rect.top,
|
|
213
|
+
startX: event.clientX,
|
|
214
|
+
startY: event.clientY,
|
|
215
|
+
moved: false,
|
|
216
|
+
};
|
|
217
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
218
|
+
setPanelVisible(false);
|
|
219
|
+
setDragging(true);
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
const handlePointerMove = useCallback(
|
|
223
|
+
(event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
224
|
+
const dragState = dragStateRef.current;
|
|
225
|
+
if (dragState === null || dragState.pointerId !== event.pointerId) return;
|
|
226
|
+
const moved =
|
|
227
|
+
dragState.moved ||
|
|
228
|
+
Math.abs(event.clientX - dragState.startX) >= INSPECTOR_PET_DRAG_THRESHOLD ||
|
|
229
|
+
Math.abs(event.clientY - dragState.startY) >= INSPECTOR_PET_DRAG_THRESHOLD;
|
|
230
|
+
dragState.moved = moved;
|
|
231
|
+
dragOccurredRef.current = moved;
|
|
232
|
+
setPosition(
|
|
233
|
+
clampInspectorPetPosition(
|
|
234
|
+
{
|
|
235
|
+
x: event.clientX - dragState.offsetX,
|
|
236
|
+
y: event.clientY - dragState.offsetY,
|
|
237
|
+
},
|
|
238
|
+
viewport,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
[viewport],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const handlePointerUp = useCallback(
|
|
246
|
+
(event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
247
|
+
const dragState = dragStateRef.current;
|
|
248
|
+
if (dragState === null || dragState.pointerId !== event.pointerId) return;
|
|
249
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
250
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
251
|
+
}
|
|
252
|
+
dragOccurredRef.current = dragState.moved;
|
|
253
|
+
dragStateRef.current = null;
|
|
254
|
+
setDragging(false);
|
|
255
|
+
setPosition((current) => snapInspectorPetToEdge(current, viewport));
|
|
256
|
+
},
|
|
257
|
+
[viewport],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const handlePointerCancel = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
261
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
262
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
263
|
+
}
|
|
264
|
+
dragStateRef.current = null;
|
|
265
|
+
dragOccurredRef.current = false;
|
|
266
|
+
setDragging(false);
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
const petStyle = useMemo<CSSProperties>(
|
|
270
|
+
() => ({
|
|
271
|
+
transform: `translate3d(${String(position.x)}px, ${String(position.y)}px, 0) rotate(${
|
|
272
|
+
motionTick % 2 === 0 ? "-4deg" : "5deg"
|
|
273
|
+
}) scale(${dragging ? "1.08" : "1"})`,
|
|
274
|
+
transition: dragging ? "none" : "transform 4800ms cubic-bezier(0.33, 1, 0.68, 1)",
|
|
275
|
+
touchAction: "none",
|
|
276
|
+
}),
|
|
277
|
+
[dragging, motionTick, position.x, position.y],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const panelStyle = useMemo<CSSProperties>(
|
|
281
|
+
() => ({ left: panelPosition.x, top: panelPosition.y }),
|
|
282
|
+
[panelPosition.x, panelPosition.y],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const roamingLabel = roaming ? "Pause pet roaming" : "Resume pet roaming";
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
className={cn(
|
|
292
|
+
"fixed left-0 top-0 z-30 flex size-14 items-center justify-center bg-transparent text-foreground outline-none transition-[filter,opacity] duration-200 hover:opacity-95 hover:drop-shadow-[0_0_24px_rgba(245,158,11,0.36)] focus-visible:drop-shadow-[0_0_12px_rgba(251,191,36,0.72)]",
|
|
293
|
+
dragging ? "cursor-grabbing" : "cursor-grab",
|
|
294
|
+
)}
|
|
295
|
+
style={petStyle}
|
|
296
|
+
data-pet-mood={status.mood}
|
|
297
|
+
onClick={handlePet}
|
|
298
|
+
onPointerDown={handlePointerDown}
|
|
299
|
+
onPointerMove={handlePointerMove}
|
|
300
|
+
onPointerUp={handlePointerUp}
|
|
301
|
+
onPointerCancel={handlePointerCancel}
|
|
302
|
+
aria-label={`Inspector pet: ${status.label}`}
|
|
303
|
+
title={status.label}
|
|
304
|
+
>
|
|
305
|
+
<span
|
|
306
|
+
className={cn(
|
|
307
|
+
"relative flex size-12 items-center justify-center",
|
|
308
|
+
reactionVisible && "inspector-pet-patted",
|
|
309
|
+
)}
|
|
310
|
+
aria-hidden="true"
|
|
311
|
+
>
|
|
312
|
+
<span
|
|
313
|
+
className={cn(
|
|
314
|
+
"relative flex size-12 items-center justify-center",
|
|
315
|
+
moodMotionClass(status.mood),
|
|
316
|
+
)}
|
|
317
|
+
>
|
|
318
|
+
<span
|
|
319
|
+
className={cn(
|
|
320
|
+
"relative flex size-12 items-center justify-center",
|
|
321
|
+
!dragging && "floating-crab-body",
|
|
322
|
+
)}
|
|
323
|
+
>
|
|
324
|
+
<CrabLogo className="relative z-10 size-11 text-amber-500 drop-shadow-[0_0_15px_rgba(245,158,11,0.30)]" />
|
|
325
|
+
<span className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex items-end justify-center gap-1 text-amber-400/80">
|
|
326
|
+
{[0, 1, 2, 3, 4, 5].map((leg) => (
|
|
327
|
+
<span
|
|
328
|
+
key={leg}
|
|
329
|
+
className={cn(
|
|
330
|
+
"floating-crab-leg h-2 w-0.5 rounded-full bg-current shadow-[0_0_7px_rgba(245,158,11,0.24)]",
|
|
331
|
+
leg < 3 ? "floating-crab-leg-left" : "floating-crab-leg-right",
|
|
332
|
+
dragging && "animate-none",
|
|
333
|
+
)}
|
|
334
|
+
style={{ animationDelay: `${String(leg % 2 === 0 ? 0 : 180)}ms` }}
|
|
335
|
+
/>
|
|
336
|
+
))}
|
|
337
|
+
</span>
|
|
338
|
+
</span>
|
|
339
|
+
</span>
|
|
340
|
+
<span
|
|
341
|
+
className={cn(
|
|
342
|
+
"absolute right-0 top-1 size-2 rounded-full border border-background/90",
|
|
343
|
+
moodDotClass(status.mood),
|
|
344
|
+
status.mood === "active" && !reduceMotion && "animate-pulse",
|
|
345
|
+
)}
|
|
346
|
+
/>
|
|
347
|
+
{reactionVisible && (
|
|
348
|
+
<Heart
|
|
349
|
+
key={reactionTick}
|
|
350
|
+
className="inspector-pet-heart absolute -right-1 -top-2 size-3.5 fill-amber-400 text-amber-400"
|
|
351
|
+
/>
|
|
352
|
+
)}
|
|
353
|
+
</span>
|
|
354
|
+
</button>
|
|
355
|
+
|
|
356
|
+
{panelVisible && (
|
|
357
|
+
<div
|
|
358
|
+
className="fixed z-50 w-48 rounded-md border border-border/80 bg-popover/95 px-3 py-2 text-popover-foreground shadow-[0_14px_34px_rgba(0,0,0,0.28)] backdrop-blur"
|
|
359
|
+
style={panelStyle}
|
|
360
|
+
role="status"
|
|
361
|
+
aria-live="polite"
|
|
362
|
+
>
|
|
363
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
364
|
+
<span className={cn("size-2 shrink-0 rounded-full", moodDotClass(status.mood))} />
|
|
365
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium">{status.label}</span>
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
className="inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
369
|
+
onClick={() => setRoaming((current) => !current)}
|
|
370
|
+
aria-label={roamingLabel}
|
|
371
|
+
title={roamingLabel}
|
|
372
|
+
>
|
|
373
|
+
{roaming ? <Pause className="size-3.5" /> : <Play className="size-3.5" />}
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
<div className="mt-1 flex items-center justify-between gap-3 font-mono text-[10px] text-muted-foreground">
|
|
377
|
+
<span>{String(status.capturedCount)} captured</span>
|
|
378
|
+
<span>
|
|
379
|
+
{status.pendingCount > 0
|
|
380
|
+
? `${String(status.pendingCount)} active`
|
|
381
|
+
: roaming
|
|
382
|
+
? "Roaming"
|
|
383
|
+
: "Resting"}
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</>
|
|
389
|
+
);
|
|
390
|
+
}
|