fix(frontend): more copilot refinements (#11858)

## Changes 🏗️

On the **Copilot** page:

- prevent unnecessary sidebar repaints 
- show a disclaimer when switching chats on the sidebar to terminate a
current stream
- handle loading better
- save streams better when disconnecting


### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally and test the above
This commit is contained in:
Ubbe
2026-01-28 00:49:28 +07:00
committed by GitHub
parent 2134d777be
commit 071b3bb5cd
10 changed files with 376 additions and 140 deletions

View File

@@ -3,7 +3,7 @@
import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { useCallback, useEffect } from "react";
import { useCopilotStore } from "../../copilot-page-store";
import { DesktopSidebar } from "./components/DesktopSidebar/DesktopSidebar";
import { LoadingState } from "./components/LoadingState/LoadingState";
@@ -25,10 +25,12 @@ export function CopilotShell({ children }: Props) {
sessions,
currentSessionId,
handleSelectSession,
performSelectSession,
handleOpenDrawer,
handleCloseDrawer,
handleDrawerOpenChange,
handleNewChat,
performNewChat,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
@@ -36,22 +38,71 @@ export function CopilotShell({ children }: Props) {
} = useCopilotShell();
const setNewChatHandler = useCopilotStore((s) => s.setNewChatHandler);
const setNewChatWithInterruptHandler = useCopilotStore(
(s) => s.setNewChatWithInterruptHandler,
);
const setSelectSessionHandler = useCopilotStore(
(s) => s.setSelectSessionHandler,
);
const setSelectSessionWithInterruptHandler = useCopilotStore(
(s) => s.setSelectSessionWithInterruptHandler,
);
const requestNewChat = useCopilotStore((s) => s.requestNewChat);
const requestSelectSession = useCopilotStore((s) => s.requestSelectSession);
const stableHandleNewChat = useCallback(handleNewChat, [handleNewChat]);
const stablePerformNewChat = useCallback(performNewChat, [performNewChat]);
useEffect(
function registerNewChatHandler() {
setNewChatHandler(handleNewChat);
function registerNewChatHandlers() {
setNewChatHandler(stableHandleNewChat);
setNewChatWithInterruptHandler(stablePerformNewChat);
return function cleanup() {
setNewChatHandler(null);
setNewChatWithInterruptHandler(null);
};
},
[handleNewChat],
[
stableHandleNewChat,
stablePerformNewChat,
setNewChatHandler,
setNewChatWithInterruptHandler,
],
);
const stableHandleSelectSession = useCallback(handleSelectSession, [
handleSelectSession,
]);
const stablePerformSelectSession = useCallback(performSelectSession, [
performSelectSession,
]);
useEffect(
function registerSelectSessionHandlers() {
setSelectSessionHandler(stableHandleSelectSession);
setSelectSessionWithInterruptHandler(stablePerformSelectSession);
return function cleanup() {
setSelectSessionHandler(null);
setSelectSessionWithInterruptHandler(null);
};
},
[
stableHandleSelectSession,
stablePerformSelectSession,
setSelectSessionHandler,
setSelectSessionWithInterruptHandler,
],
);
function handleNewChatClick() {
requestNewChat();
}
function handleSessionClick(sessionId: string) {
requestSelectSession(sessionId);
}
if (!isLoggedIn) {
return (
<div className="flex h-full items-center justify-center">
@@ -72,7 +123,7 @@ export function CopilotShell({ children }: Props) {
isLoading={isLoading}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onSelectSession={handleSelectSession}
onSelectSession={handleSessionClick}
onFetchNextPage={fetchNextPage}
onNewChat={handleNewChatClick}
hasActiveSession={Boolean(hasActiveSession)}
@@ -94,7 +145,7 @@ export function CopilotShell({ children }: Props) {
isLoading={isLoading}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onSelectSession={handleSelectSession}
onSelectSession={handleSessionClick}
onFetchNextPage={fetchNextPage}
onNewChat={handleNewChatClick}
onClose={handleCloseDrawer}

View File

@@ -1,17 +1,20 @@
"use client";
import {
getGetV2GetSessionQueryKey,
getGetV2ListSessionsQueryKey,
useGetV2GetSession,
} from "@/app/api/__generated__/endpoints/chat/chat";
import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse";
import { okData } from "@/app/api/helpers";
import { useChatStore } from "@/components/contextual/Chat/chat-store";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useQueryClient } from "@tanstack/react-query";
import { parseAsString, useQueryState } from "nuqs";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useCopilotStore } from "../../copilot-page-store";
import { useMobileDrawer } from "./components/MobileDrawer/useMobileDrawer";
import { useSessionsPagination } from "./components/SessionsList/useSessionsPagination";
import {
@@ -73,6 +76,19 @@ export function useCopilotShell() {
Map<string, SessionSummaryResponse>
>(new Map());
const [optimisticSessionId, setOptimisticSessionId] = useState<string | null>(
null,
);
useEffect(
function clearOptimisticWhenUrlMatches() {
if (optimisticSessionId && currentSessionId === optimisticSessionId) {
setOptimisticSessionId(null);
}
},
[currentSessionId, optimisticSessionId],
);
// Mark as auto-selected when sessionId is in URL
useEffect(() => {
if (paramSessionId && !hasAutoSelectedRef.current) {
@@ -142,7 +158,9 @@ export function useCopilotShell() {
const visibleSessions = filterVisibleSessions(sessions);
const sidebarSelectedSessionId =
isOnHomepage && !paramSessionId ? null : currentSessionId;
isOnHomepage && !paramSessionId && !optimisticSessionId
? null
: optimisticSessionId || currentSessionId;
const isReadyToShowContent = isOnHomepage
? true
@@ -155,8 +173,89 @@ export function useCopilotShell() {
hasAutoSelectedSession,
);
function handleSelectSession(sessionId: string) {
const stopStream = useChatStore((s) => s.stopStream);
const onStreamComplete = useChatStore((s) => s.onStreamComplete);
const setIsSwitchingSession = useCopilotStore((s) => s.setIsSwitchingSession);
async function performSelectSession(sessionId: string) {
if (sessionId === currentSessionId) return;
const sourceSessionId = currentSessionId;
if (sourceSessionId) {
setIsSwitchingSession(true);
await new Promise<void>(function waitForStreamComplete(resolve) {
const unsubscribe = onStreamComplete(
function handleComplete(completedId) {
if (completedId === sourceSessionId) {
clearTimeout(timeout);
unsubscribe();
resolve();
}
},
);
const timeout = setTimeout(function handleTimeout() {
unsubscribe();
resolve();
}, 3000);
stopStream(sourceSessionId);
});
queryClient.invalidateQueries({
queryKey: getGetV2GetSessionQueryKey(sourceSessionId),
});
}
setOptimisticSessionId(sessionId);
setUrlSessionId(sessionId, { shallow: false });
setIsSwitchingSession(false);
if (isMobile) handleCloseDrawer();
}
function handleSelectSession(sessionId: string) {
if (sessionId === currentSessionId) return;
setOptimisticSessionId(sessionId);
setUrlSessionId(sessionId, { shallow: false });
if (isMobile) handleCloseDrawer();
}
async function performNewChat() {
const sourceSessionId = currentSessionId;
if (sourceSessionId) {
setIsSwitchingSession(true);
await new Promise<void>(function waitForStreamComplete(resolve) {
const unsubscribe = onStreamComplete(
function handleComplete(completedId) {
if (completedId === sourceSessionId) {
clearTimeout(timeout);
unsubscribe();
resolve();
}
},
);
const timeout = setTimeout(function handleTimeout() {
unsubscribe();
resolve();
}, 3000);
stopStream(sourceSessionId);
});
queryClient.invalidateQueries({
queryKey: getGetV2GetSessionQueryKey(sourceSessionId),
});
setIsSwitchingSession(false);
}
resetAutoSelect();
resetPagination();
queryClient.invalidateQueries({
queryKey: getGetV2ListSessionsQueryKey(),
});
setUrlSessionId(null, { shallow: false });
setOptimisticSessionId(null);
if (isMobile) handleCloseDrawer();
}
@@ -167,6 +266,7 @@ export function useCopilotShell() {
queryKey: getGetV2ListSessionsQueryKey(),
});
setUrlSessionId(null, { shallow: false });
setOptimisticSessionId(null);
if (isMobile) handleCloseDrawer();
}
@@ -187,10 +287,12 @@ export function useCopilotShell() {
sessions: visibleSessions,
currentSessionId: sidebarSelectedSessionId,
handleSelectSession,
performSelectSession,
handleOpenDrawer,
handleCloseDrawer,
handleDrawerOpenChange,
handleNewChat,
performNewChat,
hasNextPage,
isFetchingNextPage: isSessionsFetching,
fetchNextPage,

View File

@@ -4,51 +4,106 @@ import { create } from "zustand";
interface CopilotStoreState {
isStreaming: boolean;
isNewChatModalOpen: boolean;
isSwitchingSession: boolean;
isInterruptModalOpen: boolean;
pendingAction: (() => void) | null;
newChatHandler: (() => void) | null;
newChatWithInterruptHandler: (() => void) | null;
selectSessionHandler: ((sessionId: string) => void) | null;
selectSessionWithInterruptHandler: ((sessionId: string) => void) | null;
}
interface CopilotStoreActions {
setIsStreaming: (isStreaming: boolean) => void;
setIsSwitchingSession: (isSwitchingSession: boolean) => void;
setNewChatHandler: (handler: (() => void) | null) => void;
setNewChatWithInterruptHandler: (handler: (() => void) | null) => void;
setSelectSessionHandler: (
handler: ((sessionId: string) => void) | null,
) => void;
setSelectSessionWithInterruptHandler: (
handler: ((sessionId: string) => void) | null,
) => void;
requestNewChat: () => void;
confirmNewChat: () => void;
cancelNewChat: () => void;
requestSelectSession: (sessionId: string) => void;
confirmInterrupt: () => void;
cancelInterrupt: () => void;
}
type CopilotStore = CopilotStoreState & CopilotStoreActions;
export const useCopilotStore = create<CopilotStore>((set, get) => ({
isStreaming: false,
isNewChatModalOpen: false,
isSwitchingSession: false,
isInterruptModalOpen: false,
pendingAction: null,
newChatHandler: null,
newChatWithInterruptHandler: null,
selectSessionHandler: null,
selectSessionWithInterruptHandler: null,
setIsStreaming(isStreaming) {
set({ isStreaming });
},
setIsSwitchingSession(isSwitchingSession) {
set({ isSwitchingSession });
},
setNewChatHandler(handler) {
set({ newChatHandler: handler });
},
setNewChatWithInterruptHandler(handler) {
set({ newChatWithInterruptHandler: handler });
},
setSelectSessionHandler(handler) {
set({ selectSessionHandler: handler });
},
setSelectSessionWithInterruptHandler(handler) {
set({ selectSessionWithInterruptHandler: handler });
},
requestNewChat() {
const { isStreaming, newChatHandler } = get();
const { isStreaming, newChatHandler, newChatWithInterruptHandler } = get();
if (isStreaming) {
set({ isNewChatModalOpen: true });
if (!newChatWithInterruptHandler) return;
set({
isInterruptModalOpen: true,
pendingAction: newChatWithInterruptHandler,
});
} else if (newChatHandler) {
newChatHandler();
}
},
confirmNewChat() {
const { newChatHandler } = get();
set({ isNewChatModalOpen: false });
if (newChatHandler) {
newChatHandler();
requestSelectSession(sessionId) {
const {
isStreaming,
selectSessionHandler,
selectSessionWithInterruptHandler,
} = get();
if (isStreaming) {
if (!selectSessionWithInterruptHandler) return;
set({
isInterruptModalOpen: true,
pendingAction: () => selectSessionWithInterruptHandler(sessionId),
});
} else {
if (!selectSessionHandler) return;
selectSessionHandler(sessionId);
}
},
cancelNewChat() {
set({ isNewChatModalOpen: false });
confirmInterrupt() {
const { pendingAction } = get();
set({ isInterruptModalOpen: false, pendingAction: null });
if (pendingAction) pendingAction();
},
cancelInterrupt() {
set({ isInterruptModalOpen: false, pendingAction: null });
},
}));

View File

@@ -13,22 +13,15 @@ import { useCopilotPage } from "./useCopilotPage";
export default function CopilotPage() {
const { state, handlers } = useCopilotPage();
const confirmNewChat = useCopilotStore((s) => s.confirmNewChat);
const {
greetingName,
quickActions,
isLoading,
pageState,
isNewChatModalOpen,
isReady,
} = state;
const isInterruptModalOpen = useCopilotStore((s) => s.isInterruptModalOpen);
const confirmInterrupt = useCopilotStore((s) => s.confirmInterrupt);
const cancelInterrupt = useCopilotStore((s) => s.cancelInterrupt);
const { greetingName, quickActions, isLoading, pageState, isReady } = state;
const {
handleQuickAction,
startChatWithPrompt,
handleSessionNotFound,
handleStreamingChange,
handleCancelNewChat,
handleNewChatModalOpen,
} = handlers;
if (!isReady) return null;
@@ -48,31 +41,33 @@ export default function CopilotPage() {
title="Interrupt current chat?"
styling={{ maxWidth: 300, width: "100%" }}
controlled={{
isOpen: isNewChatModalOpen,
set: handleNewChatModalOpen,
isOpen: isInterruptModalOpen,
set: (open) => {
if (!open) cancelInterrupt();
},
}}
onClose={handleCancelNewChat}
onClose={cancelInterrupt}
>
<Dialog.Content>
<div className="flex flex-col gap-4">
<Text variant="body">
The current chat response will be interrupted. Are you sure you
want to start a new chat?
want to continue?
</Text>
<Dialog.Footer>
<Button
type="button"
variant="outline"
onClick={handleCancelNewChat}
onClick={cancelInterrupt}
>
Cancel
</Button>
<Button
type="button"
variant="primary"
onClick={confirmNewChat}
onClick={confirmInterrupt}
>
Start new chat
Continue
</Button>
</Dialog.Footer>
</div>

View File

@@ -75,9 +75,7 @@ export function useCopilotPage() {
const { user, isLoggedIn, isUserLoading } = useSupabase();
const { toast } = useToast();
const isNewChatModalOpen = useCopilotStore((s) => s.isNewChatModalOpen);
const setIsStreaming = useCopilotStore((s) => s.setIsStreaming);
const cancelNewChat = useCopilotStore((s) => s.cancelNewChat);
const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
@@ -201,21 +199,12 @@ export function useCopilotPage() {
setIsStreaming(isStreamingValue);
}
function handleCancelNewChat() {
cancelNewChat();
}
function handleNewChatModalOpen(isOpen: boolean) {
if (!isOpen) cancelNewChat();
}
return {
state: {
greetingName,
quickActions,
isLoading: isUserLoading,
pageState: state.pageState,
isNewChatModalOpen,
isReady: isFlagReady && isChatEnabled !== false && isLoggedIn,
},
handlers: {
@@ -223,8 +212,6 @@ export function useCopilotPage() {
startChatWithPrompt,
handleSessionNotFound,
handleStreamingChange,
handleCancelNewChat,
handleNewChatModalOpen,
},
};
}

View File

@@ -1,11 +1,12 @@
"use client";
import { useCopilotStore } from "@/app/(platform)/copilot/copilot-page-store";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { useEffect, useRef } from "react";
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
import { ChatLoader } from "./components/ChatLoader/ChatLoader";
import { useChat } from "./useChat";
export interface ChatProps {
@@ -24,6 +25,7 @@ export function Chat({
onStreamingChange,
}: ChatProps) {
const hasHandledNotFoundRef = useRef(false);
const isSwitchingSession = useCopilotStore((s) => s.isSwitchingSession);
const {
messages,
isLoading,
@@ -47,29 +49,34 @@ export function Chat({
[onSessionNotFound, urlSessionId, isSessionNotFound, isLoading, isCreating],
);
const shouldShowLoader =
(showLoader && (isLoading || isCreating)) || isSwitchingSession;
return (
<div className={cn("flex h-full flex-col", className)}>
{/* Main Content */}
<main className="flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-[#f8f8f9]">
{/* Loading State */}
{showLoader && (isLoading || isCreating) && (
{shouldShowLoader && (
<div className="flex flex-1 items-center justify-center">
<div className="flex flex-col items-center gap-4">
<ChatLoader />
<div className="flex flex-col items-center gap-3">
<LoadingSpinner size="large" className="text-neutral-400" />
<Text variant="body" className="text-zinc-500">
Loading your chats...
{isSwitchingSession
? "Switching chat..."
: "Loading your chat..."}
</Text>
</div>
</div>
)}
{/* Error State */}
{error && !isLoading && (
{error && !isLoading && !isSwitchingSession && (
<ChatErrorState error={error} onRetry={createSession} />
)}
{/* Session Content */}
{sessionId && !isLoading && !error && (
{sessionId && !isLoading && !error && !isSwitchingSession && (
<ChatContainer
sessionId={sessionId}
initialMessages={messages}

View File

@@ -58,39 +58,17 @@ function notifyStreamComplete(
}
}
function cleanupCompletedStreams(completedStreams: Map<string, StreamResult>) {
function cleanupExpiredStreams(
completedStreams: Map<string, StreamResult>,
): Map<string, StreamResult> {
const now = Date.now();
for (const [sessionId, result] of completedStreams) {
const cleaned = new Map(completedStreams);
for (const [sessionId, result] of cleaned) {
if (now - result.completedAt > COMPLETED_STREAM_TTL) {
completedStreams.delete(sessionId);
cleaned.delete(sessionId);
}
}
}
function moveToCompleted(
activeStreams: Map<string, ActiveStream>,
completedStreams: Map<string, StreamResult>,
streamCompleteCallbacks: Set<StreamCompleteCallback>,
sessionId: string,
) {
const stream = activeStreams.get(sessionId);
if (!stream) return;
const result: StreamResult = {
sessionId,
status: stream.status,
chunks: stream.chunks,
completedAt: Date.now(),
error: stream.error,
};
completedStreams.set(sessionId, result);
activeStreams.delete(sessionId);
cleanupCompletedStreams(completedStreams);
if (stream.status === "completed" || stream.status === "error") {
notifyStreamComplete(streamCompleteCallbacks, sessionId);
}
return cleaned;
}
export const useChatStore = create<ChatStore>((set, get) => ({
@@ -106,17 +84,31 @@ export const useChatStore = create<ChatStore>((set, get) => ({
context,
onChunk,
) {
const { activeStreams, completedStreams, streamCompleteCallbacks } = get();
const state = get();
const newActiveStreams = new Map(state.activeStreams);
let newCompletedStreams = new Map(state.completedStreams);
const callbacks = state.streamCompleteCallbacks;
const existingStream = activeStreams.get(sessionId);
const existingStream = newActiveStreams.get(sessionId);
if (existingStream) {
existingStream.abortController.abort();
moveToCompleted(
activeStreams,
completedStreams,
streamCompleteCallbacks,
const normalizedStatus =
existingStream.status === "streaming"
? "completed"
: existingStream.status;
const result: StreamResult = {
sessionId,
);
status: normalizedStatus,
chunks: existingStream.chunks,
completedAt: Date.now(),
error: existingStream.error,
};
newCompletedStreams.set(sessionId, result);
newActiveStreams.delete(sessionId);
newCompletedStreams = cleanupExpiredStreams(newCompletedStreams);
if (normalizedStatus === "completed" || normalizedStatus === "error") {
notifyStreamComplete(callbacks, sessionId);
}
}
const abortController = new AbortController();
@@ -132,36 +124,76 @@ export const useChatStore = create<ChatStore>((set, get) => ({
onChunkCallbacks: initialCallbacks,
};
activeStreams.set(sessionId, stream);
newActiveStreams.set(sessionId, stream);
set({
activeStreams: newActiveStreams,
completedStreams: newCompletedStreams,
});
try {
await executeStream(stream, message, isUserMessage, context);
} finally {
if (onChunk) stream.onChunkCallbacks.delete(onChunk);
if (stream.status !== "streaming") {
moveToCompleted(
activeStreams,
completedStreams,
streamCompleteCallbacks,
sessionId,
);
const currentState = get();
const finalActiveStreams = new Map(currentState.activeStreams);
let finalCompletedStreams = new Map(currentState.completedStreams);
const storedStream = finalActiveStreams.get(sessionId);
if (storedStream === stream) {
const result: StreamResult = {
sessionId,
status: stream.status,
chunks: stream.chunks,
completedAt: Date.now(),
error: stream.error,
};
finalCompletedStreams.set(sessionId, result);
finalActiveStreams.delete(sessionId);
finalCompletedStreams = cleanupExpiredStreams(finalCompletedStreams);
set({
activeStreams: finalActiveStreams,
completedStreams: finalCompletedStreams,
});
if (stream.status === "completed" || stream.status === "error") {
notifyStreamComplete(
currentState.streamCompleteCallbacks,
sessionId,
);
}
}
}
}
},
stopStream: function stopStream(sessionId) {
const { activeStreams, completedStreams, streamCompleteCallbacks } = get();
const stream = activeStreams.get(sessionId);
if (stream) {
stream.abortController.abort();
stream.status = "completed";
moveToCompleted(
activeStreams,
completedStreams,
streamCompleteCallbacks,
sessionId,
);
}
const state = get();
const stream = state.activeStreams.get(sessionId);
if (!stream) return;
stream.abortController.abort();
stream.status = "completed";
const newActiveStreams = new Map(state.activeStreams);
let newCompletedStreams = new Map(state.completedStreams);
const result: StreamResult = {
sessionId,
status: stream.status,
chunks: stream.chunks,
completedAt: Date.now(),
error: stream.error,
};
newCompletedStreams.set(sessionId, result);
newActiveStreams.delete(sessionId);
newCompletedStreams = cleanupExpiredStreams(newCompletedStreams);
set({
activeStreams: newActiveStreams,
completedStreams: newCompletedStreams,
});
notifyStreamComplete(state.streamCompleteCallbacks, sessionId);
},
subscribeToStream: function subscribeToStream(
@@ -169,16 +201,18 @@ export const useChatStore = create<ChatStore>((set, get) => ({
onChunk,
skipReplay = false,
) {
const { activeStreams } = get();
const state = get();
const stream = state.activeStreams.get(sessionId);
const stream = activeStreams.get(sessionId);
if (stream) {
if (!skipReplay) {
for (const chunk of stream.chunks) {
onChunk(chunk);
}
}
stream.onChunkCallbacks.add(onChunk);
return function unsubscribe() {
stream.onChunkCallbacks.delete(onChunk);
};
@@ -204,7 +238,12 @@ export const useChatStore = create<ChatStore>((set, get) => ({
},
clearCompletedStream: function clearCompletedStream(sessionId) {
get().completedStreams.delete(sessionId);
const state = get();
if (!state.completedStreams.has(sessionId)) return;
const newCompletedStreams = new Map(state.completedStreams);
newCompletedStreams.delete(sessionId);
set({ completedStreams: newCompletedStreams });
},
isStreaming: function isStreaming(sessionId) {
@@ -213,11 +252,21 @@ export const useChatStore = create<ChatStore>((set, get) => ({
},
registerActiveSession: function registerActiveSession(sessionId) {
get().activeSessions.add(sessionId);
const state = get();
if (state.activeSessions.has(sessionId)) return;
const newActiveSessions = new Set(state.activeSessions);
newActiveSessions.add(sessionId);
set({ activeSessions: newActiveSessions });
},
unregisterActiveSession: function unregisterActiveSession(sessionId) {
get().activeSessions.delete(sessionId);
const state = get();
if (!state.activeSessions.has(sessionId)) return;
const newActiveSessions = new Set(state.activeSessions);
newActiveSessions.delete(sessionId);
set({ activeSessions: newActiveSessions });
},
isSessionActive: function isSessionActive(sessionId) {
@@ -225,10 +274,16 @@ export const useChatStore = create<ChatStore>((set, get) => ({
},
onStreamComplete: function onStreamComplete(callback) {
const { streamCompleteCallbacks } = get();
streamCompleteCallbacks.add(callback);
const state = get();
const newCallbacks = new Set(state.streamCompleteCallbacks);
newCallbacks.add(callback);
set({ streamCompleteCallbacks: newCallbacks });
return function unsubscribe() {
streamCompleteCallbacks.delete(callback);
const currentState = get();
const cleanedCallbacks = new Set(currentState.streamCompleteCallbacks);
cleanedCallbacks.delete(callback);
set({ streamCompleteCallbacks: cleanedCallbacks });
};
},
}));

View File

@@ -126,10 +126,6 @@ export function ChatMessage({
[displayContent, message],
);
function isLongResponse(content: string): boolean {
return content.split("\n").length > 5;
}
const handleTryAgain = useCallback(() => {
if (message.type !== "message" || !onSendMessage) return;
onSendMessage(message.content, message.role === "user");
@@ -358,7 +354,7 @@ export function ChatMessage({
<ArrowsClockwiseIcon className="size-4 text-zinc-600" />
</Button>
)}
{!isUser && isFinalMessage && isLongResponse(displayContent) && (
{!isUser && isFinalMessage && !isStreaming && (
<Button
variant="ghost"
size="icon"

View File

@@ -10,7 +10,7 @@ export function UserChatBubble({ children, className }: UserChatBubbleProps) {
return (
<div
className={cn(
"group relative min-w-20 overflow-hidden rounded-xl bg-purple-100 px-3 text-right text-[1rem] leading-relaxed transition-all duration-500 ease-in-out",
"group relative min-w-20 overflow-hidden rounded-xl bg-purple-100 px-3 text-left text-[1rem] leading-relaxed transition-all duration-500 ease-in-out",
className,
)}
style={{

View File

@@ -1,7 +1,6 @@
import {
getGetV2GetSessionQueryKey,
getGetV2GetSessionQueryOptions,
getGetV2ListSessionsQueryKey,
postV2CreateSession,
useGetV2GetSession,
usePatchV2SessionAssignUser,
@@ -102,17 +101,6 @@ export function useChatSession({
}
}, [createError, loadError]);
useEffect(
function refreshSessionsListOnLoad() {
if (sessionId && sessionData && !isLoadingSession) {
queryClient.invalidateQueries({
queryKey: getGetV2ListSessionsQueryKey(),
});
}
},
[sessionId, sessionData, isLoadingSession, queryClient],
);
async function createSession() {
try {
setError(null);