mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
## Summary This PR improves type safety across the codebase by replacing generic `any` types with proper TypeScript types, removes unnecessary record store operations, and adds TODO comments for future refactoring of useEffect hooks. ## Key Changes ### Type Safety Improvements - **SettingsAgentTurnDetail.tsx**: Replaced `any` type annotations with proper `AgentMessage` type from generated GraphQL types - **useCreateManyRecords.ts**: Added `RecordGqlNode` type for better type safety when handling mutation responses - **useLazyFindOneRecord.ts**: Replaced generic `Record<string, any>` with `Record<string, RecordGqlNode>` for improved type checking ### Removed Unnecessary Operations - **EventCardCalendarEvent.tsx**: Removed unused `useUpsertRecordsInStore` hook and its associated useEffect that was upserting calendar event records to the store - **EventCardMessage.tsx**: Removed unused `useUpsertRecordsInStore` hook and its associated useEffect that was upserting message records to the store ### Conditional Query Execution - **useLoadCurrentUser.ts**: Made the `FindAllCoreViewsDocument` query conditional - only executes when `isOnAWorkspace` is true, preventing unnecessary queries for users not on a workspace ### Documentation - Added TODO comments in multiple files (`useAgentChatData.ts`, `useWorkspaceFromInviteHash.ts`, `useGetPublicWorkspaceDataByDomain.ts`, `useFindManyRecords.ts`, `useSingleRecordPickerPerformSearch.ts`) referencing PR #18584 for future refactoring of useEffect hooks to avoid unnecessary re-renders ## Implementation Details - The removal of store upsert operations suggests these records are already being managed elsewhere or the operations were redundant - Type improvements maintain backward compatibility while providing better IDE support and compile-time checking - Conditional query execution reduces unnecessary network requests and improves performance for non-workspace users https://claude.ai/code/session_01YQErkoHotMvM6VL3JkWAqV --------- Co-authored-by: Claude <noreply@anthropic.com>
309 lines
10 KiB
TypeScript
309 lines
10 KiB
TypeScript
import { useApolloClient, useMutation, useQuery } from '@apollo/client/react';
|
|
import { getOperationName } from '~/utils/getOperationName';
|
|
import { useCallback, useEffect, useMemo } from 'react';
|
|
import { useStore } from 'jotai';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { CHAT_THREADS_PAGE_SIZE } from '@/ai/constants/ChatThreads';
|
|
import { useAgentChatScrollToBottom } from '@/ai/hooks/useAgentChatScrollToBottom';
|
|
import {
|
|
AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
|
|
agentChatDraftsByThreadIdState,
|
|
} from '@/ai/states/agentChatDraftsByThreadIdState';
|
|
import { agentChatInputState } from '@/ai/states/agentChatInputState';
|
|
import { agentChatUsageState } from '@/ai/states/agentChatUsageState';
|
|
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
|
import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState';
|
|
import { focusEditorAfterMigrateState } from '@/ai/states/focusEditorAfterMigrateState';
|
|
import { hasTriggeredCreateForDraftState } from '@/ai/states/hasTriggeredCreateForDraftState';
|
|
import { isCreatingChatThreadState } from '@/ai/states/isCreatingChatThreadState';
|
|
import { isCreatingForFirstSendState } from '@/ai/states/isCreatingForFirstSendState';
|
|
import { pendingCreateFromDraftPromiseState } from '@/ai/states/pendingCreateFromDraftPromiseState';
|
|
import { skipMessagesSkeletonUntilLoadedState } from '@/ai/states/skipMessagesSkeletonUntilLoadedState';
|
|
import { threadIdCreatedFromDraftState } from '@/ai/states/threadIdCreatedFromDraftState';
|
|
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
|
|
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
|
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
|
|
|
import {
|
|
type GetChatThreadsQuery,
|
|
GetChatThreadsDocument,
|
|
CreateChatThreadDocument,
|
|
GetChatMessagesDocument,
|
|
} from '~/generated-metadata/graphql';
|
|
|
|
export const useAgentChatData = () => {
|
|
const [currentAIChatThread, setCurrentAIChatThread] = useAtomState(
|
|
currentAIChatThreadState,
|
|
);
|
|
const setAgentChatInput = useSetAtomState(agentChatInputState);
|
|
const setAgentChatUsage = useSetAtomState(agentChatUsageState);
|
|
const setCurrentAIChatThreadTitle = useSetAtomState(
|
|
currentAIChatThreadTitleState,
|
|
);
|
|
const [, setIsCreatingChatThread] = useAtomState(isCreatingChatThreadState);
|
|
const setAgentChatDraftsByThreadId = useSetAtomState(
|
|
agentChatDraftsByThreadIdState,
|
|
);
|
|
const setPendingCreateFromDraftPromise = useSetAtomState(
|
|
pendingCreateFromDraftPromiseState,
|
|
);
|
|
const store = useStore();
|
|
const apolloClient = useApolloClient();
|
|
|
|
const { scrollToBottom } = useAgentChatScrollToBottom();
|
|
|
|
const [createChatThread] = useMutation(CreateChatThreadDocument, {
|
|
onCompleted: (data) => {
|
|
if (store.get(isCreatingForFirstSendState.atom)) {
|
|
store.set(isCreatingForFirstSendState.atom, false);
|
|
setIsCreatingChatThread(false);
|
|
return;
|
|
}
|
|
|
|
const newThreadId = data.createChatThread.id;
|
|
const previousDraftKey =
|
|
store.get(currentAIChatThreadState.atom) ??
|
|
AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
|
const draftsSnapshot = store.get(agentChatDraftsByThreadIdState.atom);
|
|
const newDraft = draftsSnapshot[AGENT_CHAT_NEW_THREAD_DRAFT_KEY] ?? '';
|
|
|
|
setIsCreatingChatThread(false);
|
|
if (previousDraftKey === AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
|
store.set(hasTriggeredCreateForDraftState.atom, true);
|
|
setAgentChatDraftsByThreadId((prev) => ({
|
|
...prev,
|
|
[newThreadId]: newDraft,
|
|
[AGENT_CHAT_NEW_THREAD_DRAFT_KEY]: '',
|
|
}));
|
|
store.set(focusEditorAfterMigrateState.atom, true);
|
|
store.set(skipMessagesSkeletonUntilLoadedState.atom, true);
|
|
store.set(threadIdCreatedFromDraftState.atom, newThreadId);
|
|
} else {
|
|
setAgentChatDraftsByThreadId((prev) => ({
|
|
...prev,
|
|
[previousDraftKey]: store.get(agentChatInputState.atom),
|
|
}));
|
|
}
|
|
setCurrentAIChatThread(newThreadId);
|
|
setAgentChatInput(newDraft);
|
|
setCurrentAIChatThreadTitle(null);
|
|
setAgentChatUsage(null);
|
|
|
|
const newThread = data.createChatThread;
|
|
const threadListVariables = {
|
|
paging: { first: CHAT_THREADS_PAGE_SIZE },
|
|
};
|
|
const existing = apolloClient.cache.readQuery<GetChatThreadsQuery>({
|
|
query: GetChatThreadsDocument,
|
|
variables: threadListVariables,
|
|
});
|
|
if (isDefined(existing) && isDefined(existing.chatThreads)) {
|
|
const newNode = {
|
|
__typename: 'AgentChatThread' as const,
|
|
...newThread,
|
|
totalInputTokens: 0,
|
|
totalOutputTokens: 0,
|
|
contextWindowTokens: null,
|
|
conversationSize: 0,
|
|
totalInputCredits: 0,
|
|
totalOutputCredits: 0,
|
|
};
|
|
const newEdge = {
|
|
__typename: 'AgentChatThreadEdge' as const,
|
|
node: newNode,
|
|
cursor: newThread.id,
|
|
};
|
|
apolloClient.cache.writeQuery({
|
|
query: GetChatThreadsDocument,
|
|
variables: threadListVariables,
|
|
data: {
|
|
chatThreads: {
|
|
...existing.chatThreads,
|
|
edges: [newEdge, ...existing.chatThreads.edges],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
},
|
|
onError: () => {
|
|
setIsCreatingChatThread(false);
|
|
store.set(isCreatingForFirstSendState.atom, false);
|
|
store.set(hasTriggeredCreateForDraftState.atom, false);
|
|
},
|
|
refetchQueries: [
|
|
getOperationName(GetChatThreadsDocument) ?? 'GetChatThreads',
|
|
],
|
|
});
|
|
|
|
const { loading: threadsLoading, data: threadsData } = useQuery(
|
|
GetChatThreadsDocument,
|
|
{
|
|
variables: { paging: { first: CHAT_THREADS_PAGE_SIZE } },
|
|
skip: isDefined(currentAIChatThread),
|
|
},
|
|
);
|
|
|
|
// TODO: Refactor this useEffect to avoid unnecessary re-renders (see PR #18584 review)
|
|
useEffect(() => {
|
|
if (!threadsData) return;
|
|
|
|
const threads = threadsData.chatThreads.edges.map((edge) => edge.node);
|
|
|
|
if (threads.length > 0) {
|
|
const firstThread = threads[0];
|
|
const newDraft =
|
|
store.get(agentChatDraftsByThreadIdState.atom)[firstThread.id] ?? '';
|
|
|
|
setCurrentAIChatThread(firstThread.id);
|
|
setAgentChatInput(newDraft);
|
|
setCurrentAIChatThreadTitle(firstThread.title ?? null);
|
|
|
|
const hasUsageData =
|
|
(firstThread.conversationSize ?? 0) > 0 &&
|
|
isDefined(firstThread.contextWindowTokens);
|
|
setAgentChatUsage(
|
|
hasUsageData
|
|
? {
|
|
lastMessage: null,
|
|
conversationSize: firstThread.conversationSize ?? 0,
|
|
contextWindowTokens: firstThread.contextWindowTokens ?? 0,
|
|
inputTokens: firstThread.totalInputTokens,
|
|
outputTokens: firstThread.totalOutputTokens,
|
|
inputCredits: firstThread.totalInputCredits,
|
|
outputCredits: firstThread.totalOutputCredits,
|
|
}
|
|
: null,
|
|
);
|
|
} else {
|
|
store.set(hasTriggeredCreateForDraftState.atom, false);
|
|
setCurrentAIChatThread(AGENT_CHAT_NEW_THREAD_DRAFT_KEY);
|
|
setAgentChatInput(
|
|
store.get(agentChatDraftsByThreadIdState.atom)[
|
|
AGENT_CHAT_NEW_THREAD_DRAFT_KEY
|
|
] ?? '',
|
|
);
|
|
setCurrentAIChatThreadTitle(null);
|
|
setAgentChatUsage(null);
|
|
}
|
|
}, [
|
|
threadsData,
|
|
store,
|
|
setCurrentAIChatThread,
|
|
setAgentChatInput,
|
|
setCurrentAIChatThreadTitle,
|
|
setAgentChatUsage,
|
|
]);
|
|
|
|
const isNewThread = useMemo(
|
|
() => currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
|
|
[currentAIChatThread],
|
|
);
|
|
|
|
const { loading: messagesLoading, data } = useQuery(GetChatMessagesDocument, {
|
|
variables: { threadId: currentAIChatThread! },
|
|
skip: !isDefined(currentAIChatThread) || isNewThread,
|
|
});
|
|
|
|
// TODO: Refactor this useEffect to avoid unnecessary re-renders (see PR #18584 review)
|
|
useEffect(() => {
|
|
if (data) {
|
|
store.set(skipMessagesSkeletonUntilLoadedState.atom, false);
|
|
scrollToBottom();
|
|
}
|
|
}, [data, store, scrollToBottom]);
|
|
|
|
const ensureThreadForDraft = useCallback(() => {
|
|
const current = store.get(currentAIChatThreadState.atom);
|
|
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
|
return;
|
|
}
|
|
const draft =
|
|
store.get(agentChatDraftsByThreadIdState.atom)[
|
|
AGENT_CHAT_NEW_THREAD_DRAFT_KEY
|
|
] ?? '';
|
|
if (draft.trim() === '') {
|
|
return;
|
|
}
|
|
if (store.get(hasTriggeredCreateForDraftState.atom)) {
|
|
return;
|
|
}
|
|
if (store.get(isCreatingChatThreadState.atom)) {
|
|
return;
|
|
}
|
|
setIsCreatingChatThread(true);
|
|
const createPromise = createChatThread();
|
|
const threadIdPromise = createPromise.then(
|
|
(result) => result?.data?.createChatThread?.id ?? null,
|
|
);
|
|
setPendingCreateFromDraftPromise(threadIdPromise);
|
|
threadIdPromise.finally(() => {
|
|
setPendingCreateFromDraftPromise(null);
|
|
});
|
|
}, [
|
|
createChatThread,
|
|
setPendingCreateFromDraftPromise,
|
|
store,
|
|
setIsCreatingChatThread,
|
|
]);
|
|
|
|
const ensureThreadIdForSend = useCallback(async (): Promise<
|
|
string | null
|
|
> => {
|
|
const current = store.get(currentAIChatThreadState.atom);
|
|
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
|
return current;
|
|
}
|
|
const inFlightCreate = store.get(pendingCreateFromDraftPromiseState.atom);
|
|
if (
|
|
store.get(isCreatingChatThreadState.atom) &&
|
|
isDefined(inFlightCreate)
|
|
) {
|
|
try {
|
|
const threadId = await inFlightCreate;
|
|
return threadId;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
store.set(isCreatingForFirstSendState.atom, true);
|
|
setIsCreatingChatThread(true);
|
|
try {
|
|
const result = await createChatThread();
|
|
return result?.data?.createChatThread?.id ?? null;
|
|
} catch {
|
|
return null;
|
|
} finally {
|
|
setIsCreatingChatThread(false);
|
|
}
|
|
}, [createChatThread, store, setIsCreatingChatThread]);
|
|
|
|
const threadsLoadingMemoized = useMemo(
|
|
() => threadsLoading,
|
|
[threadsLoading],
|
|
);
|
|
|
|
const messagesLoadingMemoized = useMemo(
|
|
() => messagesLoading,
|
|
[messagesLoading],
|
|
);
|
|
|
|
const uiMessages = useMemo(
|
|
() => mapDBMessagesToUIMessages(data?.chatMessages || []),
|
|
[data?.chatMessages],
|
|
);
|
|
|
|
const isLoading = useMemo(
|
|
() => messagesLoadingMemoized || threadsLoadingMemoized,
|
|
[messagesLoadingMemoized, threadsLoadingMemoized],
|
|
);
|
|
|
|
return {
|
|
uiMessages,
|
|
isLoading,
|
|
threadsLoading: threadsLoadingMemoized,
|
|
messagesLoading: messagesLoadingMemoized,
|
|
ensureThreadForDraft,
|
|
ensureThreadIdForSend,
|
|
};
|
|
};
|