|
|
|
@ -19,10 +19,20 @@ import {
|
|
|
|
|
import { usePageContext } from "vike-react/usePageContext";
|
|
|
|
|
import { useData } from "vike-react/useData";
|
|
|
|
|
import type { Data } from "./+data";
|
|
|
|
|
import type { CommittedMessage, DraftMessage } from "../../../types";
|
|
|
|
|
import type {
|
|
|
|
|
CommittedMessage,
|
|
|
|
|
DraftMessage,
|
|
|
|
|
OtherParameters,
|
|
|
|
|
} from "../../../types";
|
|
|
|
|
import Markdown from "react-markdown";
|
|
|
|
|
import { IconTrash, IconEdit, IconCheck, IconX } from "@tabler/icons-react";
|
|
|
|
|
import { useTRPC } from "../../../trpc/client";
|
|
|
|
|
import {
|
|
|
|
|
IconTrash,
|
|
|
|
|
IconEdit,
|
|
|
|
|
IconCheck,
|
|
|
|
|
IconX,
|
|
|
|
|
IconLoaderQuarter,
|
|
|
|
|
} from "@tabler/icons-react";
|
|
|
|
|
import { useTRPC, useTRPCClient } from "../../../trpc/client";
|
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import { nanoid } from "nanoid";
|
|
|
|
|
import type { Conversation } from "../../../database/common";
|
|
|
|
@ -49,6 +59,7 @@ export default function ChatPage() {
|
|
|
|
|
const setParameters = useStore((state) => state.setParameters);
|
|
|
|
|
const setLoading = useStore((state) => state.setLoading);
|
|
|
|
|
const trpc = useTRPC();
|
|
|
|
|
const trpcClient = useTRPCClient();
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
const messagesResult = useQuery(
|
|
|
|
@ -334,84 +345,95 @@ export default function ChatPage() {
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const sendMessage = useMutation(
|
|
|
|
|
trpc.chat.sendMessage.mutationOptions({
|
|
|
|
|
onMutate: async ({
|
|
|
|
|
// Get state from Zustand store
|
|
|
|
|
const sendMessageStatus = useStore((state) => state.sendMessageStatus);
|
|
|
|
|
const isSendingMessage = useStore((state) => state.isSendingMessage);
|
|
|
|
|
const setSendMessageStatus = useStore((state) => state.setSendMessageStatus);
|
|
|
|
|
const setIsSendingMessage = useStore((state) => state.setIsSendingMessage);
|
|
|
|
|
|
|
|
|
|
// Function to send message using subscription
|
|
|
|
|
const sendSubscriptionMessage = async ({
|
|
|
|
|
conversationId,
|
|
|
|
|
messages,
|
|
|
|
|
systemPrompt,
|
|
|
|
|
parameters,
|
|
|
|
|
}: {
|
|
|
|
|
conversationId: string;
|
|
|
|
|
messages: Array<DraftMessage | CommittedMessage>;
|
|
|
|
|
systemPrompt: string;
|
|
|
|
|
parameters: OtherParameters;
|
|
|
|
|
}) => {
|
|
|
|
|
/** 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<CommittedMessage> | undefined =
|
|
|
|
|
await queryClient.getQueryData(
|
|
|
|
|
trpc.chat.messages.fetchByConversationId.queryKey({
|
|
|
|
|
conversationId,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
if (!previousMessages) {
|
|
|
|
|
return {
|
|
|
|
|
previousMessages: [],
|
|
|
|
|
newMessages: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const newMessages: Array<CommittedMessage> = [
|
|
|
|
|
...previousMessages,
|
|
|
|
|
setIsSendingMessage(true);
|
|
|
|
|
setSendMessageStatus(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Create an abort controller for the subscription
|
|
|
|
|
const abortController = new AbortController();
|
|
|
|
|
|
|
|
|
|
// Start the subscription
|
|
|
|
|
const subscription = trpcClient.chat.sendMessage.subscribe(
|
|
|
|
|
{
|
|
|
|
|
/** 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 };
|
|
|
|
|
messages,
|
|
|
|
|
systemPrompt,
|
|
|
|
|
parameters,
|
|
|
|
|
},
|
|
|
|
|
onSettled: async (data, variables, context) => {
|
|
|
|
|
await queryClient.invalidateQueries({
|
|
|
|
|
{
|
|
|
|
|
signal: abortController.signal,
|
|
|
|
|
onData: (data) => {
|
|
|
|
|
setSendMessageStatus(data);
|
|
|
|
|
|
|
|
|
|
// If we've completed, update the UI and invalidate queries
|
|
|
|
|
if (data.status === "completed") {
|
|
|
|
|
setIsSendingMessage(false);
|
|
|
|
|
// Invalidate queries to refresh the data
|
|
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: trpc.chat.messages.fetchByConversationId.queryKey({
|
|
|
|
|
conversationId,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
await queryClient.invalidateQueries({
|
|
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: trpc.chat.facts.fetchByConversationId.queryKey({
|
|
|
|
|
conversationId,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
await queryClient.invalidateQueries({
|
|
|
|
|
queryKey: trpc.chat.factTriggers.fetchByConversationId.queryKey({
|
|
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: trpc.chat.factTriggers.fetchByConversationId.queryKey(
|
|
|
|
|
{
|
|
|
|
|
conversationId,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setSendMessageStatus(data);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError: async (error, variables, context) => {
|
|
|
|
|
console.error(error);
|
|
|
|
|
if (!context) return;
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
trpc.chat.messages.fetchByConversationId.queryKey({
|
|
|
|
|
conversationId,
|
|
|
|
|
}),
|
|
|
|
|
context.previousMessages
|
|
|
|
|
);
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
console.error("Subscription error:", error);
|
|
|
|
|
setIsSendingMessage(false);
|
|
|
|
|
setSendMessageStatus({
|
|
|
|
|
status: "error",
|
|
|
|
|
message: "An error occurred while sending the message",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Return a function to unsubscribe if needed
|
|
|
|
|
return () => {
|
|
|
|
|
abortController.abort();
|
|
|
|
|
subscription.unsubscribe();
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to start subscription:", error);
|
|
|
|
|
setIsSendingMessage(false);
|
|
|
|
|
setSendMessageStatus({
|
|
|
|
|
status: "error",
|
|
|
|
|
message: "Failed to start message sending process",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// State for editing facts
|
|
|
|
|
const [editingFactId, setEditingFactId] = useState<string | null>(null);
|
|
|
|
|
const [editingFactContent, setEditingFactContent] = useState("");
|
|
|
|
@ -483,6 +505,8 @@ export default function ChatPage() {
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{isSendingMessage && <IconLoaderQuarter size={16} stroke={1.5} />}
|
|
|
|
|
{sendMessageStatus && <span>{sendMessageStatus.message}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<Tabs defaultValue="message">
|
|
|
|
|
<Tabs.List>
|
|
|
|
@ -504,7 +528,7 @@ export default function ChatPage() {
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setLoading(true);
|
|
|
|
|
await sendMessage.mutateAsync({
|
|
|
|
|
await sendSubscriptionMessage({
|
|
|
|
|
conversationId,
|
|
|
|
|
messages: [
|
|
|
|
|
...(messages || []),
|
|
|
|
|