import { ActionIcon, Box, Group, HoverCard, JsonInput, List, Stack, Tabs, Textarea, useMantineTheme, } from "@mantine/core"; import { useEffect, useState } from "react"; import { defaultParameters, defaultSystemPrompt, useStore, } from "../../../state"; import { usePageContext } from "vike-react/usePageContext"; import { useData } from "vike-react/useData"; import type { Data } from "./+data"; import type { CommittedMessage, DraftMessage } from "../../../types"; import Markdown from "react-markdown"; import { IconTrash, IconEdit, IconCheck, IconX } from "@tabler/icons-react"; import { useTRPC } from "../../../trpc/client"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { nanoid } from "nanoid"; import type { Conversation } from "../../../database/common"; export default function ChatPage() { const pageContext = usePageContext(); const conversationId = pageContext.routeParams.id; const { conversation, // messages: initialMessages, // facts: initialFacts, // factTriggers: initialFactTriggers, } = useData(); const conversationTitle = conversation?.title; const message = useStore((state) => state.message); const systemPrompt = useStore((state) => state.systemPrompt); const parameters = useStore((state) => state.parameters); const loading = useStore((state) => state.loading); const setMessage = useStore((state) => state.setMessage); const setSystemPrompt = useStore((state) => state.setSystemPrompt); const setParameters = useStore((state) => state.setParameters); const setLoading = useStore((state) => state.setLoading); const trpc = useTRPC(); const queryClient = useQueryClient(); const messagesResult = useQuery( trpc.chat.messages.fetchByConversationId.queryOptions({ conversationId, }) ); const messages: Array | undefined = messagesResult.data?.map((m) => ({ ...m, parts: m.parts.filter((p) => p.type === "text"), })) || []; const facts = useQuery( trpc.chat.facts.fetchByConversationId.queryOptions({ conversationId, }) ); const factTriggers = useQuery( trpc.chat.factTriggers.fetchByConversationId.queryOptions({ conversationId, }) ); const deleteFact = useMutation( trpc.chat.facts.deleteOne.mutationOptions({ onMutate: async ({ factId: factIdToDelete }) => { /** Cancel affected queries that may be in-flight: */ await queryClient.cancelQueries({ queryKey: trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), }); /** Optimistically update the affected queries in react-query's cache: */ const previousFacts = await queryClient.getQueryData( trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }) ); if (!previousFacts) { return { previousFacts: [], newFacts: [], }; } const newFacts = previousFacts.filter((f) => f.id !== factIdToDelete); queryClient.setQueryData( trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), newFacts ); return { previousFacts, newFacts }; }, onSettled: async (data, variables, context) => { await queryClient.invalidateQueries({ queryKey: trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), }); }, onError: async (error, variables, context) => { console.error(error); if (!context) return; queryClient.setQueryData( trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), context.previousFacts ); }, }) ); const updateFact = useMutation( trpc.chat.facts.update.mutationOptions({ onMutate: async ({ factId, content }) => { /** Cancel affected queries that may be in-flight: */ await queryClient.cancelQueries({ queryKey: trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), }); /** Optimistically update the affected queries in react-query's cache: */ const previousFacts = await queryClient.getQueryData( trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }) ); if (!previousFacts) { return { previousFacts: [], newFacts: [], }; } const newFacts = previousFacts.map((f) => f.id === factId ? { ...f, content } : f ); queryClient.setQueryData( trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), newFacts ); return { previousFacts, newFacts }; }, onSettled: async (data, variables, context) => { await queryClient.invalidateQueries({ queryKey: trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), }); }, onError: async (error, variables, context) => { console.error(error); if (!context) return; queryClient.setQueryData( trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), context.previousFacts ); }, }) ); const deleteFactTrigger = useMutation( trpc.chat.factTriggers.deleteOne.mutationOptions({ onMutate: async ({ factTriggerId: factTriggerIdToDelete }) => { /** Cancel affected queries that may be in-flight: */ await queryClient.cancelQueries({ queryKey: trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), }); /** Optimistically update the affected queries in react-query's cache: */ const previousFactTriggers = await queryClient.getQueryData( trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }) ); if (!previousFactTriggers) { return { previousFactTriggers: [], newFactTriggers: [], }; } const newFactTriggers = previousFactTriggers.filter( (ft) => ft.id !== factTriggerIdToDelete ); queryClient.setQueryData( trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), newFactTriggers ); return { previousFactTriggers, newFactTriggers }; }, onSettled: async (data, variables, context) => { await queryClient.invalidateQueries({ queryKey: trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), }); }, onError: async (error, variables, context) => { console.error(error); if (!context) return; queryClient.setQueryData( trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), context.previousFactTriggers ); }, }) ); const updateFactTrigger = useMutation( trpc.chat.factTriggers.update.mutationOptions({ onMutate: async ({ factTriggerId, content }) => { /** Cancel affected queries that may be in-flight: */ await queryClient.cancelQueries({ queryKey: trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), }); /** Optimistically update the affected queries in react-query's cache: */ const previousFactTriggers = await queryClient.getQueryData( trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }) ); if (!previousFactTriggers) { return { previousFactTriggers: [], newFactTriggers: [], }; } const newFactTriggers = previousFactTriggers.map((ft) => ft.id === factTriggerId ? { ...ft, content } : ft ); queryClient.setQueryData( trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), newFactTriggers ); return { previousFactTriggers, newFactTriggers }; }, onSettled: async (data, variables, context) => { await queryClient.invalidateQueries({ queryKey: trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), }); }, onError: async (error, variables, context) => { console.error(error); if (!context) return; queryClient.setQueryData( trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), context.previousFactTriggers ); }, }) ); const updateConversationTitle = useMutation( trpc.chat.conversations.updateTitle.mutationOptions({ onMutate: async ({ id, title }) => { /** Cancel affected queries that may be in-flight: */ await queryClient.cancelQueries({ queryKey: trpc.chat.conversations.fetchAll.queryKey(), }); /** Optimistically update the affected queries in react-query's cache: */ const previousConversations = await queryClient.getQueryData( trpc.chat.conversations.fetchAll.queryKey() ); if (!previousConversations) { return { previousConversations: [], newConversations: null, }; } const newConversations: Array = [ ...previousConversations, { ...conversation, title, } as Conversation, ]; queryClient.setQueryData( trpc.chat.conversations.fetchAll.queryKey(), newConversations ); return { previousConversations, newConversations }; }, onSettled: async (data, variables, context) => { await queryClient.invalidateQueries({ queryKey: trpc.chat.conversations.fetchOne.queryKey({ id: conversationId, }), }); await queryClient.invalidateQueries({ queryKey: trpc.chat.conversations.fetchAll.queryKey(), }); }, onError: async (error, variables, context) => { console.error(error); if (!context) return; queryClient.setQueryData( trpc.chat.conversations.fetchAll.queryKey(), context.previousConversations ); }, }) ); const sendMessage = useMutation( trpc.chat.sendMessage.mutationOptions({ onMutate: async ({ conversationId, messages, systemPrompt, parameters, }) => { /** Cancel affected queries that may be in-flight: */ await queryClient.cancelQueries({ queryKey: trpc.chat.messages.fetchByConversationId.queryKey({ conversationId, }), }); /** Optimistically update the affected queries in react-query's cache: */ const previousMessages: Array | undefined = await queryClient.getQueryData( trpc.chat.messages.fetchByConversationId.queryKey({ conversationId, }) ); if (!previousMessages) { return { previousMessages: [], newMessages: [], }; } const newMessages: Array = [ ...previousMessages, { /** placeholder id; will be overwritten when we get the true id from the backend */ id: nanoid(), conversationId, // content: messages[messages.length - 1].content, // role: "user" as const, ...messages[messages.length - 1], index: previousMessages.length, createdAt: new Date().toISOString(), } as CommittedMessage, ]; queryClient.setQueryData( trpc.chat.messages.fetchByConversationId.queryKey({ conversationId, }), newMessages ); return { previousMessages, newMessages }; }, onSettled: async (data, variables, context) => { await queryClient.invalidateQueries({ queryKey: trpc.chat.messages.fetchByConversationId.queryKey({ conversationId, }), }); await queryClient.invalidateQueries({ queryKey: trpc.chat.facts.fetchByConversationId.queryKey({ conversationId, }), }); await queryClient.invalidateQueries({ queryKey: trpc.chat.factTriggers.fetchByConversationId.queryKey({ conversationId, }), }); }, onError: async (error, variables, context) => { console.error(error); if (!context) return; queryClient.setQueryData( trpc.chat.messages.fetchByConversationId.queryKey({ conversationId, }), context.previousMessages ); }, }) ); // State for editing facts const [editingFactId, setEditingFactId] = useState(null); const [editingFactContent, setEditingFactContent] = useState(""); // State for editing fact triggers const [editingFactTriggerId, setEditingFactTriggerId] = useState< string | null >(null); const [editingFactTriggerContent, setEditingFactTriggerContent] = useState(""); // Handle clicking outside to cancel editing useEffect(() => { function handleClickOutside(event: MouseEvent) { if (editingFactId && event.target instanceof Element) { const editingElement = event.target.closest(".editing-fact"); if (!editingElement) { setEditingFactId(null); } } if (editingFactTriggerId && event.target instanceof Element) { const editingElement = event.target.closest(".editing-fact-trigger"); if (!editingElement) { setEditingFactTriggerId(null); } } } document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [editingFactId, editingFactTriggerId]); async function handleDeleteFact(factId: string) { await deleteFact.mutateAsync({ factId }); } async function handleUpdateFact(factId: string, content: string) { await updateFact.mutateAsync({ factId, content }); } async function handleDeleteFactTrigger(factTriggerId: string) { await deleteFactTrigger.mutateAsync({ factTriggerId }); } async function handleUpdateFactTrigger( factTriggerId: string, content: string ) { await updateFactTrigger.mutateAsync({ factTriggerId, content }); } return ( <>
Conversation #{conversationId} - { // setConversationTitle(e.target.value); // }} onBlur={(e) => { updateConversationTitle.mutateAsync({ id: conversationId, title: e.target.value, }); }} />
Message System Prompt Parameters Facts Fact Triggers