Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
793a8f6
enhance ai dashboard support with widget config helper and active tab…
ehconitin Feb 4, 2026
c44e974
continue
ehconitin Feb 4, 2026
e25eb46
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 5, 2026
7b25f46
improve dashboard tools error handling and add unit tests for utilities
ehconitin Feb 5, 2026
42daef3
revert ai slop
ehconitin Feb 5, 2026
13102f3
felixs review
ehconitin Feb 5, 2026
1193fcb
validate util
ehconitin Feb 5, 2026
ec24f4b
move widget validation from dashboard tools to page layout widget ser…
ehconitin Feb 5, 2026
f8a785b
refact
ehconitin Feb 5, 2026
39e1cbf
tab add tool plus some improvements
ehconitin Feb 5, 2026
dea2cfe
fix empty rich text widgets by adding markdown to blocknote conversio…
ehconitin Feb 6, 2026
22b60ee
fix?
ehconitin Feb 6, 2026
6441a27
fix
ehconitin Feb 9, 2026
467861d
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 9, 2026
edb40b6
fix
ehconitin Feb 9, 2026
ad6b9e8
swap
ehconitin Feb 9, 2026
0b5ef78
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 9, 2026
bd01c3a
simplify
ehconitin Feb 9, 2026
3093fdd
sentry
ehconitin Feb 9, 2026
2f64318
add gauge chart in type
ehconitin Feb 9, 2026
9abb09d
fix-tests
ehconitin Feb 9, 2026
401b6e1
lint
ehconitin Feb 9, 2026
ce04451
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 9, 2026
f650cbb
fix
ehconitin Feb 9, 2026
26466b8
lint
ehconitin Feb 9, 2026
e98dbab
update snapshots
ehconitin Feb 9, 2026
f60e508
fix
ehconitin Feb 9, 2026
3ab8de1
cleanup
ehconitin Feb 9, 2026
7d33080
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 9, 2026
08205c9
grep
ehconitin Feb 9, 2026
e16191b
Merge branch 'main' into dashboards-chat-agent-improvements
FelixMalfait Feb 9, 2026
9844e93
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 9, 2026
e04fa7f
Merge remote-tracking branch 'upstream/dashboards-chat-agent-improvem…
ehconitin Feb 10, 2026
f586172
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 10, 2026
b45ba3e
continue
ehconitin Feb 10, 2026
21cd565
fix
ehconitin Feb 10, 2026
27f9cf9
update tests
ehconitin Feb 10, 2026
38da652
update snapshots
ehconitin Feb 10, 2026
be7632f
Merge remote-tracking branch 'upstream/main' into dashboards-chat-age…
ehconitin Feb 10, 2026
0b8d452
Small improvements
FelixMalfait Feb 10, 2026
9bc5c89
Typecheck
FelixMalfait Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions packages/twenty-front/src/modules/ai/hooks/useBrowsingContext.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';

import { type BrowsingContext } from '@/ai/types/BrowsingContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
Expand All @@ -7,9 +11,11 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';

export const useGetBrowsingContext = () => {
const getBrowsingContext = useRecoilCallback(
Expand Down Expand Up @@ -61,11 +67,44 @@ export const useGetBrowsingContext = () => {
return null;
}

return {
const recordContext: BrowsingContext = {
type: 'recordPage',
objectNameSingular: objectMetadataItem.nameSingular,
recordId: targetedRecordsRule.selectedRecordIds[0],
};

if (
objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard
Comment thread
ehconitin marked this conversation as resolved.
Outdated
) {
const pageLayoutId = snapshot
.getLoadable(
recordStoreFamilySelector<string | null | undefined>({
recordId: targetedRecordsRule.selectedRecordIds[0],
fieldName: 'pageLayoutId',
}),
)
.getValue();

if (isDefined(pageLayoutId)) {
const tabListInstanceId =
getTabListInstanceIdFromPageLayoutId(pageLayoutId);
const activeTabId = snapshot
.getLoadable(
activeTabIdComponentState.atomFamily({
instanceId: tabListInstanceId,
}),
)
.getValue();

return {
...recordContext,
pageLayoutId,
activeTabId,
};
}
}

return recordContext;
}

if (
Expand Down
2 changes: 2 additions & 0 deletions packages/twenty-front/src/modules/ai/types/BrowsingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export type BrowsingContext =
type: 'recordPage';
objectNameSingular: string;
recordId: string;
pageLayoutId?: string;
activeTabId?: string | null;
}
| {
type: 'listView';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export type BrowsingContextType =
type: 'recordPage';
objectNameSingular: string;
recordId: string;
pageLayoutId?: string;
activeTabId?: string | null;
}
| {
type: 'listView';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type UITools,
} from 'ai';
import { AppPath } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
import { getAppPath, isDefined } from 'twenty-shared/utils';

import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';

Expand Down Expand Up @@ -237,20 +237,28 @@ export class ChatExecutionService {
workspace,
browsingContext.objectNameSingular,
browsingContext.recordId,
browsingContext.pageLayoutId,
browsingContext.activeTabId,
);
}

if (browsingContext.type === 'listView') {
return this.buildListViewContext(browsingContext);
}

this.logger.warn(
`Unhandled browsing context type: ${(browsingContext as { type: string }).type}`,
Comment thread
ehconitin marked this conversation as resolved.
Outdated
);

return '';
}

