refactor: replace InboxPage with redirect to /hq
Converts /inbox from a 270-line interactive page to a minimal TanStack Router redirect route. Bookmarked or externally linked /inbox URLs now redirect cleanly to /hq instead of 404-ing. InboxList and InboxDetailPanel components are preserved for reuse in the HQ page (Phase 569ZNKArI1OYRolaOZLhB). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,269 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { motion } from "motion/react";
|
||||
import { AlertCircle, MessageSquare } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/Skeleton";
|
||||
import { toast } from "sonner";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { InboxList } from "@/components/InboxList";
|
||||
import { InboxDetailPanel } from "@/components/InboxDetailPanel";
|
||||
import { useLiveUpdates } from "@/hooks";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/inbox")({
|
||||
component: InboxPage,
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: "/hq" });
|
||||
},
|
||||
});
|
||||
|
||||
function InboxPage() {
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
|
||||
// Single SSE stream for live updates
|
||||
useLiveUpdates([
|
||||
{ prefix: 'agent:', invalidate: ['listWaitingAgents', 'listMessages'] },
|
||||
]);
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
// Data fetching
|
||||
const agentsQuery = trpc.listWaitingAgents.useQuery();
|
||||
const messagesQuery = trpc.listMessages.useQuery({});
|
||||
const questionsQuery = trpc.getAgentQuestions.useQuery(
|
||||
{ id: selectedAgentId! },
|
||||
{ enabled: !!selectedAgentId }
|
||||
);
|
||||
|
||||
// Mutations
|
||||
const resumeAgentMutation = trpc.resumeAgent.useMutation({
|
||||
onSuccess: () => {
|
||||
setSelectedAgentId(null);
|
||||
toast.success("Answer submitted");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to submit answer");
|
||||
},
|
||||
});
|
||||
|
||||
const dismissQuestionsMutation = trpc.stopAgent.useMutation({
|
||||
onSuccess: () => {
|
||||
setSelectedAgentId(null);
|
||||
toast.success("Questions dismissed");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to dismiss questions");
|
||||
},
|
||||
});
|
||||
|
||||
const respondToMessageMutation = trpc.respondToMessage.useMutation({
|
||||
onSuccess: () => {
|
||||
setSelectedAgentId(null);
|
||||
toast.success("Response sent");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to send response");
|
||||
},
|
||||
});
|
||||
|
||||
// Find selected agent info
|
||||
const agents = agentsQuery.data ?? [];
|
||||
const messages = messagesQuery.data ?? [];
|
||||
const selectedAgent = selectedAgentId
|
||||
? agents.find((a) => a.id === selectedAgentId) ?? null
|
||||
: null;
|
||||
|
||||
// Find the latest message for the selected agent
|
||||
const selectedMessage = selectedAgentId
|
||||
? messages
|
||||
.filter((m) => m.senderId === selectedAgentId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)[0] ?? null
|
||||
: null;
|
||||
|
||||
const pendingQuestions = questionsQuery.data ?? null;
|
||||
|
||||
// Handlers
|
||||
function handleRefresh() {
|
||||
void utils.listWaitingAgents.invalidate();
|
||||
void utils.listMessages.invalidate();
|
||||
}
|
||||
|
||||
function handleSubmitAnswers(answers: Record<string, string>) {
|
||||
if (!selectedAgentId) return;
|
||||
resumeAgentMutation.mutate({ id: selectedAgentId, answers });
|
||||
}
|
||||
|
||||
function handleDismissQuestions() {
|
||||
if (!selectedAgentId) return;
|
||||
dismissQuestionsMutation.mutate({ id: selectedAgentId });
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
if (!selectedMessage) return;
|
||||
respondToMessageMutation.mutate({
|
||||
id: selectedMessage.id,
|
||||
response: "Acknowledged",
|
||||
});
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (agentsQuery.isLoading && messagesQuery.isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Skeleton header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-6 w-28" />
|
||||
<Skeleton className="h-5 w-8 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
{/* Skeleton message rows */}
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Card key={i} className="p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-3 w-3 rounded-full" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
<Skeleton className="mt-2 ml-5 h-3 w-full" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (agentsQuery.isError || messagesQuery.isError) {
|
||||
const errorMessage =
|
||||
agentsQuery.error?.message ?? messagesQuery.error?.message ?? "Unknown error";
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-12">
|
||||
<AlertCircle className="h-8 w-8 text-destructive" />
|
||||
<p className="text-sm text-destructive">
|
||||
Failed to load inbox: {errorMessage}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Serialize agents for InboxList (convert Date to string for wire format)
|
||||
const serializedAgents = agents.map((a) => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
status: a.status,
|
||||
taskId: a.taskId,
|
||||
updatedAt: String(a.updatedAt),
|
||||
}));
|
||||
|
||||
// Serialize messages for InboxList
|
||||
const serializedMessages = messages.map((m) => ({
|
||||
id: m.id,
|
||||
senderId: m.senderId,
|
||||
content: m.content,
|
||||
requiresResponse: m.requiresResponse,
|
||||
status: m.status,
|
||||
createdAt: String(m.createdAt),
|
||||
}));
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0, 0, 0.2, 1] }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1fr_400px]">
|
||||
{/* Left: Inbox List -- hidden on mobile when agent selected */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.05, ease: [0, 0, 0.2, 1] }}
|
||||
className={selectedAgent ? "hidden lg:block" : undefined}
|
||||
>
|
||||
<InboxList
|
||||
agents={serializedAgents}
|
||||
messages={serializedMessages}
|
||||
selectedAgentId={selectedAgentId}
|
||||
onSelectAgent={setSelectedAgentId}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Right: Detail Panel */}
|
||||
{selectedAgent && (
|
||||
<InboxDetailPanel
|
||||
agent={{
|
||||
id: selectedAgent.id,
|
||||
name: selectedAgent.name,
|
||||
status: selectedAgent.status,
|
||||
taskId: selectedAgent.taskId,
|
||||
updatedAt: String(selectedAgent.updatedAt),
|
||||
}}
|
||||
message={
|
||||
selectedMessage
|
||||
? {
|
||||
id: selectedMessage.id,
|
||||
content: selectedMessage.content,
|
||||
requiresResponse: selectedMessage.requiresResponse,
|
||||
}
|
||||
: null
|
||||
}
|
||||
questions={
|
||||
pendingQuestions
|
||||
? pendingQuestions.questions.map((q) => ({
|
||||
id: q.id,
|
||||
question: q.question,
|
||||
options: q.options,
|
||||
multiSelect: q.multiSelect,
|
||||
}))
|
||||
: null
|
||||
}
|
||||
isLoadingQuestions={questionsQuery.isLoading}
|
||||
questionsError={
|
||||
questionsQuery.isError ? questionsQuery.error.message : null
|
||||
}
|
||||
onBack={() => setSelectedAgentId(null)}
|
||||
onSubmitAnswers={handleSubmitAnswers}
|
||||
onDismissQuestions={handleDismissQuestions}
|
||||
onDismissMessage={handleDismiss}
|
||||
isSubmitting={resumeAgentMutation.isPending}
|
||||
isDismissingQuestions={dismissQuestionsMutation.isPending}
|
||||
isDismissingMessage={respondToMessageMutation.isPending}
|
||||
submitError={
|
||||
resumeAgentMutation.isError
|
||||
? resumeAgentMutation.error.message
|
||||
: null
|
||||
}
|
||||
dismissMessageError={
|
||||
respondToMessageMutation.isError
|
||||
? respondToMessageMutation.error.message
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Empty detail panel placeholder */}
|
||||
{!selectedAgent && (
|
||||
<div className="hidden flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border p-8 lg:flex">
|
||||
<MessageSquare className="h-10 w-10 text-muted-foreground/30" />
|
||||
<div className="space-y-1 text-center">
|
||||
<p className="text-sm font-medium text-muted-foreground">No message selected</p>
|
||||
<p className="text-xs text-muted-foreground/70">Select an agent from the inbox to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user