diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts index bf1a8142610..816a6820809 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChatData.ts @@ -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 => { + 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, }; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChatScrollToBottom.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChatScrollToBottom.ts index c61e4132ba4..9187305611e 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentChatScrollToBottom.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChatScrollToBottom.ts @@ -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 }; }; diff --git a/packages/twenty-front/src/modules/ai/hooks/useProcessUIToolCallMessage.ts b/packages/twenty-front/src/modules/ai/hooks/useProcessUIToolCallMessage.ts index 770202e92af..c21d4ebd749 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useProcessUIToolCallMessage.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useProcessUIToolCallMessage.ts @@ -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; diff --git a/packages/twenty-front/src/modules/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent.ts b/packages/twenty-front/src/modules/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent.ts index 703da4ec218..799b0dcf5c1 100644 --- a/packages/twenty-front/src/modules/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent.ts +++ b/packages/twenty-front/src/modules/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent.ts @@ -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'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx index 5f940ac3fd5..bdb28c9e9ca 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableEmptyHasNewRecordEffect.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useDispatchObjectRecordEventsFromSseToBrowserEvents.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useDispatchObjectRecordEventsFromSseToBrowserEvents.ts index e785d821f27..08af985b676 100644 --- a/packages/twenty-front/src/modules/sse-db-event/hooks/useDispatchObjectRecordEventsFromSseToBrowserEvents.ts +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useDispatchObjectRecordEventsFromSseToBrowserEvents.ts @@ -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 }; diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts index 7ba5f2af9ef..9e482072aea 100644 --- a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerEventStreamCreation.ts @@ -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}`), ); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts index 8d55bcf5d5c..a756f4b6807 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts @@ -123,12 +123,41 @@ export class NavigateAppTool implements Tool { }; } + const { flatObjectMetadataMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps'], + }, + ); + + const objectMetadataUniversalIdentifier = + flatObjectMetadataMaps.universalIdentifierById[ + matchingView.objectMetadataId + ]; + + const objectMetadata = isDefined(objectMetadataUniversalIdentifier) + ? flatObjectMetadataMaps.byUniversalIdentifier[ + objectMetadataUniversalIdentifier + ] + : undefined; + + if (!isDefined(objectMetadata)) { + return { + success: false, + message: `Object metadata for view "${matchingView.name}" not found`, + error: `Could not resolve the object associated with view "${matchingView.name}"`, + }; + } + return { success: true, message: `Navigating to view "${matchingView.name}"`, result: { action: 'navigateToView', + viewId: matchingView.id, viewName: matchingView.name, + objectNameSingular: objectMetadata.nameSingular, }, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const.ts index 9a8972cd308..4094cf253a5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const.ts @@ -1,4 +1,4 @@ export const AGENT_CONFIG = { - MAX_STEPS: 25, + MAX_STEPS: 300, REASONING_BUDGET_TOKENS: 12000, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts index 3e291f12b83..ccb072931bb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { type ToolSet } from 'ai'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; import { z } from 'zod'; import { formatValidationErrors } from 'src/engine/core-modules/tool-provider/utils/format-validation-errors.util'; @@ -120,6 +120,48 @@ const UpdateManyFieldMetadataInputSchema = z.object({ .describe('Array of field metadata updates to apply (1-20 items).'), }); +const CreateManyRelationFieldsInputSchema = z.object({ + relations: z + .array( + z.object({ + objectMetadataId: z + .string() + .uuid() + .describe('ID of the source object to add the relation field to'), + name: z + .string() + .describe('Internal name of the relation field (camelCase)'), + label: z.string().describe('Display label of the relation field'), + description: z + .string() + .optional() + .describe('Description of the relation field'), + icon: z + .string() + .optional() + .describe('Icon identifier for the relation field'), + type: z + .nativeEnum(RelationType) + .describe('Relation type: MANY_TO_ONE or ONE_TO_MANY'), + targetObjectMetadataId: z + .string() + .uuid() + .describe('ID of the target object this relation points to'), + targetFieldLabel: z + .string() + .describe( + 'Display label for the inverse relation field on the target object', + ), + targetFieldIcon: z + .string() + .describe('Icon for the inverse relation field (e.g. IconSomething)'), + }), + ) + .min(1) + .max(20) + .describe('Array of relation fields to create (1-20 items).'), +}); + @Injectable() export class FieldMetadataToolsFactory { constructor(private readonly fieldMetadataService: FieldMetadataService) {} @@ -316,6 +358,53 @@ export class FieldMetadataToolsFactory { }), ); + return true; + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderException) { + throw new Error(formatValidationErrors(error)); + } + throw error; + } + }, + }, + create_many_relation_fields: { + description: + 'Create multiple relation fields between objects at once. This is the recommended way to add relations after creating objects and non-relation fields. Each item specifies the source object, target object, relation type, and labels for both sides of the relation.', + inputSchema: CreateManyRelationFieldsInputSchema, + execute: async (parameters: { + relations: Array<{ + objectMetadataId: string; + name: string; + label: string; + description?: string; + icon?: string; + type: RelationType; + targetObjectMetadataId: string; + targetFieldLabel: string; + targetFieldIcon: string; + }>; + }) => { + try { + await this.fieldMetadataService.createManyFields({ + createFieldInputs: parameters.relations.map((relation) => ({ + objectMetadataId: relation.objectMetadataId, + type: FieldMetadataType.RELATION, + name: relation.name, + label: relation.label, + description: relation.description, + icon: relation.icon, + relationCreationPayload: { + type: relation.type, + targetObjectMetadataId: relation.targetObjectMetadataId, + targetFieldLabel: relation.targetFieldLabel, + targetFieldIcon: relation.targetFieldIcon, + }, + })) as Parameters< + typeof this.fieldMetadataService.createManyFields + >[0]['createFieldInputs'], + workspaceId, + }); + return true; } catch (error) { if (error instanceof WorkspaceMigrationBuilderException) { diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts index 841898d0da5..af2f2149540 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts @@ -153,38 +153,65 @@ You will create a demo workspace that fits a particular type of company given by Do not ask the user for more information, just be creative with the objects and fields, but stay professional and coherent. +The goal is to be able to tell a coherent and realistic story with the data, so for example if the user says it is a car repair shop, +we can create objects for cars, employees, repairs, customers, and the relevant relations between them, +and then we can seed data that tells the story of the car repair shop, for example we can create a customer, then create a car for that customer, then create a repair for that car, and so on. +We should end up with a dashboard that shows the relevant metrics for the car repair shop, for example the number of repairs, the revenue, the most common car brands, and so on. + Create relations fields between objects, for example a car repair shop workspace would have objects for cars, employees, repairs, customers, and the relevant relations between them. -DO NOT USE code-interpreter tool at all. Prefer more steps. +Create 5 to 7 objects, with 5 to 8 fields each, and the relevant relations between them. -LIMIT TO 3 OBJECTS FOR DEMO, AND 3 FIELDS FOR EACH OBJECT, to avoid bugs. +If you have to create multiple things you *MUST* use the relevant create many tool if it exists: +- Use *create_many_object_metadata* to create all objects at once +- Use *create_many_field_metadata* to create all non-relation fields at once +- Use *create_many_relation_fields* to create all relation fields between objects at once (do this AFTER creating the objects and non-relation fields) + +If you have to wait use the navigate tool. For the fields you will create, make sure to create a good variety of field types to showcase the different capabilities of the platform, for example: - Create SELECT and SELECT_MULTIPLE field types for building demo board index views and table with groups views - Create DATE_TIME fields to be able to create calendar views - Create CURRENCY and NUMERIC fields for graphs -Here are the steps for you to work properly : -- Proceed object by object, for each object. -- Create the object with the right tool, DO THIS FIRST -- Wait 3 seconds before navigating, for the view to be populated by the backend -- Navigate to its default view -- Then create each relevant field metadata one by one, and create a view field for each of them, reorder them to the start so we see them. -- Then seed mock data relevant : - - use the tool that is related to the object, look for tools, create_my_new_object, create_many_of_my_new_object, look again in tools, don't use http +*Here are the steps to follow closely :* + +STEP 1: Create all the objects at once with create_many_object_metadata, DO THIS FIRST +name must start with lowercase letter and contain only alphanumeric letters + +STEP 2: Wait 3 seconds, for the backend side effects to be completed + +STEP 3: Create all NON-RELATION fields for ALL objects by batch with create_many_field_metadata, do a batch for each object. +DO NOT include relation fields in this step. Only create TEXT, NUMBER, BOOLEAN, DATE_TIME, SELECT, MULTI_SELECT, CURRENCY, etc. +SELECT option values must be UPPER_SNAKE_CASE + +STEP 4: Wait 3 seconds, for the backend side effects to be completed + +STEP 5: Create all RELATION fields between objects at once with create_many_relation_fields +The name property should be camel-cased or the backend will throw, targetFieldLabel must be a string, targetFieldIcon must be a string, type must be one of the following values: MANY_TO_ONE, ONE_TO_MANY +targetFieldIcon is like IconSomething, it's ok if it doesn't exist in the icon library, it will just be a blank icon, but it needs to be a string that starts with Icon and is in PascalCase + +STEP 6: Wait 3 seconds, for the backend side effects to be completed + +STEP 7: For each object: +- Navigate the object's default view USE THE NAVIGATE TOOL +- Wait 3 seconds, so the user has time to see the object default view +- Create the view fields for the default view, use the create_many_view_fields tool, and make sure to include all created fields, including the relation fields, so that we have a complete view of the object with all its fields. +BE CAREFUL to use a position that will put those view fields right after the first label identifier field +which has a position of 0 and the next system created fields which begin at 1, *so use decimal positions between 0 and 1* +*YOU MUST CREATE ALL VIEW FIELDS FOR ALL FIELDS, INCLUDING RELATION FIELDS, IN THIS STEP, DO NOT LEAVE ANY FIELD WITHOUT A VIEW FIELD, OTHERWISE IT WILL NOT BE VISIBLE IN THE DEFAULT VIEW AND THE USER WON'T KNOW IT EXISTS* + +- Then seed relevant and realistic mock data as we said earlier : + - use the relevant tool to create many records for this object - between 20 and 50 - with a coherent combination of values - - proceed with the relevant tools for each object, do not use code-interpreter - - navigate to each default view before seeding an object, so the user can see what happens. + - use dates that are around TODAY so it's relevant for seeing past / future and present records -After you've finished with this part, let's proceed to the dashboard creation. We will create a dashboard with 4 graphs. -- Navigate to the dashboard list default view -- Create a new dashboard +Loop STEP 7 for all the objects + +STEP 8 : After you've finished with this part, let's proceed to the dashboard creation. We will create a dashboard with 4 graphs. +- Create a new dashboard with a relevant story to showcase the data you've just created, for example if it's a car repair shop, you can create a dashboard that shows the number of repairs per month, the revenue per month, the most common car brands, and the distribution of repairs by employee. - Navigate to the dashboard page -- Create 4 graphs : - - For each graph, find a relevant amount for y axis, a relevant date or select field for x axis, and if necessary a relevant group by stack - - Change the name of each graph so it is relevant - - Turn on the labels on the graphs `, isCustom: false, }, diff --git a/packages/twenty-shared/src/ai/types/NavigateAppToolOutput.ts b/packages/twenty-shared/src/ai/types/NavigateAppToolOutput.ts index 2aa329a4f33..341e7ced9c0 100644 --- a/packages/twenty-shared/src/ai/types/NavigateAppToolOutput.ts +++ b/packages/twenty-shared/src/ai/types/NavigateAppToolOutput.ts @@ -5,7 +5,9 @@ export type NavigateAppToolOutput = } | { action: 'navigateToView'; + viewId: string; viewName: string; + objectNameSingular: string; } | { action: 'navigateToRecord';