private buildRecordPageContext(
workspace: WorkspaceEntity,
objectNameSingular: string,
recordId: string,
pageLayoutId?: string,
activeTabId?: string | null,
): string {
const resourceUrl = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
Expand All @@ -260,7 +268,21 @@ export class ChatExecutionService {
}),
});

return `The user is viewing a ${objectNameSingular} record (ID: ${recordId}, URL: ${resourceUrl}). Use tools to fetch record details if needed.`;
let context = `The user is viewing a ${objectNameSingular} record (ID: ${recordId}, URL: ${resourceUrl}). Use tools to fetch record details if needed.`;

if (objectNameSingular === 'dashboard') {
Comment thread
ehconitin marked this conversation as resolved.
Outdated
context += `\nLoad \`dashboard-building\` skill before making dashboard edits.`;

if (isDefined(pageLayoutId)) {
context += `\nDashboard pageLayoutId: ${pageLayoutId}.`;
Comment thread
ehconitin marked this conversation as resolved.
Outdated
}

if (isDefined(activeTabId)) {
context += `\nActive dashboard tab ID: ${activeTabId}. Use this tab for widget additions unless the user specifies otherwise.`;
Comment thread
ehconitin marked this conversation as resolved.
Outdated
}
}

return context;
}

private buildListViewContext(browsingContext: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ Before creating any GRAPH widget, you MUST:

GRAPH widgets require real UUIDs from the workspace metadata, NOT made-up values.

You can use \`build_dashboard_widget_config\` to resolve object/field IDs and generate valid GRAPH widget configuration.
KPI/aggregate charts are \`AGGREGATE_CHART\` (still GRAPH widgets).

## Understanding User Language

Users describe charts using UI terminology. Here's how to translate:
Expand All @@ -157,14 +160,14 @@ Users describe charts using UI terminology. Here's how to translate:

**X axis section (primary grouping - the bars/categories):**
- "X axis" / "data on display" / "categories": primaryAxisGroupByFieldMetadataId
- "X axis subfield" / "Address.city": primaryAxisGroupBySubFieldName
- "X axis subfield" / "addressCity": primaryAxisGroupBySubFieldName
- "Date granularity" (on X axis): primaryAxisDateGranularity
- "Sort by" (on X axis): primaryAxisOrderBy

**Y axis section (what's being measured + optional secondary grouping):**
- "Y axis" / "data on display" / "measure" / "metric": aggregateFieldMetadataId + aggregateOperation
- "Group by" / "stacking" / "colors" / "breakdown": secondaryAxisGroupByFieldMetadataId
- "Group by subfield" / "Address.city": secondaryAxisGroupBySubFieldName
- "Group by subfield" / "addressCity": secondaryAxisGroupBySubFieldName
- "Date granularity" (on Group by): secondaryAxisGroupByDateGranularity
- "Sort by" (on Group by): secondaryAxisOrderBy
- "Cumulative" / "running total": isCumulative
Expand All @@ -176,6 +179,11 @@ Users describe charts using UI terminology. Here's how to translate:
- "Data labels" / "show values": displayDataLabel
- "Legend" / "show legend": displayLegend

**Subfield syntax examples:**
- Composite: \`address\` + \`addressCity\` → \`primaryAxisGroupBySubFieldName: "addressCity"\`
- Relation: \`company.name\` → \`primaryAxisGroupBySubFieldName: "name"\`
- Relation + composite: input \`company.address.addressCity\` → stored \`primaryAxisGroupBySubFieldName: "address.addressCity"\`

### CRITICAL: "Remove groupby" / "remove stacking" / "unstacked"
When users say this for bar/line charts, they mean remove the SECONDARY grouping (the colors/stacking).
- Set secondaryAxisGroupByFieldMetadataId to null
Expand All @@ -185,7 +193,7 @@ When users say this for bar/line charts, they mean remove the SECONDARY grouping

### Pie Chart Settings
- "Each slice represents" / "slices": groupByFieldMetadataId
- "Slice subfield" / "Address.city": groupBySubFieldName
- "Slice subfield" / "addressCity": groupBySubFieldName
- "Hide empty category": hideEmptyCategory
- "Show value in center": showValueInCenter

Expand Down Expand Up @@ -260,9 +268,11 @@ Text content widget:

1. Ask user what data they want to visualize
2. Load list_object_metadata_items to discover available objects and fields
3. Create dashboard with appropriate widgets using real field IDs
4. Use get_dashboard to verify creation and see current configuration
5. When modifying, first understand current config before making changes
3. Always use get_dashboard before modifying widgets
4. Use build_dashboard_widget_config to resolve field IDs and generate config
5. Add/update widgets with add_dashboard_widget/update_dashboard_widget (use activeTabId from context if available)
6. Use get_dashboard to verify creation and see current configuration
7. When modifying, first understand current config before making changes

## Best Practices

Expand Down
Loading