mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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:
parent
db5b4d9c6c
commit
cb3e32df86
12 changed files with 262 additions and 54 deletions
|
|
@ -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,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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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}`),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const AGENT_CONFIG = {
|
||||
MAX_STEPS: 25,
|
||||
MAX_STEPS: 300,
|
||||
REASONING_BUDGET_TOKENS: 12000,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ export type NavigateAppToolOutput =
|
|||
}
|
||||
| {
|
||||
action: 'navigateToView';
|
||||
viewId: string;
|
||||
viewName: string;
|
||||
objectNameSingular: string;
|
||||
}
|
||||
| {
|
||||
action: 'navigateToRecord';
|
||||
|
|
|
|||
Loading…
Reference in a new issue