Files
hapi/web/src/router.tsx
T
weishu 3bac5c7bc6 feat(web): integrate TanStack React Router for client-side routing
Replace state-based screen navigation with proper URL-based routing. This includes:
- New router configuration with routes for sessions, machines, and spawn pages
- App context provider to share API and token across the app
- useAppGoBack hook for handling browser and Telegram back navigation
- Refactored App component to render outlet and use router hooks
- Memory history for Telegram app, browser history for web
2025-12-20 16:49:10 +08:00

229 lines
6.5 KiB
TypeScript

import { useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import {
Navigate,
createRootRoute,
createRoute,
createRouter,
useNavigate,
useParams,
} from '@tanstack/react-router'
import { App } from '@/App'
import { SessionChat } from '@/components/SessionChat'
import { SessionList } from '@/components/SessionList'
import { MachineList } from '@/components/MachineList'
import { SpawnSession } from '@/components/SpawnSession'
import { useAppContext } from '@/lib/app-context'
import { useAppGoBack } from '@/hooks/useAppGoBack'
import { useMessages } from '@/hooks/queries/useMessages'
import { useMachines } from '@/hooks/queries/useMachines'
import { useSession } from '@/hooks/queries/useSession'
import { useSessions } from '@/hooks/queries/useSessions'
import { useSendMessage } from '@/hooks/mutations/useSendMessage'
import { queryKeys } from '@/lib/query-keys'
function SessionsPage() {
const { api } = useAppContext()
const navigate = useNavigate()
const { sessions, isLoading, error, refetch } = useSessions(api)
const handleRefresh = useCallback(() => {
void refetch()
}, [refetch])
return (
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{error ? <div className="text-sm text-red-600">{error}</div> : null}
<SessionList
sessions={sessions}
onSelect={(sessionId) => navigate({
to: '/sessions/$sessionId',
params: { sessionId },
})}
onNewSession={() => navigate({ to: '/machines' })}
onRefresh={handleRefresh}
isLoading={isLoading}
/>
</div>
)
}
function SessionPage() {
const { api } = useAppContext()
const goBack = useAppGoBack()
const { sessionId } = useParams({ from: '/sessions/$sessionId' })
const {
session,
refetch: refetchSession,
} = useSession(api, sessionId)
const {
messages,
warning: messagesWarning,
isLoading: messagesLoading,
isLoadingMore: messagesLoadingMore,
hasMore: messagesHasMore,
loadMore: loadMoreMessages,
refetch: refetchMessages,
} = useMessages(api, sessionId)
const {
sendMessage,
retryMessage,
isSending,
} = useSendMessage(api, sessionId)
const refreshSelectedSession = useCallback(() => {
void refetchSession()
void refetchMessages()
}, [refetchMessages, refetchSession])
if (!session) {
return (
<div className="p-4 text-sm text-[var(--app-hint)]">
Loading session
</div>
)
}
return (
<SessionChat
api={api}
session={session}
messages={messages}
messagesWarning={messagesWarning}
hasMoreMessages={messagesHasMore}
isLoadingMessages={messagesLoading}
isLoadingMoreMessages={messagesLoadingMore}
isSending={isSending}
onBack={goBack}
onRefresh={refreshSelectedSession}
onLoadMore={() => {
void loadMoreMessages()
}}
onSend={sendMessage}
onRetryMessage={retryMessage}
/>
)
}
function MachinesPage() {
const { api } = useAppContext()
const navigate = useNavigate()
const { machines, error: machinesError } = useMachines(api, true)
return (
<div className="flex-1 overflow-y-auto">
<div className="flex items-center gap-2 border-b border-[var(--app-border)] bg-[var(--app-bg)] p-3">
<div className="flex-1 font-semibold">Machines</div>
</div>
{machinesError ? (
<div className="p-3 text-sm text-red-600">
{machinesError}
</div>
) : null}
<MachineList
machines={machines}
onSelect={(machineId) => navigate({
to: '/machines/$machineId/spawn',
params: { machineId },
})}
/>
</div>
)
}
function SpawnPage() {
const { api } = useAppContext()
const { machineId } = useParams({ from: '/machines/$machineId/spawn' })
const { machines } = useMachines(api, true)
const navigate = useNavigate()
const queryClient = useQueryClient()
const machineForSpawn = machines.find((machine) => machine.id === machineId) ?? null
const handleCancel = useCallback(() => {
navigate({ to: '/machines' })
}, [navigate])
const handleSuccess = useCallback((sessionId: string) => {
void queryClient.invalidateQueries({ queryKey: queryKeys.sessions })
navigate({
to: '/sessions/$sessionId',
params: { sessionId },
})
}, [navigate, queryClient])
return (
<div className="flex-1 overflow-y-auto">
<div className="flex items-center gap-2 border-b border-[var(--app-border)] bg-[var(--app-bg)] p-3">
<div className="flex-1 font-semibold">Create Session</div>
</div>
<SpawnSession
api={api}
machineId={machineId}
machine={machineForSpawn}
onCancel={handleCancel}
onSuccess={handleSuccess}
/>
</div>
)
}
const rootRoute = createRootRoute({
component: App,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <Navigate to="/sessions" replace />,
})
const sessionsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/sessions',
component: SessionsPage,
})
const sessionRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/sessions/$sessionId',
component: SessionPage,
})
const machinesRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/machines',
component: MachinesPage,
})
const spawnRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/machines/$machineId/spawn',
component: SpawnPage,
})
export const routeTree = rootRoute.addChildren([
indexRoute,
sessionsRoute,
sessionRoute,
machinesRoute,
spawnRoute,
])
type RouterHistory = Parameters<typeof createRouter>[0]['history']
export function createAppRouter(history?: RouterHistory) {
return createRouter({ routeTree, history })
}
export type AppRouter = ReturnType<typeof createAppRouter>
declare module '@tanstack/react-router' {
interface Register {
router: AppRouter
}
}