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 | } | ||