Files
adventure/apps/web/src/hooks/useMentorChat.ts
Zaine acea7dcf12
Some checks failed
CI / test (push) Has been cancelled
fix the memory adding
2026-06-26 16:32:34 +01:00

177 lines
5.3 KiB
TypeScript

"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import {
buildDisplayMessages,
createPendingStateOnError,
createPendingStateOnSend,
type MentorChatMessage,
type MentorPendingState,
shouldBlockSend,
} from "@/lib/mentor-chat-utils";
const DELETE_CONFIRM_MESSAGE =
"Delete this chat? Your saved AI memories will not be affected.";
type ChatSessionData = {
session: { id: string; title: string };
messages: MentorChatMessage[];
};
type SendChatResponse = {
memorySuggestions?: unknown[];
memoryAction?: {
type: string;
intent?: string;
status?: string;
error?: string;
};
};
export function useMentorChat(sessionId: string | null) {
const queryClient = useQueryClient();
const bottomRef = useRef<HTMLDivElement>(null);
const [input, setInput] = useState("");
const [pendingState, setPendingState] = useState<MentorPendingState | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const { data: chatData } = useQuery<ChatSessionData>({
queryKey: ["chat-session", sessionId],
queryFn: async () => {
const res = await fetch(`/api/ai/chat/sessions/${sessionId}`);
if (!res.ok) throw new Error("Failed to load chat");
return res.json();
},
enabled: !!sessionId,
});
const serverMessages = chatData?.messages ?? [];
const displayMessages = buildDisplayMessages(serverMessages, pendingState);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [displayMessages.length, pendingState]);
const send = useMutation({
mutationFn: async (content: string) => {
const res = await fetch(`/api/ai/chat/sessions/${sessionId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? "Send failed");
}
return res.json() as Promise<SendChatResponse>;
},
onMutate: (content) => {
setInput("");
setPendingState(createPendingStateOnSend(content));
},
onSuccess: (data) => {
setPendingState(null);
queryClient.invalidateQueries({ queryKey: ["chat-session", sessionId] });
queryClient.invalidateQueries({ queryKey: ["chat-sessions"] });
queryClient.invalidateQueries({ queryKey: ["ai-memory-suggestions"] });
queryClient.invalidateQueries({ queryKey: ["ai-knows"] });
if (
data.memoryAction?.type === "memory_created" ||
data.memoryAction?.type === "memory_suggestion_accepted"
) {
queryClient.invalidateQueries({ queryKey: ["ai-memory"] });
}
},
onError: (_err, content) => {
setPendingState(createPendingStateOnError(content));
},
});
const archive = useMutation({
mutationFn: async () => {
const res = await fetch(`/api/ai/chat/sessions/${sessionId}`, { method: "DELETE" });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? "Delete failed");
}
return res.json();
},
onMutate: () => {
setDeleteError(null);
},
onSuccess: () => {
setPendingState(null);
setInput("");
queryClient.invalidateQueries({ queryKey: ["chat-sessions"] });
},
onError: (err: Error) => {
setDeleteError(err.message || "Could not delete chat. Please try again.");
},
});
const sendMessage = useCallback(
(content: string) => {
if (!sessionId || shouldBlockSend(send.isPending, content)) return;
send.mutate(content.trim());
},
[sessionId, send]
);
const retry = useCallback(() => {
const content = pendingState?.lastFailedContent;
if (!content || send.isPending) return;
setPendingState(null);
send.mutate(content);
}, [pendingState?.lastFailedContent, send]);
const deleteChat = useCallback(() => {
if (!sessionId || archive.isPending) return;
if (!confirm(DELETE_CONFIRM_MESSAGE)) return;
archive.mutate();
}, [sessionId, archive]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && input.trim()) {
e.preventDefault();
sendMessage(input);
}
},
[input, sendMessage]
);
return {
input,
setInput,
displayMessages,
sendMessage,
retry,
deleteChat,
handleKeyDown,
bottomRef,
isSendPending: send.isPending,
isDeletePending: archive.isPending,
canRetry: !!pendingState?.lastFailedContent,
deleteError,
archiveSuccess: archive.isSuccess,
resetArchiveSuccess: archive.reset,
};
}
export async function createChatSession(title?: string): Promise<{ id: string }> {
const res = await fetch("/api/ai/chat/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(title ? { title } : {}),
});
if (!res.ok) throw new Error("Failed to create session");
return res.json();
}
export async function fetchChatSessions(): Promise<{ sessions: { id: string; title: string }[] }> {
const res = await fetch("/api/ai/chat/sessions");
if (!res.ok) throw new Error("Failed to load sessions");
return res.json();
}