Skip to content

Commit 910106c

Browse files
authored
Add chat history to Assistant Ai (#1550)
Fixes OPS-2115. Deployed to UX Approved by Olga ## Additional Notes <img width="680" height="812" alt="Screenshot 2025-11-04 at 2 30 34 PM" src="https://github.com/user-attachments/assets/fadbe0c1-4eb2-4eea-a46c-a134df2ff9bd" />
1 parent 8fd5281 commit 910106c

16 files changed

Lines changed: 437 additions & 99 deletions

packages/react-ui/src/app/constants/query-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const QueryKeys = {
1010
mcpSettings: 'mcp-settings',
1111
aiSettingsProviders: 'ai-settings-providers',
1212
aiProviderModels: 'ai-provider-models',
13+
assistantHistory: 'assistant-history',
1314

1415
// Platform
1516
organization: 'organization',

packages/react-ui/src/app/features/ai/assistant/ai-chat-resizable-panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const AiChatResizablePanel = ({ onDragging }: AiChatResizablePanelProps) => {
6161
order={2}
6262
id={RESIZABLE_PANEL_IDS.AI_CHAT}
6363
className={cn('duration-0 min-w-0 shadow-sidebar', {
64-
'min-w-[300px] max-w-[500px] z-[11]': showChat,
64+
'min-w-[388px] max-w-[500px] z-[11]': showChat,
6565
})}
6666
minSize={size}
6767
maxSize={size}

packages/react-ui/src/app/features/ai/assistant/assistant-ui-chat.tsx

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { AI_ASSISTANT_SS_KEY } from '@/app/constants/ai';
33
import { useAiModelSelector } from '@/app/features/ai/lib/ai-model-selector-hook';
44
import { useAssistantChat } from '@/app/features/ai/lib/assistant-ui-chat-hook';
55
import { useBuilderStoreOutsideProviderWithSubscription } from '@/app/features/builder/builder-state-provider';
6-
import { AssistantUiChatContainer } from '@openops/components/ui';
6+
import {
7+
AssistantUiChatContainer,
8+
AssistantUiHistory,
9+
} from '@openops/components/ui';
710
import { SourceCode } from '@openops/shared';
811
import { createFrontendTools } from '@openops/ui-kit';
912
import { t } from 'i18next';
1013
import { ReactNode, useCallback, useMemo, useState } from 'react';
1114
import { useNetworkStatusWithWarning } from '../lib/hooks/use-network-status-with-warning';
1215
import { ChatMode } from '../lib/types';
16+
import { useAssistantChatHistory } from '../lib/use-ai-assistant-chat-history';
1317

1418
type AssistantUiChatProps = {
1519
onClose: () => void;
@@ -27,6 +31,7 @@ const AssistantUiChat = ({
2731
const toolComponents = useMemo(() => {
2832
return createFrontendTools();
2933
}, []);
34+
const [showHistory, setShowHistory] = useState(false);
3035

3136
const [chatId, setChatId] = useState<string | null>(
3237
sessionStorage.getItem(AI_ASSISTANT_SS_KEY),
@@ -67,9 +72,56 @@ const AssistantUiChat = ({
6772
isLoading: isModelSelectorLoading,
6873
} = useAiModelSelector({ chatId, provider, model });
6974

75+
const {
76+
chats,
77+
isLoading: isHistoryLoading,
78+
deleteChat,
79+
renameChat,
80+
refetchChatList,
81+
} = useAssistantChatHistory();
82+
7083
const { isShowingSlowWarning, connectionError } =
7184
useNetworkStatusWithWarning(chatStatus);
7285

86+
const currentChatTitle = useMemo(() => {
87+
if (chatId) {
88+
const currentChat = chats.find((chat) => chat.id === chatId);
89+
return currentChat?.displayName || title;
90+
}
91+
return title;
92+
}, [chatId, chats, title]);
93+
94+
const onChatSelected = useCallback(
95+
(id: string) => {
96+
onChatIdChange(id);
97+
setShowHistory(false);
98+
},
99+
[onChatIdChange],
100+
);
101+
102+
const onChatDeleted = useCallback(
103+
async (id: string) => {
104+
await deleteChat(id);
105+
if (chatId === id) {
106+
onChatIdChange(null);
107+
}
108+
},
109+
[chatId, deleteChat, onChatIdChange],
110+
);
111+
112+
const onChatRenamed = useCallback(
113+
async (id: string, newName: string) => {
114+
await renameChat({ chatId: id, chatName: newName });
115+
},
116+
[renameChat],
117+
);
118+
119+
const onNewChatClick = useCallback(async () => {
120+
await createNewChat();
121+
refetchChatList();
122+
setShowHistory(false);
123+
}, [createNewChat, refetchChatList]);
124+
73125
if (isLoading) {
74126
return (
75127
<div className="w-full flex h-full items-center justify-center bg-background">
@@ -81,23 +133,39 @@ const AssistantUiChat = ({
81133
}
82134

83135
return (
84-
<AssistantUiChatContainer
85-
onClose={onClose}
86-
runtime={runtime}
87-
onNewChat={createNewChat}
88-
title={title}
89-
availableModels={availableModels}
90-
onModelSelected={onModelSelected}
91-
isModelSelectorLoading={isModelSelectorLoading}
92-
selectedModel={selectedModel}
93-
theme={theme}
94-
handleInject={handleInject}
95-
toolComponents={toolComponents}
96-
isShowingSlowWarning={isShowingSlowWarning}
97-
connectionError={connectionError}
98-
>
99-
{children}
100-
</AssistantUiChatContainer>
136+
<div className="w-full h-full flex">
137+
<AssistantUiChatContainer
138+
onClose={onClose}
139+
runtime={runtime}
140+
onNewChat={onNewChatClick}
141+
title={currentChatTitle}
142+
availableModels={availableModels}
143+
onModelSelected={onModelSelected}
144+
isModelSelectorLoading={isModelSelectorLoading}
145+
selectedModel={selectedModel}
146+
theme={theme}
147+
handleInject={handleInject}
148+
toolComponents={toolComponents}
149+
onHistoryOpenChange={setShowHistory}
150+
isHistoryOpen={showHistory}
151+
chatId={chatId}
152+
isShowingSlowWarning={isShowingSlowWarning}
153+
connectionError={connectionError}
154+
>
155+
{showHistory && (
156+
<AssistantUiHistory
157+
onNewChat={onNewChatClick}
158+
newChatDisabled={isLoading || isHistoryLoading}
159+
onChatSelected={onChatSelected}
160+
onChatDeleted={onChatDeleted}
161+
onChatRenamed={onChatRenamed}
162+
chatItems={chats}
163+
selectedItemId={chatId ?? undefined}
164+
/>
165+
)}
166+
{children}
167+
</AssistantUiChatContainer>
168+
</div>
101169
);
102170
};
103171

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { api } from '@/app/lib/api';
2+
import { ListChatsResponse } from '@openops/shared';
3+
4+
export const aiAssistantChatHistoryApi = {
5+
list() {
6+
return api.get<ListChatsResponse>('/v1/ai/conversation/all-chats');
7+
},
8+
delete(chatId: string) {
9+
return api.delete<void>(`/v1/ai/conversation/${chatId}`);
10+
},
11+
generateName(chatId: string) {
12+
return api.post<{ chatName: string }>('/v1/ai/conversation/chat-name', {
13+
chatId,
14+
});
15+
},
16+
rename(chatId: string, chatName: string) {
17+
return api.patch<{ chatName: string }>(
18+
`/v1/ai/conversation/${chatId}/name`,
19+
{
20+
chatName,
21+
},
22+
);
23+
},
24+
};

packages/react-ui/src/app/features/ai/lib/assistant-ui-chat-hook.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { QueryKeys } from '@/app/constants/query-keys';
12
import { aiAssistantChatApi } from '@/app/features/ai/lib/ai-assistant-chat-api';
23
import { getActionName, getBlockName } from '@/app/features/blocks/lib/utils';
34
import { authenticationSession } from '@/app/lib/authentication-session';
@@ -7,17 +8,20 @@ import { useAISDKRuntime } from '@assistant-ui/react-ai-sdk';
78
import { toast } from '@openops/components/ui';
89
import { flowHelper } from '@openops/shared';
910
import { getFrontendToolDefinitions } from '@openops/ui-kit';
10-
import { useQuery } from '@tanstack/react-query';
11+
import { useQuery, useQueryClient } from '@tanstack/react-query';
1112
import { DefaultChatTransport, ToolSet, UIMessage } from 'ai';
1213
import { t } from 'i18next';
1314
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1415
import { aiChatApi } from '../../builder/ai-chat/lib/chat-api';
1516
import { getBuilderStore } from '../../builder/builder-state-provider';
17+
import { aiAssistantChatHistoryApi } from './ai-assistant-chat-history-api';
1618
import { aiSettingsHooks } from './ai-settings-hooks';
1719
import { buildQueryKey } from './chat-utils';
1820
import { createAdditionalContext } from './enrich-context';
1921
import { ChatMode, UseAssistantChatProps } from './types';
2022

23+
export const MIN_MESSAGES_BEFORE_NAME_GENERATION = 1;
24+
2125
export const useAssistantChat = ({
2226
chatId,
2327
onChatIdChange,
@@ -29,6 +33,8 @@ export const useAssistantChat = ({
2933
() => getFrontendToolDefinitions() as ToolSet,
3034
[],
3135
);
36+
const qc = useQueryClient();
37+
const hasAttemptedNameGenerationRef = useRef<Record<string, boolean>>({});
3238

3339
const [provider, setProvider] = useState<string | undefined>();
3440
const [model, setModel] = useState<string | undefined>();
@@ -229,6 +235,23 @@ export const useAssistantChat = ({
229235
};
230236
toast(errorToast);
231237
},
238+
onFinish: async () => {
239+
if (!chatId || hasAttemptedNameGenerationRef.current[chatId]) {
240+
return;
241+
}
242+
243+
if (messagesRef.current.length >= MIN_MESSAGES_BEFORE_NAME_GENERATION) {
244+
try {
245+
hasAttemptedNameGenerationRef.current[chatId] = true;
246+
await aiAssistantChatHistoryApi.generateName(chatId);
247+
qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] });
248+
} catch (error) {
249+
console.error('Failed to generate chat name', error);
250+
hasAttemptedNameGenerationRef.current[chatId] = false;
251+
qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] });
252+
}
253+
}
254+
},
232255
// https://github.com/assistant-ui/assistant-ui/issues/2327
233256
// handle frontend tool calls manually until this is fixed
234257
onToolCall: async ({ toolCall }: { toolCall: any }) => {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { QueryKeys } from '@/app/constants/query-keys';
2+
import { toast } from '@openops/components/ui';
3+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4+
import { t } from 'i18next';
5+
import { aiAssistantChatHistoryApi } from './ai-assistant-chat-history-api';
6+
7+
export function useAssistantChatHistory() {
8+
const qc = useQueryClient();
9+
10+
const { data, isLoading } = useQuery({
11+
queryKey: [QueryKeys.assistantHistory],
12+
queryFn: async () => {
13+
const res = await aiAssistantChatHistoryApi.list();
14+
return res.chats ?? [];
15+
},
16+
select: (chats) =>
17+
chats.map((c) => ({
18+
id: c.chatId,
19+
displayName: c.chatName || 'New chat',
20+
})),
21+
});
22+
23+
const deleteMutation = useMutation({
24+
mutationFn: (chatId: string) => aiAssistantChatHistoryApi.delete(chatId),
25+
onSuccess: () =>
26+
qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }),
27+
onError: (error) => {
28+
console.error('Failed to delete chat', error);
29+
toast({
30+
title: t('Error'),
31+
variant: 'destructive',
32+
description: t('Failed to delete chat. Please try again.'),
33+
duration: 3000,
34+
});
35+
},
36+
});
37+
38+
const renameMutation = useMutation({
39+
mutationFn: ({ chatId, chatName }: { chatId: string; chatName: string }) =>
40+
aiAssistantChatHistoryApi.rename(chatId, chatName),
41+
onSuccess: () =>
42+
qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }),
43+
onError: (error) => {
44+
console.error('Failed to rename chat', error);
45+
toast({
46+
title: t('Error'),
47+
variant: 'destructive',
48+
description: t('Failed to rename chat. Please try again.'),
49+
duration: 3000,
50+
});
51+
},
52+
});
53+
54+
return {
55+
chats: data ?? [],
56+
isLoading,
57+
deleteChat: deleteMutation.mutateAsync,
58+
renameChat: renameMutation.mutateAsync,
59+
refetchChatList: () =>
60+
qc.invalidateQueries({ queryKey: [QueryKeys.assistantHistory] }),
61+
};
62+
}

0 commit comments

Comments
 (0)