core/apps/webapp/app/routes/home.conversation.$conversationId.tsx
Harshith Mullapudi 00f983079f 1. Added onboarding
2. fix: remove custom tool calls in chat and link directly to mcp
2025-10-21 11:32:36 +05:30

209 lines
6.4 KiB
TypeScript

import {
type LoaderFunctionArgs,
type ActionFunctionArgs,
} from "@remix-run/server-runtime";
import { sort } from "fast-sort";
import { useParams, useRevalidator, useNavigate } from "@remix-run/react";
import { parse } from "@conform-to/zod";
import {
requireUserId,
requireUser,
requireWorkpace,
} from "~/services/session.server";
import {
getConversationAndHistory,
getCurrentConversationRun,
stopConversation,
createConversation,
CreateConversationSchema,
} from "~/services/conversation.server";
import { type ConversationHistory } from "@core/database";
import {
ConversationItem,
ConversationTextarea,
StreamingConversation,
} from "~/components/conversation";
import { useTypedLoaderData } from "remix-typedjson";
import React from "react";
import { ScrollAreaWithAutoScroll } from "~/components/use-auto-scroll";
import { PageHeader } from "~/components/common/page-header";
import { Plus } from "lucide-react";
import { json } from "@remix-run/node";
import { env } from "~/env.server";
// Example loader accessing params
export async function loader({ params, request }: LoaderFunctionArgs) {
const user = await requireUser(request);
const workspace = await requireWorkpace(request);
const conversation = await getConversationAndHistory(
params.conversationId as string,
user.id,
);
if (!conversation) {
throw new Error("No conversation found");
}
const run = await getCurrentConversationRun(conversation.id, workspace.id);
return { conversation, run, apiURL: env.TRIGGER_API_URL };
}
// Example action accessing params
export async function action({ params, request }: ActionFunctionArgs) {
if (request.method.toUpperCase() !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const userId = await requireUserId(request);
const workspace = await requireWorkpace(request);
const formData = await request.formData();
const { conversationId } = params;
if (!conversationId) {
throw new Error("No conversation");
}
// Check if this is a stop request (isLoading = true means stop button was clicked)
const message = formData.get("message");
// If no message, it's a stop request
if (!message) {
const result = await stopConversation(conversationId, workspace.id);
return json(result);
}
// Otherwise, create a new conversation message
const submission = parse(formData, { schema: CreateConversationSchema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
const conversation = await createConversation(workspace?.id, userId, {
message: submission.value.message,
title: submission.value.title,
conversationId: submission.value.conversationId,
});
return json({ conversation });
}
// Accessing params in the component
export default function SingleConversation() {
const { conversation, run, apiURL } = useTypedLoaderData<typeof loader>();
const conversationHistory = conversation.ConversationHistory;
const [conversationResponse, setConversationResponse] = React.useState<
{ conversationHistoryId: string; id: string; token: string } | undefined
>(run);
const { conversationId } = useParams();
const revalidator = useRevalidator();
const navigate = useNavigate();
React.useEffect(() => {
if (run) {
setConversationResponse(run);
}
}, [run]);
const getConversations = () => {
const lastConversationHistoryId =
conversationResponse?.conversationHistoryId;
// First sort the conversation history by creation time
const sortedConversationHistory = sort(conversationHistory).asc(
(ch) => ch.createdAt,
);
const lastIndex = sortedConversationHistory.findIndex(
(item) => item.id === lastConversationHistoryId,
);
// Filter out any conversation history items that come after the lastConversationHistoryId
const filteredConversationHistory = lastConversationHistoryId
? sortedConversationHistory.filter((_ch, currentIndex: number) => {
return currentIndex <= lastIndex;
})
: sortedConversationHistory;
return (
<>
{filteredConversationHistory.map((ch: ConversationHistory) => {
return <ConversationItem key={ch.id} conversationHistory={ch} />;
})}
</>
);
};
if (typeof window === "undefined") {
return null;
}
return (
<>
<PageHeader
title="Conversation"
breadcrumbs={[
{ label: "Conversations", href: "/home/conversation" },
{ label: conversation.title || "Untitled" },
]}
actions={[
{
label: "New conversation",
icon: <Plus size={14} />,
onClick: () => navigate("/home/conversation"),
variant: "secondary",
},
]}
/>
<div className="relative flex h-[calc(100vh_-_56px)] w-full flex-col items-center justify-center overflow-auto">
<div className="flex h-[calc(100vh_-_80px)] w-full flex-col justify-end overflow-hidden">
<ScrollAreaWithAutoScroll>
{getConversations()}
{conversationResponse && (
<StreamingConversation
runId={conversationResponse.id}
token={conversationResponse.token}
afterStreaming={() => {
setConversationResponse(undefined);
revalidator.revalidate();
}}
apiURL={apiURL}
/>
)}
</ScrollAreaWithAutoScroll>
<div className="flex w-full flex-col items-center">
<div className="w-full max-w-[80ch] px-1 pr-2">
{conversation?.status !== "need_approval" && (
<ConversationTextarea
conversationId={conversationId as string}
className="bg-background-3 w-full border-1 border-gray-300"
isLoading={
!!conversationResponse || conversation?.status === "running"
}
onConversationCreated={(conversation) => {
if (conversation) {
setConversationResponse({
conversationHistoryId:
conversation.conversationHistoryId,
id: conversation.id,
token: conversation.token,
});
}
}}
/>
)}
</div>
</div>
</div>
</div>
</>
);
}