From fc9cb91ad50a4d8c932aeee6c601fa7d7174ac18 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Fri, 31 Oct 2025 17:36:03 +0530 Subject: [PATCH 01/24] Add chat history to Assistant Ai --- .../ai/assistant/assistant-ui-chat.tsx | 91 +++++++++-- .../ai/lib/ai-assistant-chat-history-api.ts | 24 +++ .../features/ai/lib/assistant-ui-chat-hook.ts | 26 +++- .../ai/lib/use-ai-assistant-chat-history.ts | 39 +++++ .../api/src/app/ai/chat/ai-chat.service.ts | 15 ++ .../src/app/ai/chat/ai-mcp-chat.controller.ts | 42 ++++- .../unit/ai/ai-mcp-chat.controller.test.ts | 144 ++++++++++++++++++ packages/shared/src/lib/ai/chat/index.ts | 5 + .../assistant-ui/assistant-top-bar.tsx | 25 ++- .../assistant-ui-chat-container.tsx | 52 ++++--- .../history/assistant-ui-history-item.tsx | 2 +- .../history/assistant-ui-history.tsx | 15 -- .../ui-components/src/components/index.ts | 2 + 13 files changed, 422 insertions(+), 60 deletions(-) create mode 100644 packages/react-ui/src/app/features/ai/lib/ai-assistant-chat-history-api.ts create mode 100644 packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts diff --git a/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx b/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx index f2fef90f01..acf059c998 100644 --- a/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx +++ b/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx @@ -3,12 +3,16 @@ import { AI_ASSISTANT_SS_KEY } from '@/app/constants/ai'; import { useAiModelSelector } from '@/app/features/ai/lib/ai-model-selector-hook'; import { useAssistantChat } from '@/app/features/ai/lib/assistant-ui-chat-hook'; import { useBuilderStoreOutsideProviderWithSubscription } from '@/app/features/builder/builder-state-provider'; -import { AssistantUiChatContainer } from '@openops/components/ui'; +import { + AssistantUiChatContainer, + AssistantUiHistory, +} from '@openops/components/ui'; import { SourceCode } from '@openops/shared'; import { createFrontendTools } from '@openops/ui-kit'; import { t } from 'i18next'; import { ReactNode, useCallback, useMemo, useState } from 'react'; import { ChatMode } from '../lib/types'; +import { useAssistantChatHistory } from '../lib/use-ai-assistant-chat-history'; type AssistantUiChatProps = { onClose: () => void; @@ -26,6 +30,7 @@ const AssistantUiChat = ({ const toolComponents = useMemo(() => { return createFrontendTools(); }, []); + const [showHistory, setShowHistory] = useState(false); const [chatId, setChatId] = useState( sessionStorage.getItem(AI_ASSISTANT_SS_KEY), @@ -66,6 +71,45 @@ const AssistantUiChat = ({ isLoading: isModelSelectorLoading, } = useAiModelSelector({ chatId, provider, model }); + const { + chats, + isLoading: isHistoryLoading, + deleteChat, + renameChat, + refetch, + } = useAssistantChatHistory(); + + const onChatSelected = useCallback( + (id: string) => { + onChatIdChange(id); + setShowHistory(false); + }, + [onChatIdChange], + ); + + const onChatDeleted = useCallback( + async (id: string) => { + await deleteChat(id); + if (chatId === id) { + onChatIdChange(null); + } + }, + [chatId, deleteChat, onChatIdChange], + ); + + const onChatRenamed = useCallback( + async (id: string, newName: string) => { + await renameChat({ chatId: id, chatName: newName }); + }, + [renameChat], + ); + + const onNewChatClick = useCallback(async () => { + await createNewChat(); + await refetch(); + setShowHistory(false); + }, [createNewChat, refetch]); + if (isLoading) { return (
@@ -77,21 +121,36 @@ const AssistantUiChat = ({ } return ( - - {children} - +
+ setShowHistory(!showHistory)} + isHistoryOpen={showHistory} + > + {showHistory && ( + + )} + {children} + +
); }; diff --git a/packages/react-ui/src/app/features/ai/lib/ai-assistant-chat-history-api.ts b/packages/react-ui/src/app/features/ai/lib/ai-assistant-chat-history-api.ts new file mode 100644 index 0000000000..aef754abaa --- /dev/null +++ b/packages/react-ui/src/app/features/ai/lib/ai-assistant-chat-history-api.ts @@ -0,0 +1,24 @@ +import { api } from '@/app/lib/api'; +import { ListChatsResponse } from '@openops/shared'; + +export const aiAssistantChatHistoryApi = { + list() { + return api.get('/v1/ai/conversation/all-chats'); + }, + delete(chatId: string) { + return api.delete(`/v1/ai/conversation/${chatId}`); + }, + generateName(chatId: string) { + return api.post<{ chatName: string }>('/v1/ai/conversation/chat-name', { + chatId, + }); + }, + rename(chatId: string, chatName: string) { + return api.patch<{ chatName: string }>( + `/v1/ai/conversation/${chatId}/name`, + { + chatName, + }, + ); + }, +}; diff --git a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts index 5d9ab3b742..9f40d8f276 100644 --- a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts +++ b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts @@ -7,17 +7,20 @@ import { useAISDKRuntime } from '@assistant-ui/react-ai-sdk'; import { toast } from '@openops/components/ui'; import { flowHelper } from '@openops/shared'; import { getFrontendToolDefinitions } from '@openops/ui-kit'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { DefaultChatTransport, ToolSet, UIMessage } from 'ai'; import { t } from 'i18next'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { aiChatApi } from '../../builder/ai-chat/lib/chat-api'; import { getBuilderStore } from '../../builder/builder-state-provider'; +import { aiAssistantChatHistoryApi } from './ai-assistant-chat-history-api'; import { aiSettingsHooks } from './ai-settings-hooks'; import { buildQueryKey } from './chat-utils'; import { createAdditionalContext } from './enrich-context'; import { ChatMode, UseAssistantChatProps } from './types'; +export const MAX_MESSAGES_BEFORE_NAME_GENERATION = 3; + export const useAssistantChat = ({ chatId, onChatIdChange, @@ -29,6 +32,8 @@ export const useAssistantChat = ({ () => getFrontendToolDefinitions() as ToolSet, [], ); + const qc = useQueryClient(); + const hasAttemptedNameGenerationRef = useRef>({}); const [provider, setProvider] = useState(); const [model, setModel] = useState(); @@ -213,6 +218,25 @@ export const useAssistantChat = ({ }; toast(errorToast); }, + onFinish: async () => { + if (!chatId || hasAttemptedNameGenerationRef.current[chatId]) { + return; + } + + if (messagesRef.current.length >= MAX_MESSAGES_BEFORE_NAME_GENERATION) { + setTimeout(async () => { + try { + hasAttemptedNameGenerationRef.current[chatId] = true; + await aiAssistantChatHistoryApi.generateName(chatId); + qc.invalidateQueries({ queryKey: ['assistant-history'] }); + } catch (error) { + console.error('Failed to generate chat name', error); + hasAttemptedNameGenerationRef.current[chatId] = false; + qc.invalidateQueries({ queryKey: ['assistant-history'] }); + } + }, 500); + } + }, // https://github.com/assistant-ui/assistant-ui/issues/2327 // handle frontend tool calls manually until this is fixed onToolCall: async ({ toolCall }: { toolCall: any }) => { diff --git a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts new file mode 100644 index 0000000000..3512d3448b --- /dev/null +++ b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts @@ -0,0 +1,39 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { aiAssistantChatHistoryApi } from './ai-assistant-chat-history-api'; + +export function useAssistantChatHistory() { + const qc = useQueryClient(); + + const { data, isLoading } = useQuery({ + queryKey: ['assistant-history'], + queryFn: async () => { + const res = await aiAssistantChatHistoryApi.list(); + return res.chats ?? []; + }, + select: (chats) => + chats.map((c) => ({ + id: c.chatId, + displayName: c.chatName || 'New chat', + })), + refetchOnWindowFocus: false, + }); + + const deleteMutation = useMutation({ + mutationFn: (chatId: string) => aiAssistantChatHistoryApi.delete(chatId), + onSuccess: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + }); + + const renameMutation = useMutation({ + mutationFn: ({ chatId, chatName }: { chatId: string; chatName: string }) => + aiAssistantChatHistoryApi.rename(chatId, chatName), + onSuccess: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + }); + + return { + chats: data ?? [], + isLoading, + deleteChat: deleteMutation.mutateAsync, + renameChat: renameMutation.mutateAsync, + refetch: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + }; +} diff --git a/packages/server/api/src/app/ai/chat/ai-chat.service.ts b/packages/server/api/src/app/ai/chat/ai-chat.service.ts index f802b4d2d4..bc693c97fc 100644 --- a/packages/server/api/src/app/ai/chat/ai-chat.service.ts +++ b/packages/server/api/src/app/ai/chat/ai-chat.service.ts @@ -216,6 +216,21 @@ export const deleteChatHistory = async ( await cacheWrapper.deleteKey(chatHistoryKey(chatId, userId, projectId)); }; +const chatContextKeyFor = ( + chatId: string, + userId: string, + projectId: string, +): string => `${projectId}:${userId}:${chatId}:context`; + +export const deleteChat = async ( + chatId: string, + userId: string, + projectId: string, +): Promise => { + await cacheWrapper.deleteKey(chatHistoryKey(chatId, userId, projectId)); + await cacheWrapper.deleteKey(chatContextKeyFor(chatId, userId, projectId)); +}; + export async function getLLMConfig( projectId: string, contextModel?: string, diff --git a/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts b/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts index db29aacd0a..67b7d0d132 100644 --- a/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts +++ b/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts @@ -1,4 +1,7 @@ -import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'; +import { + FastifyPluginAsyncTypebox, + Type, +} from '@fastify/type-provider-typebox'; import { observe, updateActiveObservation } from '@langfuse/tracing'; import { getLangfuseSpanProcessor, @@ -17,6 +20,7 @@ import { OpenChatResponse, openOpsId, PrincipalType, + RenameChatRequest, UpdateChatModelRequest, UpdateChatModelResponse, } from '@openops/shared'; @@ -25,7 +29,7 @@ import { FastifyReply } from 'fastify'; import { StatusCodes } from 'http-status-codes'; import { createChatContext, - deleteChatHistory, + deleteChat, generateChatId, generateChatIdForMCP, generateChatName, @@ -45,7 +49,7 @@ import { parseUserMessage } from './message-parser'; import { createUserMessage } from './model-message-factory'; import { getBlockSystemPrompt } from './prompts.service'; -const DEFAULT_CHAT_NAME = 'New Chat'; +export const DEFAULT_CHAT_NAME = 'New Chat'; export const aiMCPChatController: FastifyPluginAsyncTypebox = async (app) => { app.post( @@ -254,6 +258,20 @@ export const aiMCPChatController: FastifyPluginAsyncTypebox = async (app) => { } }); + app.patch('/:chatId/name', RenameChatOptions, async (request, reply) => { + const { chatId } = request.params; + const { chatName } = request.body; + const userId = request.principal.id; + const projectId = request.principal.projectId; + + try { + await updateChatName(chatId, userId, projectId, chatName); + return await reply.code(200).send({ chatName }); + } catch (error) { + return handleError(error, reply, 'rename chat'); + } + }); + app.post('/code', CodeGenerationOptions, async (request, reply) => { const chatId = request.body.chatId; const projectId = request.principal.projectId; @@ -396,10 +414,10 @@ export const aiMCPChatController: FastifyPluginAsyncTypebox = async (app) => { const projectId = request.principal.projectId; try { - await deleteChatHistory(chatId, userId, projectId); + await deleteChat(chatId, userId, projectId); return await reply.code(StatusCodes.OK).send(); } catch (error) { - return handleError(error, reply, 'delete chat history'); + return handleError(error, reply, 'delete chat'); } }); }; @@ -439,6 +457,20 @@ const ChatNameOptions = { }, }; +const RenameChatOptions = { + config: { + allowedPrincipals: [PrincipalType.USER], + }, + schema: { + tags: ['ai', 'ai-chat-mcp'], + description: 'Rename a chat session with a user provided name.', + params: RenameChatRequest, + body: Type.Object({ + chatName: Type.String(), + }), + }, +}; + const CodeGenerationOptions = { config: { allowedPrincipals: [PrincipalType.USER], diff --git a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts index 73ac8e7554..c300857f02 100644 --- a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts +++ b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts @@ -121,6 +121,10 @@ describe('AI MCP Chat Controller - Tool Service Interactions', () => { handlers[path] = handler; return mockApp; }), + patch: jest.fn((path: string, _: unknown, handler: RouteHandler) => { + handlers[path] = handler; + return mockApp; + }), delete: jest.fn((path: string, _: unknown, handler: RouteHandler) => { handlers[path] = handler; return mockApp; @@ -846,4 +850,144 @@ describe('AI MCP Chat Controller - Tool Service Interactions', () => { ); }); }); + + describe('PATCH /:chatId/name (rename chat)', () => { + let patchHandler: RouteHandler; + + const mockChatContext = { + chatId: 'test-chat-id', + chatName: 'Old Chat Name', + provider: AiProviderEnum.ANTHROPIC, + model: 'claude-3-sonnet', + }; + + beforeEach(async () => { + jest.clearAllMocks(); + handlers = {}; + await aiMCPChatController(mockApp, {} as FastifyPluginOptions); + patchHandler = handlers['/:chatId/name']; + }); + + it('should successfully rename a chat', async () => { + (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); + (updateChatName as jest.Mock).mockResolvedValue(undefined); + + const request = { + ...mockRequest, + params: { chatId: 'test-chat-id' }, + body: { chatName: 'New Chat Name' }, + } as FastifyRequest; + + await patchHandler(request, mockReply as unknown as FastifyReply); + + expect(updateChatName).toHaveBeenCalledWith( + 'test-chat-id', + 'test-user-id', + 'test-project-id', + 'New Chat Name', + ); + expect(mockReply.code).toHaveBeenCalledWith(200); + expect(mockReply.send).toHaveBeenCalledWith({ + chatName: 'New Chat Name', + }); + }); + + it('should return error when chat context not found', async () => { + (updateChatName as jest.Mock).mockRejectedValue( + new Error('Chat context not found'), + ); + + const request = { + ...mockRequest, + params: { chatId: 'non-existent-chat-id' }, + body: { chatName: 'New Name' }, + } as FastifyRequest; + + await patchHandler(request, mockReply as unknown as FastifyReply); + + expect(updateChatName).toHaveBeenCalledWith( + 'non-existent-chat-id', + 'test-user-id', + 'test-project-id', + 'New Name', + ); + expect(mockReply.code).toHaveBeenCalledWith(500); + expect(mockReply.send).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Internal server error' }), + ); + }); + + it('should handle service errors gracefully', async () => { + (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); + (updateChatName as jest.Mock).mockRejectedValue( + new Error('Database error'), + ); + + const request = { + ...mockRequest, + params: { chatId: 'test-chat-id' }, + body: { chatName: 'New Name' }, + } as FastifyRequest; + + await patchHandler(request, mockReply as unknown as FastifyReply); + + expect(updateChatName).toHaveBeenCalledWith( + 'test-chat-id', + 'test-user-id', + 'test-project-id', + 'New Name', + ); + expect(mockReply.code).toHaveBeenCalledWith(500); + expect(mockReply.send).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Internal server error' }), + ); + }); + + it('should handle empty chat name', async () => { + (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); + (updateChatName as jest.Mock).mockResolvedValue(undefined); + + const request = { + ...mockRequest, + params: { chatId: 'test-chat-id' }, + body: { chatName: '' }, + } as FastifyRequest; + + await patchHandler(request, mockReply as unknown as FastifyReply); + + expect(updateChatName).toHaveBeenCalledWith( + 'test-chat-id', + 'test-user-id', + 'test-project-id', + '', + ); + expect(mockReply.code).toHaveBeenCalledWith(200); + expect(mockReply.send).toHaveBeenCalledWith({ chatName: '' }); + }); + + it('should handle chat names with special characters', async () => { + (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); + (updateChatName as jest.Mock).mockResolvedValue(undefined); + + const specialName = 'Chat with & special chars: #1 @test'; + const request = { + ...mockRequest, + params: { chatId: 'test-chat-id' }, + body: { chatName: specialName }, + } as FastifyRequest; + + await patchHandler(request, mockReply as unknown as FastifyReply); + + expect(updateChatName).toHaveBeenCalledWith( + 'test-chat-id', + 'test-user-id', + 'test-project-id', + specialName, + ); + expect(mockReply.code).toHaveBeenCalledWith(200); + expect(mockReply.send).toHaveBeenCalledWith({ + chatName: specialName, + }); + }); + }); }); diff --git a/packages/shared/src/lib/ai/chat/index.ts b/packages/shared/src/lib/ai/chat/index.ts index fdeccc64db..1c6fa4f72d 100644 --- a/packages/shared/src/lib/ai/chat/index.ts +++ b/packages/shared/src/lib/ai/chat/index.ts @@ -84,6 +84,11 @@ export const ChatNameRequest = Type.Object({ }); export type ChatNameRequest = Static; +export const RenameChatRequest = Type.Object({ + chatId: Type.String(), +}); +export type RenameChatRequest = Static; + export const ChatsSummary = Type.Object({ chatId: Type.String(), chatName: Type.String(), diff --git a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx index e9d6847ee1..f5d3a4b4d8 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx @@ -1,5 +1,5 @@ import { t } from 'i18next'; -import { SquareArrowOutDownLeft, SquarePen } from 'lucide-react'; +import { PanelLeft, SquareArrowOutDownLeft, SquarePen } from 'lucide-react'; import { ReactNode } from 'react'; import { TooltipWrapper } from '../../components/tooltip-wrapper'; import { Button } from '../../ui/button'; @@ -8,6 +8,8 @@ type AssistantTopBarProps = { onClose: () => void; onNewChat: () => void; title?: string; + onToggleHistory?: () => void; + isHistoryOpen?: boolean; children: ReactNode; }; @@ -15,11 +17,28 @@ const AssistantTopBar = ({ onNewChat, onClose, title, + onToggleHistory, + isHistoryOpen, children, }: AssistantTopBarProps) => { return (
+ {onToggleHistory && ( + + + + )} {title} diff --git a/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx b/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx index e082118fa4..470e7c32fc 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx @@ -29,6 +29,8 @@ const AssistantUiChatContainer = ({ children, handleInject, toolComponents, + onToggleHistory, + isHistoryOpen, }: AssistantUiChatContainerProps) => { const codeVariation = useMemo(() => { return handleInject @@ -38,26 +40,38 @@ const AssistantUiChatContainer = ({ return (
- - {children} + + <> - - {Object.entries(toolComponents || {}).map(([key, tool]) => ( -
{tool}
- ))} - - - -
+ {isHistoryOpen ? ( +
{children}
+ ) : ( + + {Object.entries(toolComponents || {}).map(([key, tool]) => ( +
{tool}
+ ))} + +
+ +
+
+
+ )}
); }; diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx index a5cbe003b1..1e1cc2fcf9 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx @@ -14,7 +14,7 @@ type AssistantUiHistoryItemProps = { }; const ICON_CLASS_NAME = - 'text-primary cursor-pointer hover:bg-gray-300 rounded-xs'; + 'text-primary cursor-pointer hover:bg-gray-300 hover:text-outline rounded-xs'; const AssistantUiHistoryItem = ({ displayName, diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx index 7fe28eee80..50351f72ff 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx @@ -1,9 +1,5 @@ -import { t } from 'i18next'; -import { Plus } from 'lucide-react'; import { cn } from '../../../lib/cn'; -import { Button } from '../../../ui/button'; import { ScrollArea } from '../../../ui/scroll-area'; -import { TooltipWrapper } from '../../tooltip-wrapper'; import { AssistantUiHistoryItem } from './assistant-ui-history-item'; type AssistantUiHistoryProps = { @@ -34,17 +30,6 @@ const AssistantUiHistory = ({ className, )} > - - -
{chatItems.map((chatItem) => ( diff --git a/packages/ui-components/src/components/index.ts b/packages/ui-components/src/components/index.ts index 11a179d6a1..1dc190137f 100644 --- a/packages/ui-components/src/components/index.ts +++ b/packages/ui-components/src/components/index.ts @@ -1,6 +1,8 @@ export * from './ai-chat-container'; export * from './assistant-ui/assistant-top-bar'; export * from './assistant-ui/assistant-ui-chat-container'; +export * from './assistant-ui/history/assistant-ui-history'; +export * from './assistant-ui/history/assistant-ui-history-item'; export * from './assistant-ui/thread'; export * from './assistant-ui/thread-extra-context'; export * from './block-icon'; From 01396f261bb2c9875b0e5932833ec7395e617bb4 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 3 Nov 2025 13:23:20 +0530 Subject: [PATCH 02/24] Fix history item, assistant width --- .../ai/assistant/ai-chat-resizable-panel.tsx | 2 +- .../features/ai/lib/assistant-ui-chat-hook.ts | 2 +- .../assistant-ui/assistant-top-bar.tsx | 5 +- .../assistant-ui-chat-container.tsx | 41 +++++----- .../history/assistant-ui-history-item.tsx | 81 ++++++++++--------- .../src/components/tooltip-wrapper.tsx | 12 ++- 6 files changed, 84 insertions(+), 59 deletions(-) diff --git a/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx b/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx index 3554c5b40c..0faaa2730b 100644 --- a/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx +++ b/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx @@ -61,7 +61,7 @@ const AiChatResizablePanel = ({ onDragging }: AiChatResizablePanelProps) => { order={2} id={RESIZABLE_PANEL_IDS.AI_CHAT} className={cn('duration-0 min-w-0 shadow-sidebar', { - 'min-w-[300px] max-w-[500px] z-[11]': showChat, + 'min-w-[520px] max-w-[720px] z-[11]': showChat, })} minSize={size} maxSize={size} diff --git a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts index 9f40d8f276..cb4a9d426f 100644 --- a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts +++ b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts @@ -19,7 +19,7 @@ import { buildQueryKey } from './chat-utils'; import { createAdditionalContext } from './enrich-context'; import { ChatMode, UseAssistantChatProps } from './types'; -export const MAX_MESSAGES_BEFORE_NAME_GENERATION = 3; +export const MAX_MESSAGES_BEFORE_NAME_GENERATION = 1; export const useAssistantChat = ({ chatId, diff --git a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx index f5d3a4b4d8..7e064537c2 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx @@ -25,7 +25,10 @@ const AssistantTopBar = ({
{onToggleHistory && ( - +
); }; diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx index 1e1cc2fcf9..b28718343c 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx @@ -13,8 +13,7 @@ type AssistantUiHistoryItemProps = { onRename?: (newName: string) => void; }; -const ICON_CLASS_NAME = - 'text-primary cursor-pointer hover:bg-gray-300 hover:text-outline rounded-xs'; +const ICON_CLASS_NAME = 'text-primary cursor-pointer hover:text-outline'; const AssistantUiHistoryItem = ({ displayName, @@ -88,27 +87,31 @@ const AssistantUiHistoryItem = ({ autoFocus /> - { - event.stopPropagation(); - handleRename(); - }} - /> +
+ { + event.stopPropagation(); + handleRename(); + }} + /> +
- { - event.stopPropagation(); - setEditedName(displayName); - setIsEditing(false); - }} - /> +
+ { + event.stopPropagation(); + setEditedName(displayName); + setIsEditing(false); + }} + /> +
) : ( @@ -125,29 +128,33 @@ const AssistantUiHistoryItem = ({ > {onRename && ( - + { + event.stopPropagation(); + setEditedName(displayName); + setIsEditing(true); + }} + /> +
+ + )} + + +
+ { event.stopPropagation(); - setEditedName(displayName); - setIsEditing(true); + onDelete(); }} /> - - )} - - - { - event.stopPropagation(); - onDelete(); - }} - /> +
)} diff --git a/packages/ui-components/src/components/tooltip-wrapper.tsx b/packages/ui-components/src/components/tooltip-wrapper.tsx index a07bf39a8f..a49e12068d 100644 --- a/packages/ui-components/src/components/tooltip-wrapper.tsx +++ b/packages/ui-components/src/components/tooltip-wrapper.tsx @@ -4,12 +4,16 @@ type Props = { tooltipText: string | null | undefined; tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right'; delayDuration?: number; + align?: 'start' | 'center' | 'end'; + alignOffset?: number; children: React.ReactNode; }; const TooltipWrapper = ({ tooltipText, tooltipPlacement, + align, + alignOffset, children, delayDuration, }: Props) => { @@ -19,7 +23,13 @@ const TooltipWrapper = ({ return ( {children} - + {tooltipText} From fe222df332770da74f0c1bd676bf7beb849dd0e6 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Tue, 4 Nov 2025 12:45:02 +0530 Subject: [PATCH 03/24] Replace history panel with popover --- .../ai/assistant/ai-chat-resizable-panel.tsx | 2 +- .../ai/assistant/assistant-ui-chat.tsx | 8 +++++- .../assistant-ui/assistant-top-bar.tsx | 18 ++++++++----- .../assistant-ui-chat-container.tsx | 27 ++++++++++++------- .../history/assistant-ui-history-item.tsx | 11 ++++---- .../history/assistant-ui-history.tsx | 21 ++++++++++++--- .../assistant-ui-history.stories.tsx | 2 +- 7 files changed, 62 insertions(+), 27 deletions(-) diff --git a/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx b/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx index 0faaa2730b..efa7fc5302 100644 --- a/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx +++ b/packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx @@ -61,7 +61,7 @@ const AiChatResizablePanel = ({ onDragging }: AiChatResizablePanelProps) => { order={2} id={RESIZABLE_PANEL_IDS.AI_CHAT} className={cn('duration-0 min-w-0 shadow-sidebar', { - 'min-w-[520px] max-w-[720px] z-[11]': showChat, + 'min-w-[388px] max-w-[500px] z-[11]': showChat, })} minSize={size} maxSize={size} diff --git a/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx b/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx index acf059c998..e933f36427 100644 --- a/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx +++ b/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx @@ -134,7 +134,13 @@ const AssistantUiChat = ({ theme={theme} handleInject={handleInject} toolComponents={toolComponents} - onToggleHistory={() => setShowHistory(!showHistory)} + onHistoryOpenChange={(open) => { + if (open !== undefined) { + setShowHistory(open); + } else { + setShowHistory((prev) => !prev); + } + }} isHistoryOpen={showHistory} > {showHistory && ( diff --git a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx index 7e064537c2..1206d5959c 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx @@ -1,14 +1,15 @@ import { t } from 'i18next'; -import { PanelLeft, SquareArrowOutDownLeft, SquarePen } from 'lucide-react'; +import { History, SquareArrowOutDownLeft, SquarePen } from 'lucide-react'; import { ReactNode } from 'react'; import { TooltipWrapper } from '../../components/tooltip-wrapper'; +import { cn } from '../../lib/cn'; import { Button } from '../../ui/button'; type AssistantTopBarProps = { onClose: () => void; onNewChat: () => void; title?: string; - onToggleHistory?: () => void; + onHistoryOpenChange?: (open?: boolean) => void; isHistoryOpen?: boolean; children: ReactNode; }; @@ -17,14 +18,14 @@ const AssistantTopBar = ({ onNewChat, onClose, title, - onToggleHistory, + onHistoryOpenChange, isHistoryOpen, children, }: AssistantTopBarProps) => { return (
- {onToggleHistory && ( + {onHistoryOpenChange && ( { e.stopPropagation(); - onToggleHistory(); + onHistoryOpenChange?.(!isHistoryOpen); }} variant="secondary" size="icon" - className="text-outline size-[24px] rounded-xs" + className={cn( + 'text-outline size-[24px] rounded-xs', + isHistoryOpen && 'bg-gray-200', + )} > - + )} diff --git a/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx b/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx index 456be81f50..43629d066a 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx @@ -4,6 +4,7 @@ import { } from '@assistant-ui/react'; import { SourceCode } from '@openops/shared'; import { ReactNode, useMemo } from 'react'; +import { Popover, PopoverAnchor, PopoverContent } from '../../ui/popover'; import { MarkdownCodeVariations } from '../custom'; import { AssistantTopBar, AssistantTopBarProps } from './assistant-top-bar'; import { Thread, ThreadProps } from './thread'; @@ -31,7 +32,7 @@ const AssistantUiChatContainer = ({ children, handleInject, toolComponents, - onToggleHistory, + onHistoryOpenChange, isHistoryOpen, isShowingSlowWarning, connectionError, @@ -43,26 +44,34 @@ const AssistantUiChatContainer = ({ }, [handleInject]); return ( -
+
+ + +
+ + + {children} + + - <> + {null} {Object.entries(toolComponents || {}).map(([key, tool]) => (
{tool}
))}
- {isHistoryOpen ? ( -
- {children} -
- ) : null} void; }; -const ICON_CLASS_NAME = 'text-primary cursor-pointer hover:text-outline'; +const ICON_CLASS_NAME = 'text-[#0F0830] cursor-pointer hover:opacity-70'; const AssistantUiHistoryItem = ({ displayName, @@ -66,9 +66,10 @@ const AssistantUiHistoryItem = ({ role="option" tabIndex={isEditing ? -1 : 0} className={cn( - 'flex justify-between items-center gap-2 py-[9px] pl-[9px] pr-2 rounded-sm overflow-hidden cursor-pointer hover:bg-input hover:dark:bg-muted-foreground/80 group', + 'flex justify-between items-center gap-2 h-[38px] pl-[9px] pr-2 rounded-[8px] overflow-hidden cursor-pointer group', { - 'bg-input': isActive, + 'bg-gray-200': isActive, + 'hover:bg-gray-200/50': !isActive && !isEditing, }, )} onClick={isEditing ? undefined : onClick} @@ -83,7 +84,7 @@ const AssistantUiHistoryItem = ({ onChange={(e) => setEditedName(e.target.value)} onKeyDown={handleKeyDown} onBlur={handleRename} - className="flex-1 bg-transparent border-none outline-none focus:ring-0 font-normal dark:text-primary text-sm leading-snug" + className="flex-1 bg-transparent border-none outline-none focus:ring-0 font-normal text-black text-[14px] leading-[20px]" autoFocus /> @@ -117,7 +118,7 @@ const AssistantUiHistoryItem = ({ ) : ( )} {!isEditing && ( diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx index 50351f72ff..df9ab45c61 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx @@ -1,3 +1,4 @@ +import { Plus } from 'lucide-react'; import { cn } from '../../../lib/cn'; import { ScrollArea } from '../../../ui/scroll-area'; import { AssistantUiHistoryItem } from './assistant-ui-history-item'; @@ -26,12 +27,26 @@ const AssistantUiHistory = ({ return (
- -
+
+ +
+ +
{chatItems.map((chatItem) => ( ( -
+
From 975da575fe2d27724707d66eab8da610b0207afb Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Tue, 4 Nov 2025 13:04:36 +0530 Subject: [PATCH 04/24] Improve history popover trigger --- .../assistant-ui/assistant-top-bar.tsx | 49 ++++++++++++------- .../assistant-ui-chat-container.tsx | 15 +----- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx index 1206d5959c..07c6584d72 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx @@ -4,6 +4,7 @@ import { ReactNode } from 'react'; import { TooltipWrapper } from '../../components/tooltip-wrapper'; import { cn } from '../../lib/cn'; import { Button } from '../../ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'; type AssistantTopBarProps = { onClose: () => void; @@ -11,6 +12,7 @@ type AssistantTopBarProps = { title?: string; onHistoryOpenChange?: (open?: boolean) => void; isHistoryOpen?: boolean; + historyContent?: ReactNode; children: ReactNode; }; @@ -20,31 +22,42 @@ const AssistantTopBar = ({ title, onHistoryOpenChange, isHistoryOpen, + historyContent, children, }: AssistantTopBarProps) => { return (
{onHistoryOpenChange && ( - - + + + - - - + {historyContent} + + )} - {title} + {animatedTitle}
{children} diff --git a/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx b/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx index 8bdf1c0cc3..1a3647a40e 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-ui-chat-container.tsx @@ -35,6 +35,7 @@ const AssistantUiChatContainer = ({ isHistoryOpen, isShowingSlowWarning, connectionError, + chatId, }: AssistantUiChatContainerProps) => { const codeVariation = useMemo(() => { return handleInject @@ -51,6 +52,7 @@ const AssistantUiChatContainer = ({ onHistoryOpenChange={onHistoryOpenChange} isHistoryOpen={isHistoryOpen} historyContent={children} + chatId={chatId} > {null} diff --git a/packages/ui-components/src/hooks/use-typing-animation.ts b/packages/ui-components/src/hooks/use-typing-animation.ts new file mode 100644 index 0000000000..c0da126469 --- /dev/null +++ b/packages/ui-components/src/hooks/use-typing-animation.ts @@ -0,0 +1,61 @@ +import { useEffect, useRef, useState } from 'react'; + +type UseTypingAnimationOptions = { + text: string; + speed?: number; + fromText?: string; + chatId?: string | null; + defaultText?: string; +}; + +export function useTypingAnimation({ + text, + speed = 50, + fromText, + chatId, + defaultText, +}: UseTypingAnimationOptions): string { + const [displayedText, setDisplayedText] = useState(text); + const prevTextRef = useRef(text); + const prevChatIdRef = useRef(chatId); + + useEffect(() => { + let shouldAnimate = false; + + if (fromText !== undefined && defaultText !== undefined) { + const isSameChat = prevChatIdRef.current === chatId; + const wasDefault = + prevTextRef.current === defaultText || prevTextRef.current === fromText; + const nowHasName = text !== defaultText && text !== fromText; + + shouldAnimate = isSameChat && wasDefault && nowHasName; + } else if (fromText !== undefined) { + shouldAnimate = prevTextRef.current === fromText && text !== fromText; + } + + if (shouldAnimate && text.length > 0) { + setDisplayedText(''); + let currentIndex = 0; + + const interval = setInterval(() => { + if (currentIndex < text.length) { + setDisplayedText(text.slice(0, currentIndex + 1)); + currentIndex++; + } else { + clearInterval(interval); + } + }, speed); + + return () => { + clearInterval(interval); + }; + } else { + setDisplayedText(text); + } + + prevTextRef.current = text; + prevChatIdRef.current = chatId; + }, [text, speed, fromText, chatId, defaultText]); + + return displayedText; +} diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts index 6d3e8f316b..89d18e012b 100644 --- a/packages/ui-components/src/index.ts +++ b/packages/ui-components/src/index.ts @@ -81,6 +81,7 @@ export * from './lib/user-agent-utils'; /* export hooks */ export * from './hooks/use-copy-to-clipboard'; +export * from './hooks/use-typing-animation'; /* export custom components */ export * from './components'; From 61a5f163617cf439e45fc6bb73c67ff1730b016c Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Tue, 4 Nov 2025 14:24:24 +0530 Subject: [PATCH 08/24] Fix top bar buttons tooltip content --- .../src/components/assistant-ui/assistant-top-bar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx index c8745dbbe1..28dea5a253 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx @@ -47,6 +47,7 @@ const AssistantTopBar = ({ tooltipText={ isHistoryOpen ? t('Close history') : t('Open history') } + tooltipPlacement={isHistoryOpen ? 'right' : 'bottom'} align="start" > @@ -72,7 +73,11 @@ const AssistantTopBar = ({ )} - + diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx index df9ab45c61..003fe21509 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx @@ -31,14 +31,15 @@ const AssistantUiHistory = ({ className, )} > -
+
-
+
{chatItems.map((chatItem) => ( Date: Tue, 4 Nov 2025 19:21:17 +0530 Subject: [PATCH 11/24] Prevent chat name from overflowing in the top bar --- .../src/components/assistant-ui/assistant-top-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx index 0533f33904..0636811844 100644 --- a/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx +++ b/packages/ui-components/src/components/assistant-ui/assistant-top-bar.tsx @@ -91,7 +91,7 @@ const AssistantTopBar = ({ - {animatedTitle} + {animatedTitle}
{children} From 95af9d0a71bff37c277aacb70f4724198efc5348 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Tue, 4 Nov 2025 20:24:11 +0530 Subject: [PATCH 12/24] Rename constant for better clarity --- .../src/app/features/ai/lib/assistant-ui-chat-hook.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts index 917dd8d9c4..afe211d208 100644 --- a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts +++ b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts @@ -19,7 +19,7 @@ import { buildQueryKey } from './chat-utils'; import { createAdditionalContext } from './enrich-context'; import { ChatMode, UseAssistantChatProps } from './types'; -export const MAX_MESSAGES_BEFORE_NAME_GENERATION = 1; +export const MIN_MESSAGES_BEFORE_NAME_GENERATION = 1; export const useAssistantChat = ({ chatId, @@ -227,7 +227,7 @@ export const useAssistantChat = ({ return; } - if (messagesRef.current.length >= MAX_MESSAGES_BEFORE_NAME_GENERATION) { + if (messagesRef.current.length >= MIN_MESSAGES_BEFORE_NAME_GENERATION) { setTimeout(async () => { try { hasAttemptedNameGenerationRef.current[chatId] = true; From f02d4108067b084263d9de0cbd4993af36992a41 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Wed, 5 Nov 2025 12:53:42 +0530 Subject: [PATCH 13/24] Add error handling for AI history hook --- .../ai/lib/use-ai-assistant-chat-history.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts index 3512d3448b..934ed2d9b6 100644 --- a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts +++ b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts @@ -1,4 +1,6 @@ +import { toast } from '@openops/components/ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { t } from 'i18next'; import { aiAssistantChatHistoryApi } from './ai-assistant-chat-history-api'; export function useAssistantChatHistory() { @@ -21,12 +23,30 @@ export function useAssistantChatHistory() { const deleteMutation = useMutation({ mutationFn: (chatId: string) => aiAssistantChatHistoryApi.delete(chatId), onSuccess: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + onError: (error) => { + console.error('Failed to delete chat', error); + toast({ + title: t('Error'), + variant: 'destructive', + description: t('Failed to delete chat. Please try again.'), + duration: 3000, + }); + }, }); const renameMutation = useMutation({ mutationFn: ({ chatId, chatName }: { chatId: string; chatName: string }) => aiAssistantChatHistoryApi.rename(chatId, chatName), onSuccess: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + onError: (error) => { + console.error('Failed to rename chat', error); + toast({ + title: t('Error'), + variant: 'destructive', + description: t('Failed to rename chat. Please try again.'), + duration: 3000, + }); + }, }); return { From 67d3cb1538a39c1cfda6678155d1594d964d522b Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Wed, 5 Nov 2025 13:15:13 +0530 Subject: [PATCH 14/24] Remove redundant chatContextKey function --- packages/server/api/src/app/ai/chat/ai-chat.service.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/server/api/src/app/ai/chat/ai-chat.service.ts b/packages/server/api/src/app/ai/chat/ai-chat.service.ts index bc693c97fc..2e6e42bf2d 100644 --- a/packages/server/api/src/app/ai/chat/ai-chat.service.ts +++ b/packages/server/api/src/app/ai/chat/ai-chat.service.ts @@ -216,19 +216,13 @@ export const deleteChatHistory = async ( await cacheWrapper.deleteKey(chatHistoryKey(chatId, userId, projectId)); }; -const chatContextKeyFor = ( - chatId: string, - userId: string, - projectId: string, -): string => `${projectId}:${userId}:${chatId}:context`; - export const deleteChat = async ( chatId: string, userId: string, projectId: string, ): Promise => { await cacheWrapper.deleteKey(chatHistoryKey(chatId, userId, projectId)); - await cacheWrapper.deleteKey(chatContextKeyFor(chatId, userId, projectId)); + await cacheWrapper.deleteKey(chatContextKey(chatId, userId, projectId)); }; export async function getLLMConfig( From 7dd6e7ac941daee0598c285a1e230f35c02eabb9 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Wed, 5 Nov 2025 13:22:57 +0530 Subject: [PATCH 15/24] Add tests for delete chat endpoint --- .../unit/ai/ai-mcp-chat.controller.test.ts | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts index c300857f02..ecac806b8a 100644 --- a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts +++ b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts @@ -8,6 +8,7 @@ import { } from 'fastify'; import { createChatContext, + deleteChat, generateChatName, getAllChats, getChatContext, @@ -90,6 +91,7 @@ jest.mock('../../../src/app/ai/chat/ai-chat.service', () => ({ generateChatName: jest.fn(), updateChatName: jest.fn(), getAllChats: jest.fn(), + deleteChat: jest.fn(), })); jest.mock('@openops/common', () => ({ @@ -990,4 +992,149 @@ describe('AI MCP Chat Controller - Tool Service Interactions', () => { }); }); }); + + describe('DELETE /:chatId (delete chat)', () => { + let deleteHandler: RouteHandler; + + beforeEach(async () => { + jest.clearAllMocks(); + handlers = {}; + await aiMCPChatController(mockApp, {} as FastifyPluginOptions); + deleteHandler = handlers['/:chatId']; + }); + + it('should successfully delete a chat', async () => { + (deleteChat as jest.Mock).mockResolvedValue(undefined); + + const request = { + ...mockRequest, + params: { chatId: 'test-chat-id' }, + } as FastifyRequest; + + await deleteHandler(request, mockReply as unknown as FastifyReply); + + expect(deleteChat).toHaveBeenCalledWith( + 'test-chat-id', + 'test-user-id', + 'test-project-id', + ); + expect(mockReply.code).toHaveBeenCalledWith(200); + expect(mockReply.send).toHaveBeenCalled(); + }); + + it('should handle deletion of non-existent chat gracefully', async () => { + (deleteChat as jest.Mock).mockResolvedValue(undefined); + + const request = { + ...mockRequest, + params: { chatId: 'non-existent-chat-id' }, + } as FastifyRequest; + + await deleteHandler(request, mockReply as unknown as FastifyReply); + + expect(deleteChat).toHaveBeenCalledWith( + 'non-existent-chat-id', + 'test-user-id', + 'test-project-id', + ); + expect(mockReply.code).toHaveBeenCalledWith(200); + expect(mockReply.send).toHaveBeenCalled(); + }); + + it('should handle ApplicationError correctly', async () => { + const { ApplicationError, ErrorCode } = await import('@openops/shared'); + const appError = new ApplicationError({ + code: ErrorCode.ENTITY_NOT_FOUND, + params: { + message: 'Chat not found', + entityType: 'Chat Session', + entityId: 'test-chat-id', + }, + }); + + (deleteChat as jest.Mock).mockRejectedValue(appError); + + const request = { + ...mockRequest, + params: { chatId: 'test-chat-id' }, + } as FastifyRequest; + + await deleteHandler(request, mockReply as unknown as FastifyReply); + + expect(deleteChat).toHaveBeenCalledWith( + 'test-chat-id', + 'test-user-id', + 'test-project-id', + ); + expect(mockReply.code).toHaveBeenCalledWith(400); + expect(mockReply.send).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.any(String) }), + ); + }); + + it('should handle timeout errors', async () => { + (deleteChat as jest.Mock).mockRejectedValue( + new Error('Operation timed out'), + ); + + const request = { + ...mockRequest, + params: { chatId: 'test-chat-id' }, + } as FastifyRequest; + + await deleteHandler(request, mockReply as unknown as FastifyReply); + + expect(deleteChat).toHaveBeenCalledWith( + 'test-chat-id', + 'test-user-id', + 'test-project-id', + ); + expect(mockReply.code).toHaveBeenCalledWith(500); + expect(mockReply.send).toHaveBeenCalledWith({ + message: 'Internal server error', + }); + }); + + it('should call deleteChat with correct user and project parameters', async () => { + (deleteChat as jest.Mock).mockResolvedValue(undefined); + + const customRequest = { + ...mockRequest, + principal: { + id: 'different-user-id', + projectId: 'different-project-id', + type: PrincipalType.USER, + }, + params: { chatId: 'custom-chat-id' }, + } as FastifyRequest; + + await deleteHandler(customRequest, mockReply as unknown as FastifyReply); + + expect(deleteChat).toHaveBeenCalledWith( + 'custom-chat-id', + 'different-user-id', + 'different-project-id', + ); + expect(mockReply.code).toHaveBeenCalledWith(200); + }); + + it('should handle chat IDs with special characters', async () => { + (deleteChat as jest.Mock).mockResolvedValue(undefined); + + const specialChatId = 'chat-id-with-special-chars-123!@#'; + const request = { + ...mockRequest, + params: { chatId: specialChatId }, + } as FastifyRequest; + + await deleteHandler(request, mockReply as unknown as FastifyReply); + + expect(deleteChat).toHaveBeenCalledWith( + specialChatId, + 'test-user-id', + 'test-project-id', + ); + expect(mockReply.code).toHaveBeenCalledWith(200); + }); + }); }); From 0141f868c707ff3768dde9b524dcfd484f1af46a Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Wed, 5 Nov 2025 13:42:42 +0530 Subject: [PATCH 16/24] Fix lint error --- .../unit/ai/ai-mcp-chat.controller.test.ts | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts index ecac806b8a..60bb7ded87 100644 --- a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts +++ b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts @@ -1041,37 +1041,6 @@ describe('AI MCP Chat Controller - Tool Service Interactions', () => { expect(mockReply.send).toHaveBeenCalled(); }); - it('should handle ApplicationError correctly', async () => { - const { ApplicationError, ErrorCode } = await import('@openops/shared'); - const appError = new ApplicationError({ - code: ErrorCode.ENTITY_NOT_FOUND, - params: { - message: 'Chat not found', - entityType: 'Chat Session', - entityId: 'test-chat-id', - }, - }); - - (deleteChat as jest.Mock).mockRejectedValue(appError); - - const request = { - ...mockRequest, - params: { chatId: 'test-chat-id' }, - } as FastifyRequest; - - await deleteHandler(request, mockReply as unknown as FastifyReply); - - expect(deleteChat).toHaveBeenCalledWith( - 'test-chat-id', - 'test-user-id', - 'test-project-id', - ); - expect(mockReply.code).toHaveBeenCalledWith(400); - expect(mockReply.send).toHaveBeenCalledWith( - expect.objectContaining({ message: expect.any(String) }), - ); - }); - it('should handle timeout errors', async () => { (deleteChat as jest.Mock).mockRejectedValue( new Error('Operation timed out'), From aa04c005681f646eb6da677565d06d418b531fe4 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Wed, 5 Nov 2025 16:51:27 +0530 Subject: [PATCH 17/24] Revert backend changes to open a new PR --- .../api/src/app/ai/chat/ai-chat.service.ts | 9 - .../src/app/ai/chat/ai-mcp-chat.controller.ts | 42 +-- .../unit/ai/ai-mcp-chat.controller.test.ts | 260 ------------------ packages/shared/src/lib/ai/chat/index.ts | 5 - 4 files changed, 5 insertions(+), 311 deletions(-) diff --git a/packages/server/api/src/app/ai/chat/ai-chat.service.ts b/packages/server/api/src/app/ai/chat/ai-chat.service.ts index 1640074c51..d7c240943f 100644 --- a/packages/server/api/src/app/ai/chat/ai-chat.service.ts +++ b/packages/server/api/src/app/ai/chat/ai-chat.service.ts @@ -264,15 +264,6 @@ export const getChatTools = async ( return toolNames ?? []; }; -export const deleteChat = async ( - chatId: string, - userId: string, - projectId: string, -): Promise => { - await cacheWrapper.deleteKey(chatHistoryKey(chatId, userId, projectId)); - await cacheWrapper.deleteKey(chatContextKey(chatId, userId, projectId)); -}; - export async function getLLMConfig( projectId: string, contextModel?: string, diff --git a/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts b/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts index 67b7d0d132..db29aacd0a 100644 --- a/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts +++ b/packages/server/api/src/app/ai/chat/ai-mcp-chat.controller.ts @@ -1,7 +1,4 @@ -import { - FastifyPluginAsyncTypebox, - Type, -} from '@fastify/type-provider-typebox'; +import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'; import { observe, updateActiveObservation } from '@langfuse/tracing'; import { getLangfuseSpanProcessor, @@ -20,7 +17,6 @@ import { OpenChatResponse, openOpsId, PrincipalType, - RenameChatRequest, UpdateChatModelRequest, UpdateChatModelResponse, } from '@openops/shared'; @@ -29,7 +25,7 @@ import { FastifyReply } from 'fastify'; import { StatusCodes } from 'http-status-codes'; import { createChatContext, - deleteChat, + deleteChatHistory, generateChatId, generateChatIdForMCP, generateChatName, @@ -49,7 +45,7 @@ import { parseUserMessage } from './message-parser'; import { createUserMessage } from './model-message-factory'; import { getBlockSystemPrompt } from './prompts.service'; -export const DEFAULT_CHAT_NAME = 'New Chat'; +const DEFAULT_CHAT_NAME = 'New Chat'; export const aiMCPChatController: FastifyPluginAsyncTypebox = async (app) => { app.post( @@ -258,20 +254,6 @@ export const aiMCPChatController: FastifyPluginAsyncTypebox = async (app) => { } }); - app.patch('/:chatId/name', RenameChatOptions, async (request, reply) => { - const { chatId } = request.params; - const { chatName } = request.body; - const userId = request.principal.id; - const projectId = request.principal.projectId; - - try { - await updateChatName(chatId, userId, projectId, chatName); - return await reply.code(200).send({ chatName }); - } catch (error) { - return handleError(error, reply, 'rename chat'); - } - }); - app.post('/code', CodeGenerationOptions, async (request, reply) => { const chatId = request.body.chatId; const projectId = request.principal.projectId; @@ -414,10 +396,10 @@ export const aiMCPChatController: FastifyPluginAsyncTypebox = async (app) => { const projectId = request.principal.projectId; try { - await deleteChat(chatId, userId, projectId); + await deleteChatHistory(chatId, userId, projectId); return await reply.code(StatusCodes.OK).send(); } catch (error) { - return handleError(error, reply, 'delete chat'); + return handleError(error, reply, 'delete chat history'); } }); }; @@ -457,20 +439,6 @@ const ChatNameOptions = { }, }; -const RenameChatOptions = { - config: { - allowedPrincipals: [PrincipalType.USER], - }, - schema: { - tags: ['ai', 'ai-chat-mcp'], - description: 'Rename a chat session with a user provided name.', - params: RenameChatRequest, - body: Type.Object({ - chatName: Type.String(), - }), - }, -}; - const CodeGenerationOptions = { config: { allowedPrincipals: [PrincipalType.USER], diff --git a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts index 60bb7ded87..73ac8e7554 100644 --- a/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts +++ b/packages/server/api/test/unit/ai/ai-mcp-chat.controller.test.ts @@ -8,7 +8,6 @@ import { } from 'fastify'; import { createChatContext, - deleteChat, generateChatName, getAllChats, getChatContext, @@ -91,7 +90,6 @@ jest.mock('../../../src/app/ai/chat/ai-chat.service', () => ({ generateChatName: jest.fn(), updateChatName: jest.fn(), getAllChats: jest.fn(), - deleteChat: jest.fn(), })); jest.mock('@openops/common', () => ({ @@ -123,10 +121,6 @@ describe('AI MCP Chat Controller - Tool Service Interactions', () => { handlers[path] = handler; return mockApp; }), - patch: jest.fn((path: string, _: unknown, handler: RouteHandler) => { - handlers[path] = handler; - return mockApp; - }), delete: jest.fn((path: string, _: unknown, handler: RouteHandler) => { handlers[path] = handler; return mockApp; @@ -852,258 +846,4 @@ describe('AI MCP Chat Controller - Tool Service Interactions', () => { ); }); }); - - describe('PATCH /:chatId/name (rename chat)', () => { - let patchHandler: RouteHandler; - - const mockChatContext = { - chatId: 'test-chat-id', - chatName: 'Old Chat Name', - provider: AiProviderEnum.ANTHROPIC, - model: 'claude-3-sonnet', - }; - - beforeEach(async () => { - jest.clearAllMocks(); - handlers = {}; - await aiMCPChatController(mockApp, {} as FastifyPluginOptions); - patchHandler = handlers['/:chatId/name']; - }); - - it('should successfully rename a chat', async () => { - (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); - (updateChatName as jest.Mock).mockResolvedValue(undefined); - - const request = { - ...mockRequest, - params: { chatId: 'test-chat-id' }, - body: { chatName: 'New Chat Name' }, - } as FastifyRequest; - - await patchHandler(request, mockReply as unknown as FastifyReply); - - expect(updateChatName).toHaveBeenCalledWith( - 'test-chat-id', - 'test-user-id', - 'test-project-id', - 'New Chat Name', - ); - expect(mockReply.code).toHaveBeenCalledWith(200); - expect(mockReply.send).toHaveBeenCalledWith({ - chatName: 'New Chat Name', - }); - }); - - it('should return error when chat context not found', async () => { - (updateChatName as jest.Mock).mockRejectedValue( - new Error('Chat context not found'), - ); - - const request = { - ...mockRequest, - params: { chatId: 'non-existent-chat-id' }, - body: { chatName: 'New Name' }, - } as FastifyRequest; - - await patchHandler(request, mockReply as unknown as FastifyReply); - - expect(updateChatName).toHaveBeenCalledWith( - 'non-existent-chat-id', - 'test-user-id', - 'test-project-id', - 'New Name', - ); - expect(mockReply.code).toHaveBeenCalledWith(500); - expect(mockReply.send).toHaveBeenCalledWith( - expect.objectContaining({ message: 'Internal server error' }), - ); - }); - - it('should handle service errors gracefully', async () => { - (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); - (updateChatName as jest.Mock).mockRejectedValue( - new Error('Database error'), - ); - - const request = { - ...mockRequest, - params: { chatId: 'test-chat-id' }, - body: { chatName: 'New Name' }, - } as FastifyRequest; - - await patchHandler(request, mockReply as unknown as FastifyReply); - - expect(updateChatName).toHaveBeenCalledWith( - 'test-chat-id', - 'test-user-id', - 'test-project-id', - 'New Name', - ); - expect(mockReply.code).toHaveBeenCalledWith(500); - expect(mockReply.send).toHaveBeenCalledWith( - expect.objectContaining({ message: 'Internal server error' }), - ); - }); - - it('should handle empty chat name', async () => { - (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); - (updateChatName as jest.Mock).mockResolvedValue(undefined); - - const request = { - ...mockRequest, - params: { chatId: 'test-chat-id' }, - body: { chatName: '' }, - } as FastifyRequest; - - await patchHandler(request, mockReply as unknown as FastifyReply); - - expect(updateChatName).toHaveBeenCalledWith( - 'test-chat-id', - 'test-user-id', - 'test-project-id', - '', - ); - expect(mockReply.code).toHaveBeenCalledWith(200); - expect(mockReply.send).toHaveBeenCalledWith({ chatName: '' }); - }); - - it('should handle chat names with special characters', async () => { - (getChatContext as jest.Mock).mockResolvedValue(mockChatContext); - (updateChatName as jest.Mock).mockResolvedValue(undefined); - - const specialName = 'Chat with & special chars: #1 @test'; - const request = { - ...mockRequest, - params: { chatId: 'test-chat-id' }, - body: { chatName: specialName }, - } as FastifyRequest; - - await patchHandler(request, mockReply as unknown as FastifyReply); - - expect(updateChatName).toHaveBeenCalledWith( - 'test-chat-id', - 'test-user-id', - 'test-project-id', - specialName, - ); - expect(mockReply.code).toHaveBeenCalledWith(200); - expect(mockReply.send).toHaveBeenCalledWith({ - chatName: specialName, - }); - }); - }); - - describe('DELETE /:chatId (delete chat)', () => { - let deleteHandler: RouteHandler; - - beforeEach(async () => { - jest.clearAllMocks(); - handlers = {}; - await aiMCPChatController(mockApp, {} as FastifyPluginOptions); - deleteHandler = handlers['/:chatId']; - }); - - it('should successfully delete a chat', async () => { - (deleteChat as jest.Mock).mockResolvedValue(undefined); - - const request = { - ...mockRequest, - params: { chatId: 'test-chat-id' }, - } as FastifyRequest; - - await deleteHandler(request, mockReply as unknown as FastifyReply); - - expect(deleteChat).toHaveBeenCalledWith( - 'test-chat-id', - 'test-user-id', - 'test-project-id', - ); - expect(mockReply.code).toHaveBeenCalledWith(200); - expect(mockReply.send).toHaveBeenCalled(); - }); - - it('should handle deletion of non-existent chat gracefully', async () => { - (deleteChat as jest.Mock).mockResolvedValue(undefined); - - const request = { - ...mockRequest, - params: { chatId: 'non-existent-chat-id' }, - } as FastifyRequest; - - await deleteHandler(request, mockReply as unknown as FastifyReply); - - expect(deleteChat).toHaveBeenCalledWith( - 'non-existent-chat-id', - 'test-user-id', - 'test-project-id', - ); - expect(mockReply.code).toHaveBeenCalledWith(200); - expect(mockReply.send).toHaveBeenCalled(); - }); - - it('should handle timeout errors', async () => { - (deleteChat as jest.Mock).mockRejectedValue( - new Error('Operation timed out'), - ); - - const request = { - ...mockRequest, - params: { chatId: 'test-chat-id' }, - } as FastifyRequest; - - await deleteHandler(request, mockReply as unknown as FastifyReply); - - expect(deleteChat).toHaveBeenCalledWith( - 'test-chat-id', - 'test-user-id', - 'test-project-id', - ); - expect(mockReply.code).toHaveBeenCalledWith(500); - expect(mockReply.send).toHaveBeenCalledWith({ - message: 'Internal server error', - }); - }); - - it('should call deleteChat with correct user and project parameters', async () => { - (deleteChat as jest.Mock).mockResolvedValue(undefined); - - const customRequest = { - ...mockRequest, - principal: { - id: 'different-user-id', - projectId: 'different-project-id', - type: PrincipalType.USER, - }, - params: { chatId: 'custom-chat-id' }, - } as FastifyRequest; - - await deleteHandler(customRequest, mockReply as unknown as FastifyReply); - - expect(deleteChat).toHaveBeenCalledWith( - 'custom-chat-id', - 'different-user-id', - 'different-project-id', - ); - expect(mockReply.code).toHaveBeenCalledWith(200); - }); - - it('should handle chat IDs with special characters', async () => { - (deleteChat as jest.Mock).mockResolvedValue(undefined); - - const specialChatId = 'chat-id-with-special-chars-123!@#'; - const request = { - ...mockRequest, - params: { chatId: specialChatId }, - } as FastifyRequest; - - await deleteHandler(request, mockReply as unknown as FastifyReply); - - expect(deleteChat).toHaveBeenCalledWith( - specialChatId, - 'test-user-id', - 'test-project-id', - ); - expect(mockReply.code).toHaveBeenCalledWith(200); - }); - }); }); diff --git a/packages/shared/src/lib/ai/chat/index.ts b/packages/shared/src/lib/ai/chat/index.ts index 1c6fa4f72d..fdeccc64db 100644 --- a/packages/shared/src/lib/ai/chat/index.ts +++ b/packages/shared/src/lib/ai/chat/index.ts @@ -84,11 +84,6 @@ export const ChatNameRequest = Type.Object({ }); export type ChatNameRequest = Static; -export const RenameChatRequest = Type.Object({ - chatId: Type.String(), -}); -export type RenameChatRequest = Static; - export const ChatsSummary = Type.Object({ chatId: Type.String(), chatName: Type.String(), From fea2b0893d046f76a87b5ff53ac67b04c8f02f43 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 10 Nov 2025 11:35:09 +0530 Subject: [PATCH 18/24] Fix potential memory leak with animation effect --- .../ui-components/src/hooks/use-typing-animation.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/ui-components/src/hooks/use-typing-animation.ts b/packages/ui-components/src/hooks/use-typing-animation.ts index c0da126469..e0dc0864c0 100644 --- a/packages/ui-components/src/hooks/use-typing-animation.ts +++ b/packages/ui-components/src/hooks/use-typing-animation.ts @@ -21,6 +21,7 @@ export function useTypingAnimation({ useEffect(() => { let shouldAnimate = false; + let interval: NodeJS.Timeout | undefined; if (fromText !== undefined && defaultText !== undefined) { const isSameChat = prevChatIdRef.current === chatId; @@ -37,7 +38,7 @@ export function useTypingAnimation({ setDisplayedText(''); let currentIndex = 0; - const interval = setInterval(() => { + interval = setInterval(() => { if (currentIndex < text.length) { setDisplayedText(text.slice(0, currentIndex + 1)); currentIndex++; @@ -45,16 +46,18 @@ export function useTypingAnimation({ clearInterval(interval); } }, speed); - - return () => { - clearInterval(interval); - }; } else { setDisplayedText(text); } prevTextRef.current = text; prevChatIdRef.current = chatId; + + return () => { + if (interval) { + clearInterval(interval); + } + }; }, [text, speed, fromText, chatId, defaultText]); return displayedText; From 840454dcf09ea43d348f45e4b747c57f6daaa2a2 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 10 Nov 2025 11:40:33 +0530 Subject: [PATCH 19/24] Remove unnecessary use of timeout --- .../features/ai/lib/assistant-ui-chat-hook.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts index 23dfb7516f..29bc01d271 100644 --- a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts +++ b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts @@ -240,17 +240,15 @@ export const useAssistantChat = ({ } if (messagesRef.current.length >= MIN_MESSAGES_BEFORE_NAME_GENERATION) { - setTimeout(async () => { - try { - hasAttemptedNameGenerationRef.current[chatId] = true; - await aiAssistantChatHistoryApi.generateName(chatId); - qc.invalidateQueries({ queryKey: ['assistant-history'] }); - } catch (error) { - console.error('Failed to generate chat name', error); - hasAttemptedNameGenerationRef.current[chatId] = false; - qc.invalidateQueries({ queryKey: ['assistant-history'] }); - } - }, 500); + try { + hasAttemptedNameGenerationRef.current[chatId] = true; + await aiAssistantChatHistoryApi.generateName(chatId); + qc.invalidateQueries({ queryKey: ['assistant-history'] }); + } catch (error) { + console.error('Failed to generate chat name', error); + hasAttemptedNameGenerationRef.current[chatId] = false; + qc.invalidateQueries({ queryKey: ['assistant-history'] }); + } } }, // https://github.com/assistant-ui/assistant-ui/issues/2327 From c57b6c0e9dd361564f5cf6fc8c8748bc6d66ab34 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 10 Nov 2025 11:50:35 +0530 Subject: [PATCH 20/24] Extract assistant history query key to keys file --- packages/react-ui/src/app/constants/query-keys.ts | 1 + .../app/features/ai/lib/assistant-ui-chat-hook.ts | 5 +++-- .../features/ai/lib/use-ai-assistant-chat-history.ts | 12 ++++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/react-ui/src/app/constants/query-keys.ts b/packages/react-ui/src/app/constants/query-keys.ts index 9e9a25bdaf..8734056b04 100644 --- a/packages/react-ui/src/app/constants/query-keys.ts +++ b/packages/react-ui/src/app/constants/query-keys.ts @@ -10,6 +10,7 @@ export const QueryKeys = { mcpSettings: 'mcp-settings', aiSettingsProviders: 'ai-settings-providers', aiProviderModels: 'ai-provider-models', + assistantHistory: 'assistant-history', // Platform organization: 'organization', diff --git a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts index 29bc01d271..56e38bfa73 100644 --- a/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts +++ b/packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts @@ -1,3 +1,4 @@ +import { QueryKeys } from '@/app/constants/query-keys'; import { aiAssistantChatApi } from '@/app/features/ai/lib/ai-assistant-chat-api'; import { getActionName, getBlockName } from '@/app/features/blocks/lib/utils'; import { authenticationSession } from '@/app/lib/authentication-session'; @@ -243,11 +244,11 @@ export const useAssistantChat = ({ try { hasAttemptedNameGenerationRef.current[chatId] = true; await aiAssistantChatHistoryApi.generateName(chatId); - qc.invalidateQueries({ queryKey: ['assistant-history'] }); + qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }); } catch (error) { console.error('Failed to generate chat name', error); hasAttemptedNameGenerationRef.current[chatId] = false; - qc.invalidateQueries({ queryKey: ['assistant-history'] }); + qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }); } } }, diff --git a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts index 934ed2d9b6..1c966a9d47 100644 --- a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts +++ b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts @@ -1,3 +1,4 @@ +import { QueryKeys } from '@/app/constants/query-keys'; import { toast } from '@openops/components/ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { t } from 'i18next'; @@ -7,7 +8,7 @@ export function useAssistantChatHistory() { const qc = useQueryClient(); const { data, isLoading } = useQuery({ - queryKey: ['assistant-history'], + queryKey: [QueryKeys.assistantHistory], queryFn: async () => { const res = await aiAssistantChatHistoryApi.list(); return res.chats ?? []; @@ -22,7 +23,8 @@ export function useAssistantChatHistory() { const deleteMutation = useMutation({ mutationFn: (chatId: string) => aiAssistantChatHistoryApi.delete(chatId), - onSuccess: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + onSuccess: () => + qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }), onError: (error) => { console.error('Failed to delete chat', error); toast({ @@ -37,7 +39,8 @@ export function useAssistantChatHistory() { const renameMutation = useMutation({ mutationFn: ({ chatId, chatName }: { chatId: string; chatName: string }) => aiAssistantChatHistoryApi.rename(chatId, chatName), - onSuccess: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + onSuccess: () => + qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }), onError: (error) => { console.error('Failed to rename chat', error); toast({ @@ -54,6 +57,7 @@ export function useAssistantChatHistory() { isLoading, deleteChat: deleteMutation.mutateAsync, renameChat: renameMutation.mutateAsync, - refetch: () => qc.invalidateQueries({ queryKey: ['assistant-history'] }), + refetch: () => + qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }), }; } From 98f6b77c3597a3ac017eae917f614964a0896f35 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 10 Nov 2025 11:58:44 +0530 Subject: [PATCH 21/24] Refetch chat history on window focus --- .../src/app/features/ai/lib/use-ai-assistant-chat-history.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts index 1c966a9d47..15f0672534 100644 --- a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts +++ b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts @@ -18,7 +18,6 @@ export function useAssistantChatHistory() { id: c.chatId, displayName: c.chatName || 'New chat', })), - refetchOnWindowFocus: false, }); const deleteMutation = useMutation({ From 4ec5fb3abc8d2df0d8c72a187e79c3e4b2758a88 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 10 Nov 2025 12:38:07 +0530 Subject: [PATCH 22/24] Use better name for refetch function --- .../src/app/features/ai/assistant/assistant-ui-chat.tsx | 6 +++--- .../app/features/ai/lib/use-ai-assistant-chat-history.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx b/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx index 45fc5e5180..08685e63da 100644 --- a/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx +++ b/packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx @@ -77,7 +77,7 @@ const AssistantUiChat = ({ isLoading: isHistoryLoading, deleteChat, renameChat, - refetch, + refetchChatList, } = useAssistantChatHistory(); const { isShowingSlowWarning, connectionError } = @@ -118,9 +118,9 @@ const AssistantUiChat = ({ const onNewChatClick = useCallback(async () => { await createNewChat(); - await refetch(); + refetchChatList(); setShowHistory(false); - }, [createNewChat, refetch]); + }, [createNewChat, refetchChatList]); if (isLoading) { return ( diff --git a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts index 15f0672534..a9491e1882 100644 --- a/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts +++ b/packages/react-ui/src/app/features/ai/lib/use-ai-assistant-chat-history.ts @@ -56,7 +56,7 @@ export function useAssistantChatHistory() { isLoading, deleteChat: deleteMutation.mutateAsync, renameChat: renameMutation.mutateAsync, - refetch: () => + refetchChatList: () => qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }), }; } From 33c472ab4692a64cfcf3e8858c2129cfbbc2b8a1 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 10 Nov 2025 13:23:52 +0530 Subject: [PATCH 23/24] Fix comments about tailwind classes --- .../assistant-ui/history/assistant-ui-history-item.tsx | 6 +++--- .../assistant-ui/history/assistant-ui-history.tsx | 2 +- packages/ui-components/tailwind.base.config.cjs | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx index 1951797881..8f48933a8b 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history-item.tsx @@ -66,7 +66,7 @@ const AssistantUiHistoryItem = ({ role="option" tabIndex={isEditing ? -1 : 0} className={cn( - 'flex justify-between items-center gap-2 h-[38px] pl-[9px] pr-2 rounded-[8px] overflow-hidden cursor-pointer group', + 'flex justify-between items-center gap-2 h-[38px] pl-[9px] pr-2 rounded-sm overflow-hidden cursor-pointer group', { 'bg-gray-200': isActive, 'hover:bg-gray-200/50': !isActive && !isEditing, @@ -84,7 +84,7 @@ const AssistantUiHistoryItem = ({ onChange={(e) => setEditedName(e.target.value)} onKeyDown={handleKeyDown} onBlur={handleRename} - className="flex-1 bg-transparent border-none outline-none focus:ring-0 font-normal text-black text-[14px] leading-[20px]" + className="flex-1 bg-transparent border-none outline-none focus:ring-0 font-normal text-black text-sm" autoFocus /> @@ -118,7 +118,7 @@ const AssistantUiHistoryItem = ({ ) : ( )} {!isEditing && ( diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx index 003fe21509..b5e2958668 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx @@ -27,7 +27,7 @@ const AssistantUiHistory = ({ return (
diff --git a/packages/ui-components/tailwind.base.config.cjs b/packages/ui-components/tailwind.base.config.cjs index 3a36722975..654c8f5597 100644 --- a/packages/ui-components/tailwind.base.config.cjs +++ b/packages/ui-components/tailwind.base.config.cjs @@ -175,6 +175,7 @@ module.exports = { 'step-container': '0px 0px 22px hsl(var(--border))', 'add-button': 'var(--add-button-shadow)', sidebar: '0px 0px 10px 0px rgba(var(--sidebar-shadow-color))', + 'ai-history-popover': '0px 0px 7px 0px rgba(187,193,218,0.5)', }, contain: { layout: 'layout', From bf86fd433d6284424227a92bd360136c6b3b1599 Mon Sep 17 00:00:00 2001 From: Ravi Kiran Date: Mon, 10 Nov 2025 13:44:04 +0530 Subject: [PATCH 24/24] Fix tailwind classes --- .../assistant-ui/history/assistant-ui-history.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx index b5e2958668..070372cac4 100644 --- a/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx +++ b/packages/ui-components/src/components/assistant-ui/history/assistant-ui-history.tsx @@ -27,11 +27,11 @@ const AssistantUiHistory = ({ return (
-
+
- -
+ +
{chatItems.map((chatItem) => (