Files
aris-old/aris/apps/companion/app/(tabs)/orchestrator.tsx

427 lines
10 KiB
TypeScript
Raw Normal View History

import type { ReactNode } from "react";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SafeAreaScrollView } from "@/components/safe-area-scroll-view";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Text } from "@/components/ui/text";
import { cn } from "@/lib/utils";
const fixture = {
location: {
authorization: "When In Use",
lastLocation: {
latitude: 37.33182,
longitude: -122.03118,
accuracyMeters: 12.4,
speedMps: 0.4,
},
},
recompute: {
lastReason: "manual",
lastTime: new Date("2024-05-21T09:32:00Z"),
elapsedMs: 482,
fetchFailed: false,
error: null as string | null,
},
winner: {
title: "Glass Now online",
subtitle: "Connected to iPhone",
type: "INFO",
bucket: "RIGHT_NOW",
priority: 0.8,
ttlSec: 86400,
poiType: "CAFE",
startsAt: 1767717000,
id: "demo:welcome",
},
feed: [
{
id: "demo:welcome",
title: "Glass Now online",
subtitle: "Connected to iPhone",
type: "INFO",
bucket: "RIGHT_NOW",
priority: 0.8,
ttlSec: 86400,
},
{
id: "cal:demo:1767717000",
title: "Team Sync",
subtitle: "",
type: "CALENDAR_EVENT",
bucket: "FYI",
priority: 0.7,
ttlSec: 5400,
},
{
id: "demo:next",
title: "Next: Calendar",
subtitle: "Then Weather + POI",
type: "INFO",
bucket: "FYI",
priority: 0.4,
ttlSec: 86400,
},
{
id: "music:now:demo",
title: "Midnight City",
subtitle: "M83 - Hurry Up, We're Dreaming",
type: "NOW_PLAYING",
bucket: "FYI",
priority: 0.35,
ttlSec: 30,
},
],
nowPlaying: {
auth: "Authorized",
title: "Midnight City",
artist: "M83",
album: "Hurry Up, We're Dreaming",
playbackStatus: "playing",
},
weatherDiagnostics: {
provider: "WeatherKit",
last_fetch: "2024-05-21 09:30:14",
alerts: "0",
conditions: "mostly_clear_day",
},
calendarDiagnostics: {
events_upcoming: "3",
next_event: "Team Sync - 10:30",
},
test: {
note: "Test actions are not available in the React Native client.",
},
};
const formatTime = (date: Date | null) => {
if (!date) {
return "--";
}
return date.toLocaleTimeString();
};
const formatSpeed = (speedMps: number) => {
if (speedMps < 0) {
return "--";
}
return `${speedMps.toFixed(1)} m/s`;
};
const formatLatLon = (value: number) => value.toFixed(5);
const formatPriority = (value: number) => value.toFixed(2);
export default function OrchestratorScreen() {
const insets = useSafeAreaInsets();
return (
<View className="flex flex-1 bg-white">
<SafeAreaScrollView>
<View className="gap-4" style={{ paddingBottom: insets.bottom + 24 }}>
<View className="gap-1 px-8">
<Text variant="h3">Orchestrator</Text>
<Text className="text-sm text-muted-foreground">
Context engine status and diagnostics.
</Text>
</View>
<View className="w-full h-px bg-muted" />
<LocationSection />
<View className="w-full h-px bg-muted" />
<RecomputeSection />
<View className="w-full h-px bg-muted" />
<WinnerSection />
<View className="w-full h-px bg-muted" />
<FeedSection />
<View className="w-full h-px bg-muted" />
<NowPlayingSection />
<View className="w-full h-px bg-muted" />
<DiagnosticsSection
title="Weather Diagnostics"
entries={fixture.weatherDiagnostics}
/>
<View className="w-full h-px bg-muted" />
<DiagnosticsSection
title="Calendar Diagnostics"
entries={fixture.calendarDiagnostics}
/>
<View className="w-full h-px bg-muted" />
<TestSection />
</View>
</SafeAreaScrollView>
</View>
);
}
function ValueRow({ label, value }: { label: ReactNode; value: ReactNode }) {
return (
<View className="flex flex-row items-center justify-between">
{typeof label === "string" ? (
<Text className="text-sm text-muted-foreground">{label}</Text>
) : (
label
)}
{typeof value === "string" || typeof value === "number" ? (
<Text className="text-sm">{value}</Text>
) : (
value
)}
</View>
);
}
function LocationSection() {
const { authorization, lastLocation } = fixture.location;
return (
<View className="px-8">
<Text variant="h4" className="mb-2 font-medium">
Location
</Text>
<View className="gap-1">
<ValueRow label="Auth" value={authorization} />
{lastLocation ? (
<>
<ValueRow
label="Lat/Lon"
value={
<Text selectable className="text-sm">
{`${formatLatLon(lastLocation.latitude)}, ${formatLatLon(lastLocation.longitude)}`}
</Text>
}
/>
<ValueRow
label="Accuracy"
value={`${Math.round(lastLocation.accuracyMeters)} m`}
/>
<ValueRow
label="Speed"
value={formatSpeed(lastLocation.speedMps)}
/>
</>
) : (
<Text className="text-sm text-muted-foreground">No location yet</Text>
)}
</View>
</View>
);
}
function RecomputeSection() {
const { lastReason, lastTime, elapsedMs, fetchFailed, error } =
fixture.recompute;
return (
<View className="px-8">
<Text variant="h4" className="mb-2 font-medium">
Recompute
</Text>
<View className="gap-1">
<ValueRow label="Last reason" value={lastReason || "--"} />
<ValueRow label="Last time" value={formatTime(lastTime)} />
<ValueRow label="Elapsed" value={`${elapsedMs} ms`} />
<ValueRow label="Fetch failed" value={fetchFailed ? "Yes" : "No"} />
{error ? (
<Text className="text-xs text-muted-foreground">{error}</Text>
) : null}
</View>
<Button onPress={() => {}} variant="outline" className="mt-3">
<Text>Recompute Now</Text>
</Button>
</View>
);
}
function WinnerSection() {
const winner = fixture.winner;
return (
<View>
<Text variant="h4" className="mb-2 font-medium px-8">
Winner
</Text>
{winner ? (
<Card className="mx-4 gap-2 pb-2">
<CardHeader className="gap-0 border-b border-border pb-2 flex-row items-start justify-between">
<View className="gap-1.5">
<CardTitle className="flex-1">{winner.title}</CardTitle>
<CardDescription>
{winner.subtitle ?? "No description"}
</CardDescription>
</View>
<Badge variant="default">
<Text>{winner.type}</Text>
</Badge>
</CardHeader>
<CardContent className="gap-1">
<ValueRow label="Bucket" value={winner.bucket} />
<ValueRow
label="Priority"
value={formatPriority(winner.priority)}
/>
<ValueRow label="TTL" value={`${winner.ttlSec}s`} />
{winner.poiType ? (
<ValueRow label="POI type" value={winner.poiType} />
) : null}
{winner.startsAt ? (
<ValueRow label="Starts at" value={`${winner.startsAt}`} />
) : null}
<ValueRow
label="ID"
value={
<Text selectable className="text-xs text-muted-foreground">
{winner.id}
</Text>
}
/>
</CardContent>
</Card>
) : (
<Text className="text-sm text-muted-foreground">No winner yet</Text>
)}
</View>
);
}
function FeedSection() {
const feed = fixture.feed;
return (
<View>
<Text variant="h4" className="mb-2 font-medium px-8">
Feed
</Text>
{feed.length === 0 ? (
<Text className="text-sm text-muted-foreground">No feed items yet</Text>
) : (
<View className="px-4">
{feed.map((item, i) => (
<View
key={item.id}
className={cn("px-4 py-2 gap-1 border border-b-0 border-border", {
"rounded-t-lg": i === 0,
"rounded-b-lg border-b": i === feed.length - 1,
})}
>
<View className="flex-row items-center justify-between gap-2">
<View className="gap-1">
<Text
className="text-sm font-semibold flex-1"
numberOfLines={1}
>
{item.title}
</Text>
<Text
className="text-xs text-muted-foreground"
numberOfLines={1}
>
{item.subtitle || "No description"}
</Text>
<Text className="text-xs text-muted-foreground">
{`bucket ${item.bucket} | prio ${formatPriority(item.priority)} | ttl ${item.ttlSec}s`}
</Text>
</View>
<Badge variant="secondary">
<Text>{item.type}</Text>
</Badge>
</View>
</View>
))}
</View>
)}
</View>
);
}
function NowPlayingSection() {
const { auth, title, artist, album, playbackStatus } = fixture.nowPlaying;
const subtitle = [artist, album].filter(Boolean).join(" | ") || "Apple Music";
return (
<View className="px-8">
<Text variant="h4" className="mb-2 font-medium">
Now Playing
</Text>
<View className="gap-1">
<ValueRow label="Music auth" value={auth} />
{title ? (
<>
<Text className="text-sm font-semibold" numberOfLines={1}>
{title}
</Text>
<Text className="text-xs text-muted-foreground" numberOfLines={1}>
{subtitle}
</Text>
<Text className="text-xs text-muted-foreground">
{playbackStatus}
</Text>
</>
) : (
<Text className="text-sm text-muted-foreground">
{auth === "Authorized" ? "Nothing playing" : "Not authorized"}
</Text>
)}
</View>
</View>
);
}
function DiagnosticsSection({
title,
entries,
}: {
title: string;
entries: Record<string, string>;
}) {
const keys = Object.keys(entries).sort();
return (
<View className="px-8">
<Text variant="h4" className="mb-2 font-medium">
{title}
</Text>
<View className="gap-1">
{keys.map((key) => (
<ValueRow
key={key}
label={key}
value={
<Text selectable className="text-xs text-muted-foreground">
{entries[key]}
</Text>
}
/>
))}
</View>
</View>
);
}
function TestSection() {
return (
<View className="px-8">
<Text variant="h4" className="mb-2 font-medium">
Test
</Text>
<Text className="text-sm text-muted-foreground">{fixture.test.note}</Text>
</View>
);
}