rowboat/apps/rowboat/app/projects/[projectId]/conversations/components/conversation-view.tsx
2025-08-10 06:21:37 +05:30

184 lines
9.6 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState } from "react";
import { Spinner } from "@heroui/react";
import { Panel } from "@/components/common/panel-common";
import { fetchConversation } from "@/app/actions/conversation_actions";
import { Conversation } from "@/src/entities/models/conversation";
import { Turn } from "@/src/entities/models/turn";
import { z } from "zod";
import Link from "next/link";
import { MessageDisplay } from "../../../../lib/components/message-display";
import { ReasonBadge } from "../../../../lib/components/reason-badge";
function TurnContainer({ turn, index, projectId }: { turn: z.infer<typeof Turn>; index: number; projectId: string }) {
return (
<div id={`turn-${turn.id}`} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
{/* Turn Header */}
<div className="bg-gray-100 dark:bg-gray-800 px-4 py-2 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-mono font-semibold text-gray-700 dark:text-gray-300">
TURN #{index + 1}
</span>
<ReasonBadge reason={turn.reason} projectId={projectId} />
</div>
<div className="text-xs text-gray-500 dark:text-gray-500">
{new Date(turn.createdAt).toLocaleTimeString()}
</div>
</div>
</div>
{/* Turn Content */}
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{/* Input Messages */}
{turn.input.messages && turn.input.messages.length > 0 && (
<div className="p-4 bg-gray-50 dark:bg-gray-900/50">
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-3 uppercase tracking-wide">
Input Messages ({turn.input.messages.length})
</div>
<div className="space-y-1">
{turn.input.messages.map((message, msgIndex) => (
<MessageDisplay key={`input-${msgIndex}`} message={message} index={msgIndex} />
))}
</div>
</div>
)}
{/* Output Messages */}
{turn.output && turn.output.length > 0 && (
<div className="p-4">
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-3 uppercase tracking-wide">
Output Messages ({turn.output.length})
</div>
<div className="space-y-1">
{turn.output.map((message, msgIndex) => (
<MessageDisplay key={`output-${msgIndex}`} message={message} index={msgIndex} />
))}
</div>
</div>
)}
{/* Error Display */}
{turn.error && (
<div className="p-4 bg-red-50 dark:bg-red-900/10 border-l-4 border-red-500">
<div className="text-xs font-semibold text-red-600 dark:text-red-400 mb-1 uppercase tracking-wide">
Error
</div>
<div className="text-sm text-red-700 dark:text-red-300 font-mono">
{turn.error}
</div>
</div>
)}
</div>
</div>
);
}
export function ConversationView({ projectId, conversationId }: { projectId: string; conversationId: string; }) {
const [conversation, setConversation] = useState<z.infer<typeof Conversation> | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
let ignore = false;
(async () => {
setLoading(true);
const res = await fetchConversation({ conversationId });
if (ignore) return;
setConversation(res);
setLoading(false);
})();
return () => { ignore = true; };
}, [conversationId]);
const title = useMemo(() => {
if (!conversation) return 'Conversation';
return `Conversation ${conversation.id}`;
}, [conversation]);
return (
<Panel
title={<div className="flex items-center gap-3"><div className="text-sm font-medium text-gray-900 dark:text-gray-100">{title}</div></div>}
rightActions={<div className="flex items-center gap-3"></div>}
>
<div className="h-full overflow-auto px-4 py-4">
<div className="max-w-[1024px] mx-auto">
{loading && (
<div className="flex items-center gap-2">
<Spinner size="sm" />
<div>Loading...</div>
</div>
)}
{!loading && conversation && (
<div className="flex flex-col gap-6">
{/* Conversation Metadata */}
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Conversation ID:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{conversation.id}</span>
</div>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Created:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{new Date(conversation.createdAt).toLocaleString()}
</span>
</div>
{conversation.updatedAt && (
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Updated:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{new Date(conversation.updatedAt).toLocaleString()}
</span>
</div>
)}
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Live Workflow:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{conversation.isLiveWorkflow ? 'Yes' : 'No'}
</span>
</div>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Reason:</span>
<span className="ml-2">
<ReasonBadge reason={conversation.reason} projectId={projectId} />
</span>
</div>
</div>
</div>
{/* Workflow */}
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 uppercase tracking-wide">
Workflow
</div>
<pre className="bg-gray-100 dark:bg-gray-900 p-3 rounded text-xs overflow-x-auto border border-gray-200 dark:border-gray-700 font-mono max-h-[400px]">
{JSON.stringify(conversation.workflow, null, 2)}
</pre>
</div>
{/* Turns */}
{conversation.turns && conversation.turns.length > 0 ? (
<div className="space-y-4">
<div className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
Turns ({conversation.turns.length})
</div>
{conversation.turns.map((turn, index) => (
<TurnContainer key={turn.id} turn={turn} index={index} projectId={projectId} />
))}
</div>
) : (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<div className="text-sm font-mono">No turns in this conversation.</div>
</div>
)}
</div>
)}
</div>
</div>
</Panel>
);
}