mirror of
https://github.com/hanzoai/mobile.git
synced 2026-10-30 14:53:17 +00:00
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { Button, ScrollView, Spinner, Text, XStack, YStack } from '@hanzo/gui'
|
|
import { ImagePlus } from '@hanzogui/lucide-icons-2'
|
|
import { router } from 'expo-router'
|
|
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
|
import { RefreshControl } from 'react-native'
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
|
|
import { Note, Refuse } from '@/components/tasks/note'
|
|
import { Run } from '@/components/tasks/run'
|
|
import { Session } from '@/components/tasks/session'
|
|
import * as runs from '@/lib/runs'
|
|
import * as sessions from '@/lib/sessions'
|
|
import { dim, fg } from '@/lib/theme'
|
|
|
|
function Label({ children }: { children: string }) {
|
|
return (
|
|
<Text color="$color11" fontSize={12} fontWeight="600" letterSpacing={1.2} mt="$2">
|
|
{children.toUpperCase()}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
export default function Tasks() {
|
|
const insets = useSafeAreaInsets()
|
|
const live = useSyncExternalStore(runs.subscribe, runs.live)
|
|
const history = useSyncExternalStore(runs.subscribe, runs.history)
|
|
|
|
// null is the first load only; afterwards the last answer stays visible
|
|
// while a pull refreshes it.
|
|
const [cloud, setCloud] = useState<sessions.Load | null>(null)
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
setCloud(await sessions.load())
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
load()
|
|
}, [load])
|
|
|
|
const refresh = useCallback(async () => {
|
|
setRefreshing(true)
|
|
try {
|
|
await load()
|
|
} finally {
|
|
setRefreshing(false)
|
|
}
|
|
}, [load])
|
|
|
|
return (
|
|
<ScrollView
|
|
flex={1}
|
|
bg="$background"
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={refresh} tintColor={dim} colors={[fg]} />
|
|
}
|
|
contentContainerStyle={{ p: 20, pt: insets.top + 16, pb: 32 }}
|
|
>
|
|
<YStack gap="$3">
|
|
<XStack items="center" justify="space-between">
|
|
<Text color="$color" fontSize={28} fontWeight="700">
|
|
Tasks
|
|
</Text>
|
|
<Button
|
|
chromeless
|
|
circular
|
|
icon={<ImagePlus size={20} color="$color11" />}
|
|
onPress={() => router.push('/image')}
|
|
aria-label="Generate an image"
|
|
/>
|
|
</XStack>
|
|
|
|
<Label>Local runs</Label>
|
|
{live.length === 0 ? (
|
|
<Note
|
|
title="Nothing is running"
|
|
detail="Work this app starts — a chat turn, an image, a voice session — shows here live."
|
|
/>
|
|
) : (
|
|
live.map((run) => <Run key={run.id} run={run} />)
|
|
)}
|
|
{history.length > 0 ? (
|
|
<>
|
|
<Label>Recent</Label>
|
|
{history.map((run) => (
|
|
<Run key={run.id} run={run} />
|
|
))}
|
|
</>
|
|
) : null}
|
|
|
|
<Label>Cloud sessions</Label>
|
|
{cloud === null ? (
|
|
<XStack p="$4" justify="center">
|
|
<Spinner color="$color11" />
|
|
</XStack>
|
|
) : cloud.kind === 'anon' ? (
|
|
<Note
|
|
title="Signed out"
|
|
detail="Cloud sessions belong to an account. Add a credential in Settings to see yours."
|
|
>
|
|
<Button mt="$3" self="flex-start" size="$3" onPress={() => router.push('/(tabs)/settings')}>
|
|
Open Settings
|
|
</Button>
|
|
</Note>
|
|
) : cloud.kind === 'refused' ? (
|
|
<Refuse refusal={cloud.refusal} />
|
|
) : cloud.sessions.length === 0 ? (
|
|
<Note
|
|
title="No cloud sessions"
|
|
detail="Agents running in the Hanzo cloud land here. None exist for this account right now."
|
|
/>
|
|
) : (
|
|
cloud.sessions.map((session) => <Session key={session.id} session={session} />)
|
|
)}
|
|
</YStack>
|
|
</ScrollView>
|
|
)
|
|
}
|