Fix AI demo workspace skill (#18575)
This PR fixes what allows to have a working demo workspace skill. - Skill updated many times into something that works - Fixed infinite loop in AI chat by memoizing ai-sdk output - Finished navigateToView implementation - Increased MAX_STEPS to 300 so the chat don't quit in the middle of a long running skill - Added CreateManyRelationFields
This commit is contained in:
@@ -24,6 +24,7 @@ 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 { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
type GetChatThreadsQuery,
|
||||
GetChatThreadsDocument,
|
||||
@@ -181,7 +182,11 @@ export const useAgentChatData = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const isNewThread = currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
||||
const isNewThread = useMemo(
|
||||
() => currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
|
||||
[currentAIChatThread],
|
||||
);
|
||||
|
||||
const { loading: messagesLoading, data } = useGetChatMessagesQuery({
|
||||
variables: { threadId: currentAIChatThread! },
|
||||
skip: !isDefined(currentAIChatThread) || isNewThread,
|
||||
@@ -191,7 +196,7 @@ export const useAgentChatData = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const ensureThreadForDraft = () => {
|
||||
const ensureThreadForDraft = useCallback(() => {
|
||||
const current = store.get(currentAIChatThreadState.atom);
|
||||
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
||||
return;
|
||||
@@ -218,9 +223,16 @@ export const useAgentChatData = () => {
|
||||
threadIdPromise.finally(() => {
|
||||
setPendingCreateFromDraftPromise(null);
|
||||
});
|
||||
};
|
||||
}, [
|
||||
createChatThread,
|
||||
setPendingCreateFromDraftPromise,
|
||||
store,
|
||||
setIsCreatingChatThread,
|
||||
]);
|
||||
|
||||
const ensureThreadIdForSend = async (): Promise<string | null> => {
|
||||
const ensureThreadIdForSend = useCallback(async (): Promise<
|
||||
string | null
|
||||
> => {
|
||||
const current = store.get(currentAIChatThreadState.atom);
|
||||
if (current !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
||||
return current;
|
||||
@@ -247,16 +259,33 @@ export const useAgentChatData = () => {
|
||||
} finally {
|
||||
setIsCreatingChatThread(false);
|
||||
}
|
||||
};
|
||||
}, [createChatThread, store, setIsCreatingChatThread]);
|
||||
|
||||
const uiMessages = mapDBMessagesToUIMessages(data?.chatMessages || []);
|
||||
const isLoading = messagesLoading || threadsLoading;
|
||||
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,
|
||||
messagesLoading,
|
||||
threadsLoading: threadsLoadingMemoized,
|
||||
messagesLoading: messagesLoadingMemoized,
|
||||
ensureThreadForDraft,
|
||||
ensureThreadIdForSend,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AI_CHAT_SCROLL_WRAPPER_ID } from '@/ai/constants/AiChatScrollWrapperId'
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const SCROLL_BOTTOM_THRESHOLD_PX = 10;
|
||||
@@ -16,9 +17,12 @@ export const useAgentChatScrollToBottom = () => {
|
||||
AI_CHAT_SCROLL_WRAPPER_ID,
|
||||
);
|
||||
|
||||
const isNearBottom = scrollWrapperScrollBottom <= SCROLL_BOTTOM_THRESHOLD_PX;
|
||||
const isNearBottom = useMemo(
|
||||
() => scrollWrapperScrollBottom <= SCROLL_BOTTOM_THRESHOLD_PX,
|
||||
[scrollWrapperScrollBottom],
|
||||
);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const { scrollWrapperElement } = getScrollWrapperElement();
|
||||
if (!isDefined(scrollWrapperElement)) {
|
||||
return;
|
||||
@@ -27,7 +31,7 @@ export const useAgentChatScrollToBottom = () => {
|
||||
scrollWrapperElement.scrollTo({
|
||||
top: scrollWrapperElement.scrollHeight,
|
||||
});
|
||||
};
|
||||
}, [getScrollWrapperElement]);
|
||||
|
||||
return { scrollToBottom, isNearBottom };
|
||||
};
|
||||
|
||||
@@ -80,9 +80,26 @@ export const useProcessUIToolCallMessage = () => {
|
||||
|
||||
break;
|
||||
}
|
||||
case 'navigateToView':
|
||||
// TODO: implement
|
||||
case 'navigateToView': {
|
||||
const viewObjectNamePlural = objectMetadataItems.find(
|
||||
(item) =>
|
||||
item.nameSingular === navigateAppOutput.objectNameSingular,
|
||||
)?.namePlural;
|
||||
|
||||
if (!isDefined(viewObjectNamePlural)) {
|
||||
throw new Error(
|
||||
`Object with singular name ${navigateAppOutput.objectNameSingular} not found, cannot navigate to view from chat.`,
|
||||
);
|
||||
}
|
||||
|
||||
navigateApp(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: viewObjectNamePlural },
|
||||
{ viewId: navigateAppOutput.viewId },
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'wait': {
|
||||
await sleep(navigateAppOutput.durationMs);
|
||||
break;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { OBJECT_RECORD_OPERATION_BROWSER_EVENT_NAME } from '@/browser-event/constants/ObjectRecordOperationBrowserEventName';
|
||||
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { type ObjectRecordOperation } from '@/object-record/types/ObjectRecordOperation';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
|
||||
+21
-13
@@ -15,7 +15,7 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/h
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
|
||||
import { useStore } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableEmptyHasNewRecordEffect = () => {
|
||||
@@ -79,20 +79,28 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
|
||||
operationSignature,
|
||||
});
|
||||
|
||||
const handleObjectRecordOperation = (
|
||||
objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail,
|
||||
) => {
|
||||
const objectRecordOperation = objectRecordOperationEventDetail.operation;
|
||||
const handleObjectRecordOperation = useCallback(
|
||||
(
|
||||
objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail,
|
||||
) => {
|
||||
const objectRecordOperation = objectRecordOperationEventDetail.operation;
|
||||
|
||||
if (
|
||||
objectRecordOperation.type.includes('update') ||
|
||||
objectRecordOperation.type.includes('create')
|
||||
) {
|
||||
if (!isRecordTableInitialLoading && !recordTableHasRecords) {
|
||||
store.set(recordTableWentFromEmptyToNotEmptyCallbackState, true);
|
||||
if (
|
||||
objectRecordOperation.type.includes('update') ||
|
||||
objectRecordOperation.type.includes('create')
|
||||
) {
|
||||
if (!isRecordTableInitialLoading && !recordTableHasRecords) {
|
||||
store.set(recordTableWentFromEmptyToNotEmptyCallbackState, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
recordTableHasRecords,
|
||||
isRecordTableInitialLoading,
|
||||
store,
|
||||
recordTableWentFromEmptyToNotEmptyCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
useListenToObjectRecordOperationBrowserEvent({
|
||||
onObjectRecordOperationBrowserEvent: handleObjectRecordOperation,
|
||||
|
||||
+6
-3
@@ -1,13 +1,14 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { groupObjectRecordSseEventsByObjectMetadataItemNameSingular } from '@/sse-db-event/utils/groupObjectRecordSseEventsByObjectMetadataItemNameSingular';
|
||||
import { turnSseObjectRecordEventsToObjectRecordOperationBrowserEvents } from '@/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ObjectRecordEventWithQueryIds } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const store = useStore();
|
||||
|
||||
const dispatchObjectRecordEventsFromSseToBrowserEvents = useCallback(
|
||||
(objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[]) => {
|
||||
@@ -24,6 +25,8 @@ export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
|
||||
objectRecordEventsByObjectMetadataItemNameSingular.keys(),
|
||||
);
|
||||
|
||||
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
|
||||
|
||||
for (const objectMetadataItemNameSingular of objectMetadataItemNamesSingular) {
|
||||
const objectRecordEventsForThisObjectMetadataItem =
|
||||
objectRecordEventsByObjectMetadataItemNameSingular.get(
|
||||
@@ -49,7 +52,7 @@ export const useDispatchObjectRecordEventsFromSseToBrowserEvents = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[objectMetadataItems],
|
||||
[store],
|
||||
);
|
||||
|
||||
return { dispatchObjectRecordEventsFromSseToBrowserEvents };
|
||||
|
||||
+2
-2
@@ -14,11 +14,11 @@ import { captureException } from '@sentry/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { print, type ExecutionResult } from 'graphql';
|
||||
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { type EventSubscription } from '~/generated-metadata/graphql';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useTriggerEventStreamCreation = () => {
|
||||
const store = useStore();
|
||||
@@ -79,7 +79,7 @@ export const useTriggerEventStreamCreation = () => {
|
||||
onEventSubscription: EventSubscription;
|
||||
}>,
|
||||
) => {
|
||||
if (isDefined(value?.errors)) {
|
||||
if (isDefined(value?.errors) && Array.isArray(value.errors)) {
|
||||
captureException(
|
||||
new Error(`SSE subscription error: ${value.errors[0]?.message}`),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user