evOo-local/apps/mobile/app/routine.tsx
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

141 lines
4.5 KiB
TypeScript

import type { Routine } from "@rakazo/contracts";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
import { rpc } from "../lib/api";
import { useI18n } from "../lib/i18n";
import { useMobileTokens } from "../lib/native";
export default function RoutineDetail() {
const tokens = useMobileTokens();
const { t } = useI18n();
const { botId, botName, routineId } = useLocalSearchParams<{
botId?: string;
botName?: string;
routineId?: string;
}>();
const router = useRouter();
const [routine, setRoutine] = useState<Routine | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!botId || !routineId) {
setError(t("Routine link is incomplete"));
setLoading(false);
return;
}
let cancelled = false;
setLoading(true);
void rpc<Routine[]>("routines/list", { botId })
.then((routines) => {
if (cancelled) return;
const match = routines.find((item) => item.id === routineId);
if (match) setRoutine(match);
else setError(t("This routine no longer exists"));
})
.catch((loadError) => {
if (!cancelled) {
setError(loadError instanceof Error ? loadError.message : t("Could not load routine"));
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [botId, routineId]);
return (
<ScrollView
style={{ flex: 1, backgroundColor: tokens.background }}
contentContainerStyle={{ padding: 24, gap: 18 }}
>
<Stack.Screen options={{ title: routine?.name ?? t("Routine") }} />
{loading ? <ActivityIndicator color={tokens.mutedForeground} /> : null}
{error ? <Text style={{ color: tokens.destructive, fontSize: 15 }}>{error}</Text> : null}
{routine ? (
<>
<View
style={{
borderRadius: 16,
borderWidth: 1,
borderColor: tokens.border,
backgroundColor: tokens.card,
padding: 18,
gap: 8,
}}
>
<Text style={{ color: tokens.foreground, fontSize: 20, fontWeight: "600" }}>
{routine.name}
</Text>
<Text
style={{
color: routine.active ? tokens.success : tokens.mutedForeground,
fontSize: 14,
}}
>
{[
routine.active ? t("Active") : t("Paused"),
[
...routine.crons,
...(routine.webhookEnabled ? [t("Webhook")] : []),
...(routine.githubEnabled ? [t("Git event")] : []),
...(routine.messageProvider === "slack"
? [t("Slack message")]
: routine.messageProvider === "teams"
? [t("Teams message")]
: routine.messageProvider
? [t("Message event")]
: []),
].join(", "),
routine.timezone,
].join(" · ")}
</Text>
</View>
<View style={{ gap: 8 }}>
<Text
style={{ color: tokens.mutedForeground, fontSize: 13, textTransform: "uppercase" }}
>
{t("Prompt")}
</Text>
<Text
selectable
style={{
color: tokens.foreground,
fontSize: 15,
lineHeight: 23,
borderRadius: 16,
backgroundColor: tokens.card,
padding: 18,
}}
>
{routine.prompt}
</Text>
</View>
<Pressable
accessibilityRole="button"
onPress={() =>
router.push({
pathname: "/thread",
params: { botId: botId ?? "", name: botName ?? t("Bot") },
})
}
style={{
alignItems: "center",
borderRadius: 12,
backgroundColor: tokens.primary,
padding: 14,
}}
>
<Text style={{ color: tokens.primaryForeground, fontSize: 15, fontWeight: "600" }}>
{t("Open conversation")}
</Text>
</Pressable>
</>
) : null}
</ScrollView>
);
}