Showing the changes to apps/mobile/src/features/threads/ThreadQueueControl.tsx (1 of 16 files). Show all
Unified · Split
| @@ -0,0 +1,115 @@ | ||
| 1 | +import * as Haptics from "expo-haptics"; | |
| 2 | +import { SymbolView } from "expo-symbols"; | |
| 3 | +import { deriveThreadQueueWorkflowState } from "@t3tools/client-runtime/state/thread-workflows"; | |
| 4 | +import type { EnvironmentId, RunId, ThreadId } from "@t3tools/contracts"; | |
| 5 | +import { useMemo, useState } from "react"; | |
| 6 | +import { Pressable, ScrollView, View } from "react-native"; | |
| 7 | + | |
| 8 | +import { AppText as Text } from "../../components/AppText"; | |
| 9 | +import { useThemeColor } from "../../lib/useThemeColor"; | |
| 10 | +import { threadEnvironment } from "../../state/threads"; | |
| 11 | +import { useAtomCommand } from "../../state/use-atom-command"; | |
| 12 | +import { useThreadProjection } from "../../state/use-thread-detail"; | |
| 13 | + | |
| 14 | +export function ThreadQueueControl(props: { | |
| 15 | + readonly environmentId: EnvironmentId; | |
| 16 | + readonly threadId: ThreadId; | |
| 17 | +}) { | |
| 18 | + const scoped = useThreadProjection(props); | |
| 19 | + const workflow = useMemo( | |
| 20 | + () => (scoped ? deriveThreadQueueWorkflowState(scoped.projection) : null), | |
| 21 | + [scoped], | |
| 22 | + ); | |
| 23 | + const reorder = useAtomCommand(threadEnvironment.reorderQueuedRun, "reorder queued message"); | |
| 24 | + const promote = useAtomCommand(threadEnvironment.promoteQueuedRun, "promote queued message"); | |
| 25 | + const [busyRunId, setBusyRunId] = useState<RunId | null>(null); | |
| 26 | + const iconColor = useThemeColor("--color-icon-subtle"); | |
| 27 | + | |
| 28 | + if (!workflow || workflow.queuedRuns.length === 0) return null; | |
| 29 | + | |
| 30 | + const move = async (runId: RunId, beforeRunId: RunId | null) => { | |
| 31 | + setBusyRunId(runId); | |
| 32 | + void Haptics.selectionAsync(); | |
| 33 | + await reorder({ | |
| 34 | + environmentId: props.environmentId, | |
| 35 | + input: { threadId: props.threadId, runId, beforeRunId }, | |
| 36 | + }); | |
| 37 | + setBusyRunId(null); | |
| 38 | + }; | |
| 39 | + | |
| 40 | + const steer = async (queuedRunId: RunId) => { | |
| 41 | + if (!workflow.activeRun || !workflow.canPromoteToSteer) return; | |
| 42 | + setBusyRunId(queuedRunId); | |
| 43 | + void Haptics.selectionAsync(); | |
| 44 | + await promote({ | |
| 45 | + environmentId: props.environmentId, | |
| 46 | + input: { | |
| 47 | + threadId: props.threadId, | |
| 48 | + queuedRunId, | |
| 49 | + targetRunId: workflow.activeRun.id, | |
| 50 | + }, | |
| 51 | + }); | |
| 52 | + setBusyRunId(null); | |
| 53 | + }; | |
| 54 | + | |
| 55 | + return ( | |
| 56 | + <View className="mx-4 mb-3 overflow-hidden rounded-2xl border border-neutral-300/60 bg-card dark:border-white/[0.1]"> | |
| 57 | + <View className="flex-row items-center gap-2 border-b border-neutral-300/50 px-3 py-2 dark:border-white/[0.08]"> | |
| 58 | + <SymbolView name="list.number" size={13} tintColor={iconColor} type="monochrome" /> | |
| 59 | + <Text className="font-t3-medium text-xs text-foreground">Queue</Text> | |
| 60 | + <Text className="ml-auto text-2xs tabular-nums text-foreground-muted"> | |
| 61 | + {workflow.queuedRuns.length} | |
| 62 | + </Text> | |
| 63 | + </View> | |
| 64 | + <ScrollView style={{ maxHeight: 156 }} contentContainerStyle={{ paddingVertical: 4 }}> | |
| 65 | + {workflow.queuedRuns.map(({ run, text }, index) => ( | |
| 66 | + <View key={run.id} className="min-h-11 flex-row items-center gap-1.5 px-2"> | |
| 67 | + <Text className="w-5 text-right text-3xs tabular-nums text-foreground-muted"> | |
| 68 | + {index + 1} | |
| 69 | + </Text> | |
| 70 | + <Text className="min-w-0 flex-1 text-xs text-foreground" numberOfLines={1}> | |
| 71 | + {text} | |
| 72 | + </Text> | |
| 73 | + <Pressable | |
| 74 | + accessibilityRole="button" | |
| 75 | + accessibilityLabel="Move queued message up" | |
| 76 | + disabled={busyRunId !== null || !workflow.canReorder || index === 0} | |
| 77 | + onPress={() => void move(run.id, workflow.queuedRuns[index - 1]?.run.id ?? null)} | |
| 78 | + className="h-9 w-9 items-center justify-center disabled:opacity-30" | |
| 79 | + > | |
| 80 | + <SymbolView name="arrow.up" size={13} tintColor={iconColor} type="monochrome" /> | |
| 81 | + </Pressable> | |
| 82 | + <Pressable | |
| 83 | + accessibilityRole="button" | |
| 84 | + accessibilityLabel="Move queued message down" | |
| 85 | + disabled={ | |
| 86 | + busyRunId !== null || | |
| 87 | + !workflow.canReorder || | |
| 88 | + index === workflow.queuedRuns.length - 1 | |
| 89 | + } | |
| 90 | + onPress={() => void move(run.id, workflow.queuedRuns[index + 2]?.run.id ?? null)} | |
| 91 | + className="h-9 w-9 items-center justify-center disabled:opacity-30" | |
| 92 | + > | |
| 93 | + <SymbolView name="arrow.down" size={13} tintColor={iconColor} type="monochrome" /> | |
| 94 | + </Pressable> | |
| 95 | + <Pressable | |
| 96 | + accessibilityRole="button" | |
| 97 | + accessibilityLabel="Promote queued message to steer" | |
| 98 | + disabled={busyRunId !== null || !workflow.canPromoteToSteer} | |
| 99 | + onPress={() => void steer(run.id)} | |
| 100 | + className="min-h-8 flex-row items-center gap-1 rounded-lg border border-neutral-300/60 px-2 disabled:opacity-30 dark:border-white/[0.1]" | |
| 101 | + > | |
| 102 | + <SymbolView | |
| 103 | + name="arrow.turn.left.up" | |
| 104 | + size={12} | |
| 105 | + tintColor={iconColor} | |
| 106 | + type="monochrome" | |
| 107 | + /> | |
| 108 | + <Text className="font-t3-medium text-2xs text-foreground">Steer</Text> | |
| 109 | + </Pressable> | |
| 110 | + </View> | |
| 111 | + ))} | |
| 112 | + </ScrollView> | |
| 113 | + </View> | |
| 114 | + ); | |
| 115 | +} | |