diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx index 5f3d41879e..4c2e78e6d4 100644 --- a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx @@ -131,6 +131,14 @@ export function barChartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Bar Chart data", + fieldName: "data", + fieldDescription: + "JSON array of objects representing bar chart data. Each object should have keys for the x-axis category and numeric values for each series.", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartPropertyView.tsx index 44eda032b3..b5a003a900 100644 --- a/client/packages/lowcoder-comps/src/comps/basicChartComp/chartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/basicChartComp/chartPropertyView.tsx @@ -152,6 +152,14 @@ export function chartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Chart data", + fieldName: "data", + fieldDescription: + "JSON array of objects representing chart data. Each object should include keys for categories and numeric values for each series.", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartPropertyView.tsx index b6694e9103..33f1fe78a5 100644 --- a/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/boxplotChartComp/boxplotChartPropertyView.tsx @@ -11,7 +11,6 @@ import { controlItem, } from "lowcoder-sdk"; import { trans } from "i18n/comps"; - export function boxplotChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -76,6 +75,14 @@ export function boxplotChartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Box plot chart data", + fieldName: "data", + fieldDescription: + "JSON array of objects for the box plot. Include keys for category (x) and numeric columns used for box plot statistics.", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/candleStickChartComp/candleStickChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/candleStickChartComp/candleStickChartPropertyView.tsx index 058a35d8d4..cfa91b27f8 100644 --- a/client/packages/lowcoder-comps/src/comps/candleStickChartComp/candleStickChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/candleStickChartComp/candleStickChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function candleStickChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,9 +15,30 @@ export function candleStickChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "CandleStick chart data", + fieldName: "echartsData", + fieldDescription: + "JSON array of OHLC values. Each item is [open, close, low, high] for one period, in the same order as x-axis labels.", + }, + })} {children.echartsTitleConfig.getPropertyView()} - {children.echartsTitleData.propertyView({ label: trans("chart.xAxisLabels"), tooltip: trans("chart.xAxisLabelsTooltip") })} + {children.echartsTitleData.propertyView({ + label: trans("chart.xAxisLabels"), + tooltip: trans("chart.xAxisLabelsTooltip"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "CandleStick x-axis labels", + fieldName: "echartsTitleData", + fieldDescription: + "JSON array of x-axis labels (e.g. dates). Length must match the number of OHLC rows in Data.", + }, + })} {children.echartsTitleVerticalConfig.getPropertyView()} {children.echartsTitle.propertyView({ label: trans("candleStickChart.title"), tooltip: trans("echarts.titleTooltip") })} {children.left.propertyView({ label: trans("candleStickChart.left"), tooltip: trans("echarts.leftTooltip") })} @@ -52,6 +72,14 @@ export function candleStickChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "CandleStick option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/chartComp/chartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/chartComp/chartPropertyView.tsx index 754bab376d..fe6722c5a7 100644 --- a/client/packages/lowcoder-comps/src/comps/chartComp/chartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/chartComp/chartPropertyView.tsx @@ -163,6 +163,15 @@ export function chartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "ECharts option JSON", + fieldName: "echartsOption", + fieldDescription: + "Apache ECharts option JSON for the selected chart component.", + }, tooltip: (
@@ -211,6 +220,15 @@ export function chartPropertyView( {children.mapOptions.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "ECharts map option JSON", + fieldName: "mapOptions", + fieldDescription: + "Apache ECharts map option JSON for the selected chart component.", + }, tooltip: (
@@ -257,4 +275,4 @@ export function chartPropertyView( {getChatConfigByMode(children.mode.getView())} ); -} \ No newline at end of file +} diff --git a/client/packages/lowcoder-comps/src/comps/funnelChartComp/funnelChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/funnelChartComp/funnelChartPropertyView.tsx index b3b0f61aaa..0f6cad18c4 100644 --- a/client/packages/lowcoder-comps/src/comps/funnelChartComp/funnelChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/funnelChartComp/funnelChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function funnelChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,7 +15,17 @@ export function funnelChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Funnel chart data", + fieldName: "echartsData", + fieldDescription: + "JSON array of funnel stages. Each item should have name and value (and optional color).", + }, + })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} @@ -65,6 +74,14 @@ export function funnelChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Funnel option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/gaugeChartComp/gaugeChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/gaugeChartComp/gaugeChartPropertyView.tsx index cab358d440..1e25310e03 100644 --- a/client/packages/lowcoder-comps/src/comps/gaugeChartComp/gaugeChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/gaugeChartComp/gaugeChartPropertyView.tsx @@ -64,6 +64,13 @@ export function gaugeChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Gauge option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this gauge chart component.", + }, tooltip: (
@@ -131,6 +138,13 @@ export function gaugeChartPropertyView( {children.stageGaugeOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Stage gauge option", + fieldName: "stageGaugeOption", + fieldDescription: "Apache ECharts option JSON for this stage gauge chart component.", + }, tooltip: (
@@ -198,6 +212,13 @@ export function gaugeChartPropertyView( {children.gradeGaugeOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Grade gauge option", + fieldName: "gradeGaugeOption", + fieldDescription: "Apache ECharts option JSON for this grade gauge chart component.", + }, tooltip: (
@@ -265,6 +286,13 @@ export function gaugeChartPropertyView( {children.temperatureGaugeOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Temperature gauge option", + fieldName: "temperatureGaugeOption", + fieldDescription: "Apache ECharts option JSON for this temperature gauge chart component.", + }, tooltip: (
@@ -331,6 +359,13 @@ export function gaugeChartPropertyView( {children.multiTitleGaugeOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Multi-title gauge option", + fieldName: "multiTitleGaugeOption", + fieldDescription: "Apache ECharts option JSON for this multi-title gauge chart component.", + }, tooltip: (
@@ -386,6 +421,13 @@ export function gaugeChartPropertyView( {children.ringGaugeOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Ring gauge option", + fieldName: "ringGaugeOption", + fieldDescription: "Apache ECharts option JSON for this ring gauge chart component.", + }, tooltip: (
@@ -445,6 +487,13 @@ export function gaugeChartPropertyView( {children.barometerGaugeOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Barometer gauge option", + fieldName: "barometerGaugeOption", + fieldDescription: "Apache ECharts option JSON for this barometer gauge chart component.", + }, tooltip: (
@@ -499,6 +548,13 @@ export function gaugeChartPropertyView( {children.clockGaugeOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Clock gauge option", + fieldName: "clockGaugeOption", + fieldDescription: "Apache ECharts option JSON for this clock gauge chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/graphChartComp/graphChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/graphChartComp/graphChartPropertyView.tsx index 51ab7e517b..f7159560f9 100644 --- a/client/packages/lowcoder-comps/src/comps/graphChartComp/graphChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/graphChartComp/graphChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function graphChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,9 +15,36 @@ export function graphChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsCategories.propertyView({ label: trans("graphChart.categories") })} - {children.echartsLinks.propertyView({ label: trans("graphChart.links") })} - {children.echartsNodes.propertyView({ label: trans("graphChart.nodes") })} + {children.echartsCategories.propertyView({ + label: trans("graphChart.categories"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Graph categories", + fieldName: "echartsCategories", + fieldDescription: "JSON array of category names for grouping graph nodes.", + }, + })} + {children.echartsLinks.propertyView({ + label: trans("graphChart.links"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Graph links", + fieldName: "echartsLinks", + fieldDescription: "JSON array of edges: source, target, and optional value.", + }, + })} + {children.echartsNodes.propertyView({ + label: trans("graphChart.nodes"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Graph nodes", + fieldName: "echartsNodes", + fieldDescription: "JSON array of nodes with name, value, and optional category index.", + }, + })} {children.echartsTitle.propertyView({ label: trans("graphChart.title"), tooltip: trans("echarts.titleTooltip") })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} @@ -48,6 +74,14 @@ export function graphChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Graph option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/heatmapChartComp/heatmapChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/heatmapChartComp/heatmapChartPropertyView.tsx index aefed93a75..ac16783c43 100644 --- a/client/packages/lowcoder-comps/src/comps/heatmapChartComp/heatmapChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/heatmapChartComp/heatmapChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function heatmapChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,9 +15,37 @@ export function heatmapChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} - {children.echartsDataX.propertyView({ label: trans("heatmapChart.xAxisData") })} - {children.echartsDataY.propertyView({ label: trans("heatmapChart.yAxisData") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Heatmap values", + fieldName: "echartsData", + fieldDescription: + "JSON array of heatmap cells as [xIndex, yIndex, value] (indices match x/y axis label arrays).", + }, + })} + {children.echartsDataX.propertyView({ + label: trans("heatmapChart.xAxisData"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Heatmap x-axis labels", + fieldName: "echartsDataX", + fieldDescription: "JSON array of x-axis category labels.", + }, + })} + {children.echartsDataY.propertyView({ + label: trans("heatmapChart.yAxisData"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Heatmap y-axis labels", + fieldName: "echartsDataY", + fieldDescription: "JSON array of y-axis category labels.", + }, + })} {children.echartsColor.propertyView({ label: trans("heatmapChart.color") })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} @@ -64,6 +91,14 @@ export function heatmapChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Heatmap option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartPropertyView.tsx index bbcebf3586..e8f5ce15c4 100644 --- a/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/line3dChartComp/line3dChartPropertyView.tsx @@ -11,7 +11,6 @@ import { controlItem, } from "lowcoder-sdk"; import { trans } from "i18n/comps"; - export function line3dChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -43,6 +42,14 @@ export function line3dChartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "3D line chart data", + fieldName: "data", + fieldDescription: + "JSON array of coordinate points for the 3D line (lng/lat/alt or x/y/z style fields per row).", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartPropertyView.tsx index 5a67d8ecfd..ab8b8a98c3 100644 --- a/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/lineChartComp/lineChartPropertyView.tsx @@ -12,7 +12,6 @@ import { controlItem, } from "lowcoder-sdk"; import { trans } from "i18n/comps"; - export function lineChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -168,6 +167,14 @@ export function lineChartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Line chart data", + fieldName: "data", + fieldDescription: + "JSON array of objects for the Line chart. Each row is one category; include keys for the x-axis and numeric values for each series.", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/mermaidComp/index.tsx b/client/packages/lowcoder-comps/src/comps/mermaidComp/index.tsx index ea143725c4..5cbca382d9 100644 --- a/client/packages/lowcoder-comps/src/comps/mermaidComp/index.tsx +++ b/client/packages/lowcoder-comps/src/comps/mermaidComp/index.tsx @@ -119,7 +119,21 @@ const CompBase = new UICompBuilder(childrenMap, (props: any) => { .setPropertyViewFn((children: any) => { return ( <> -
{children.code.propertyView({ label: "code" })}
+
+ {children.code.propertyView({ + label: "code", + styleName: "medium", + enableAIHelp: true, + aiHelp: { + targetKind: "component-field", + label: "Mermaid diagram", + fieldName: "code", + fieldDescription: + "Mermaid diagram definition (flowchart, sequence, class, gantt, ER, journey, etc.). Generate valid Mermaid syntax, not JSON or ECharts.", + targetId: "mermaid.code", + }, + })} +
{children.onEvent.propertyView()}
); diff --git a/client/packages/lowcoder-comps/src/comps/parallelChartComp/parallelChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/parallelChartComp/parallelChartPropertyView.tsx index 3106c44d78..7e00d14121 100644 --- a/client/packages/lowcoder-comps/src/comps/parallelChartComp/parallelChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/parallelChartComp/parallelChartPropertyView.tsx @@ -11,7 +11,6 @@ import { controlItem, } from "lowcoder-sdk"; import { trans } from "i18n/comps"; - export function parallelChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -51,6 +50,14 @@ export function parallelChartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Parallel coordinates chart data", + fieldName: "data", + fieldDescription: + "JSON array of objects; each object is one line across parallel axes (one numeric field per dimension).", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartPropertyView.tsx index 626a491c03..2f0363020f 100644 --- a/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/pieChartComp/pieChartPropertyView.tsx @@ -125,6 +125,14 @@ export function pieChartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Pie Chart data", + fieldName: "data", + fieldDescription: + "JSON array of objects representing pie chart data. Each object should have a name/category key and a numeric value key for each slice.", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/radarChartComp/radarChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/radarChartComp/radarChartPropertyView.tsx index b59b6f3dfd..6f1f8dfe83 100644 --- a/client/packages/lowcoder-comps/src/comps/radarChartComp/radarChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/radarChartComp/radarChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function radarChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,8 +15,28 @@ export function radarChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} - {children.echartsIndicators.propertyView({ label: trans("radarChart.indicators") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Radar chart series data", + fieldName: "echartsData", + fieldDescription: + "JSON array of radar series. Each item has name and value (array of numbers matching indicator count).", + }, + })} + {children.echartsIndicators.propertyView({ + label: trans("radarChart.indicators"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Radar indicators", + fieldName: "echartsIndicators", + fieldDescription: + "JSON array of radar axes. Each item has name and max (e.g. { name: \"Speed\", max: 100 }).", + }, + })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} {children.legendVisibility.getView() && children.echartsLegendAlignConfig.getPropertyView()} @@ -60,6 +79,14 @@ export function radarChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Radar option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/sankeyChartComp/sankeyChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/sankeyChartComp/sankeyChartPropertyView.tsx index 3962175314..af820bc656 100644 --- a/client/packages/lowcoder-comps/src/comps/sankeyChartComp/sankeyChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/sankeyChartComp/sankeyChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function sankeyChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,8 +15,27 @@ export function sankeyChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} - {children.echartsLinks.propertyView({ label: trans("chart.links") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Sankey nodes", + fieldName: "echartsData", + fieldDescription: "JSON array of Sankey nodes (each with at least a name).", + }, + })} + {children.echartsLinks.propertyView({ + label: trans("chart.links"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Sankey links", + fieldName: "echartsLinks", + fieldDescription: + "JSON array of links between nodes: source, target, and value (names must match nodes).", + }, + })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} @@ -64,6 +82,14 @@ export function sankeyChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Sankey option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartPropertyView.tsx index 77ac49eb0c..6ad26f52c9 100644 --- a/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/scatterChartComp/scatterChartPropertyView.tsx @@ -12,7 +12,6 @@ import { controlItem, } from "lowcoder-sdk"; import { trans } from "i18n/comps"; - export function scatterChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -121,6 +120,14 @@ export function scatterChartPropertyView(
{children.data.propertyView({ label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Scatter chart data", + fieldName: "data", + fieldDescription: + "JSON array of objects for the Scatter chart. Each row is one category; include keys for the x-axis and numeric values for each series.", + }, })}
diff --git a/client/packages/lowcoder-comps/src/comps/sunburstChartComp/sunburstChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/sunburstChartComp/sunburstChartPropertyView.tsx index 22564c1893..6ebdeaf516 100644 --- a/client/packages/lowcoder-comps/src/comps/sunburstChartComp/sunburstChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/sunburstChartComp/sunburstChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function sunburstChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,8 +15,27 @@ export function sunburstChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} - {children.echartsLevels.propertyView({ label: trans("sunburstChart.levels") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Sunburst data", + fieldName: "echartsData", + fieldDescription: + "JSON sunburst hierarchy (nested children with name and value), not full ECharts option JSON.", + }, + })} + {children.echartsLevels.propertyView({ + label: trans("sunburstChart.levels"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Sunburst levels", + fieldName: "echartsLevels", + fieldDescription: "JSON array of level style configs (radius, label, etc.) per ring.", + }, + })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} @@ -53,6 +71,14 @@ export function sunburstChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Sunburst option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/themeriverChartComp/themeriverChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/themeriverChartComp/themeriverChartPropertyView.tsx index f0e5b99864..0533af43c2 100644 --- a/client/packages/lowcoder-comps/src/comps/themeriverChartComp/themeriverChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/themeriverChartComp/themeriverChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function themeriverChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,7 +15,17 @@ export function themeriverChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Theme river data", + fieldName: "echartsData", + fieldDescription: + "JSON array of theme river rows: [date/time, value, seriesName] per ECharts themeRiver format.", + }, + })} {children.echartsColors.propertyView({ label: trans("themeriverChart.colors") })} {children.echartsTitleConfig.getPropertyView()} @@ -60,6 +69,14 @@ export function themeriverChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Theme river option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/treeChartComp/treeChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/treeChartComp/treeChartPropertyView.tsx index f4cc5ca43d..54b5eaff7e 100644 --- a/client/packages/lowcoder-comps/src/comps/treeChartComp/treeChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/treeChartComp/treeChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function treeChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,7 +15,17 @@ export function treeChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Tree chart data", + fieldName: "echartsData", + fieldDescription: + "JSON tree hierarchy (nested children with name and optional value), not a full ECharts option.", + }, + })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} {children.echartsTitle.propertyView({ label: trans("treeChart.title") })} @@ -54,6 +63,14 @@ export function treeChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Tree option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder-comps/src/comps/treemapChartComp/treemapChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/treemapChartComp/treemapChartPropertyView.tsx index 2632f4349f..e1c4797a1d 100644 --- a/client/packages/lowcoder-comps/src/comps/treemapChartComp/treemapChartPropertyView.tsx +++ b/client/packages/lowcoder-comps/src/comps/treemapChartComp/treemapChartPropertyView.tsx @@ -7,7 +7,6 @@ import { } from "lowcoder-sdk"; import { trans } from "i18n/comps"; import { examplesUrl,optionUrl } from "../chartComp/chartConfigs/chartUrls"; - export function treeChartPropertyView( children: ChartCompChildrenType, dispatch: (action: CompAction) => void @@ -16,7 +15,17 @@ export function treeChartPropertyView( const jsonModePropertyView = ( <>
- {children.echartsData.propertyView({ label: trans("chart.data") })} + {children.echartsData.propertyView({ + label: trans("chart.data"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Treemap data", + fieldName: "echartsData", + fieldDescription: + "JSON treemap hierarchy (nested children with name and value), not full ECharts option JSON.", + }, + })} {children.echartsTitleConfig.getPropertyView()} {children.echartsTitleVerticalConfig.getPropertyView()} @@ -48,6 +57,14 @@ export function treeChartPropertyView( {children.echartsOption.propertyView({ label: trans("chart.echartsOptionLabel"), styleName: "higher", + language: "json", + enableAIHelp: true, + aiHelp: { + targetKind: "echarts-option", + label: "Treemap option", + fieldName: "echartsOption", + fieldDescription: "Apache ECharts option JSON for this chart component.", + }, tooltip: (
diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index 7c2899bfcc..542327bee7 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -8,10 +8,8 @@ "dependencies": { "@ai-sdk/openai": "^1.3.22", "@ant-design/icons": "^5.3.0", - "@assistant-ui/react": "^0.10.24", - "@assistant-ui/react-ai-sdk": "^0.10.14", - "@assistant-ui/react-markdown": "^0.10.5", - "@assistant-ui/styles": "^0.1.13", + "@assistant-ui/react": "^0.14.5", + "@assistant-ui/react-markdown": "^0.14.0", "@bany/curl-to-json": "^1.2.8", "@codemirror/autocomplete": "^6.11.1", "@codemirror/commands": "^6.3.2", diff --git a/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx b/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx index 02630eefef..44fedbc32a 100644 --- a/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx +++ b/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx @@ -15,6 +15,7 @@ import type { CodeEditorProps, StyleName } from "./codeEditorTypes"; import { useClickCompNameEffect } from "./clickCompName"; import { Layers } from "../../constants/Layers"; import { debounce } from "lodash"; +import { CodeEditorAIHelpButton } from "components/ai-helper"; type StyleConfig = { minHeight: string; @@ -214,6 +215,7 @@ function useCodeMirror( ) { const { value, onChange } = props; const viewRef = useRef(); + const [viewVersion, setViewVersion] = useState(0); // will not trigger view.setState when typing inputs, to avoid focus chaos const isTypingRef = useRef(0); @@ -250,6 +252,7 @@ function useCodeMirror( view.setState(state); } else { viewRef.current = new EditorView({ state, parent: container.current }); + setViewVersion((version) => version + 1); } } }, [container, value, extensions]); @@ -262,7 +265,7 @@ function useCodeMirror( }; }, []); - return { view: viewRef.current, isFocus }; + return { view: viewRef.current, isFocus, viewVersion }; } function clickCompNameCss(enableClickCompName?: boolean) { @@ -338,6 +341,20 @@ const CodeEditorPanelContainer = styled.div<{ const CodeEditorWrapper = styled.div` height: 100%; + position: relative; + + .code-editor-ai-help-button { + opacity: 0; + pointer-events: none; + transition: opacity 120ms ease; + } + + &:hover { + .code-editor-ai-help-button { + opacity: 1; + pointer-events: auto; + } + } `; function canShowCard(props: CodeEditorProps) { @@ -358,6 +375,21 @@ function CodeEditorCommon( view && onClick(e, view) : undefined}> {!disabled && view && props.widgetPopup?.(view)} {children} + {!disabled && props.enableAIHelp && view && ( + + )} ReactNode; cardTips?: ReactNode; enableMetaCompletion?: boolean; + enableAIHelp?: boolean; + aiHelp?: CodeEditorAIHelp; } export interface CodeEditorProps extends CodeEditorControlParams { diff --git a/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx b/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx new file mode 100644 index 0000000000..64bbee42f9 --- /dev/null +++ b/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx @@ -0,0 +1,225 @@ +import { useContext, useEffect, useRef } from "react"; +import Button from "antd/es/button"; +import Empty from "antd/es/empty"; +import Select from "antd/es/select"; +import { AssistantModalPrimitive } from "@assistant-ui/react"; +import { SparklesIcon, XIcon } from "lucide-react"; +import { useSelector } from "react-redux"; +import styled from "styled-components"; + +import { EditorContext } from "comps/editorState"; +import { getDataSourceStructures } from "redux/selectors/datasourceSelectors"; +import { getSelectedAIQueryName } from "util/localStorageUtil"; + +import { AIHelperRuntime } from "./AIHelperRuntime"; +import { useAIHelper } from "./context/AIHelperController"; + +const Anchor = styled.div` + position: fixed; + right: 16px; + bottom: 16px; + width: 1px; + height: 1px; +`; + +const Content = styled(AssistantModalPrimitive.Content)` + width: 430px; + height: min(640px, calc(100vh - 128px)); + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid #e1e3eb; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 12px 40px rgba(15, 23, 42, 0.18); + z-index: 2147483000; + + .aui-thread-root { + min-height: 0; + flex: 1 1 auto; + background: #fafbfc; + } + + .aui-thread-viewport { + padding: 12px 12px 0; + } + + .aui-thread-welcome-root { + padding: 16px 8px; + } + + .aui-thread-welcome-suggestions { + display: none; + } +`; + +const Header = styled.div` + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-bottom: 1px solid #e1e3eb; +`; + +const Title = styled.div` + min-width: 0; +`; + +const TitleLine = styled.div` + display: flex; + align-items: center; + gap: 8px; + color: #111827; + font-size: 13px; + font-weight: 600; +`; + +const TargetLabel = styled.div` + max-width: 300px; + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #6b7280; + font-size: 11px; +`; + +const IconButton = styled.button` + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 6px; + background: transparent; + color: #6b7280; + cursor: pointer; + + &:hover { + background: #f3f4f6; + color: #111827; + } +`; + +const QueryBar = styled.div` + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid #f1f5f9; + background: #fcfcfd; + color: #6b7280; + font-size: 12px; +`; + +const EmptyState = styled.div` + flex: 1 1 auto; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +`; + +export function AIHelperModal() { + const helper = useAIHelper(); + const editorState = useContext(EditorContext); + const datasourceStructures = useSelector(getDataSourceStructures); + const datasourceStructuresRef = useRef(datasourceStructures); + + useEffect(() => { + datasourceStructuresRef.current = datasourceStructures; + }, [datasourceStructures]); + + useEffect(() => { + if (!helper?.open) { + return; + } + const selectedQueryName = getSelectedAIQueryName(); + if (selectedQueryName !== helper.helperQueryName) { + helper.setHelperQueryName(selectedQueryName); + } + }, [helper?.open]); + + if (!helper) return null; + + const queryOptions = (() => { + if (!editorState) return []; + try { + return editorState.getQueriesComp().getView().map((query: any) => { + const name = query.children.name.getView(); + const type = query.children.compType.getView(); + return { + label: type ? `${name} (${type})` : name, + value: name, + }; + }); + } catch { + return []; + } + })(); + + const target = helper.target; + + return ( + + + + + +
+ + <TitleLine> + <SparklesIcon size={16} color="#4965f2" /> + <span>AI Helper</span> + </TitleLine> + {target?.label && ( + <TargetLabel title={target.label}>{target.label}</TargetLabel> + )} + + + + +
+ + + AI query: + handleRename(e.target.value)} - onPressEnter={(e) => handleRename((e.target as HTMLInputElement).value)} - onKeyDown={(e) => { - if (e.key === 'Escape') onFinish(); - }} - autoFocus - style={{ fontSize: '14px', padding: '2px 8px' }} - /> - ); -}; - - -const ThreadListItemRename: FC<{ onStartEdit: () => void; editing: boolean }> = ({ - onStartEdit, - editing -}) => { - if (editing) return null; - - return ( - - - - ); -}; - diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/thread.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/thread.tsx deleted file mode 100644 index a45e5fe147..0000000000 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/thread.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import { - ActionBarPrimitive, - BranchPickerPrimitive, - ComposerPrimitive, - MessagePrimitive, - ThreadPrimitive, - } from "@assistant-ui/react"; - import { useMemo, type FC } from "react"; - import { trans } from "i18n"; - import { - ArrowDownIcon, - CheckIcon, - ChevronLeftIcon, - ChevronRightIcon, - CopyIcon, - PencilIcon, - SendHorizontalIcon, - } from "lucide-react"; - import { cn } from "../../utils/cn"; - - import { Button } from "../ui/button"; - import { MarkdownText } from "./markdown-text"; - import { TooltipIconButton } from "./tooltip-icon-button"; - import { Spin, Flex } from "antd"; - import { LoadingOutlined } from "@ant-design/icons"; - import styled from "styled-components"; -import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments } from "../ui/attachment"; - const SimpleANTDLoader = () => { - const antIcon = ; - - return ( -
- - - Working on it... - -
- ); - }; - - const StyledThreadRoot = styled(ThreadPrimitive.Root)` - /* Hide entire assistant message container when it contains running status */ - .aui-assistant-message-root:has([data-status="running"]) { - display: none; - } - - /* Fallback for older browsers that don't support :has() */ - .aui-assistant-message-content [data-status="running"] { - display: none; - } -`; - - - interface ThreadProps { - placeholder?: string; - showAttachments?: boolean; - } - - export const Thread: FC = ({ - placeholder = trans("chat.composerPlaceholder"), - showAttachments = true - }) => { - // Stable component reference so React doesn't unmount/remount on every render - const UserMessageComponent = useMemo(() => { - const Wrapper: FC = () => ; - Wrapper.displayName = "UserMessage"; - return Wrapper; - }, [showAttachments]); - - return ( - - - - - - - - - - - -
- - -
- - -
- - - ); - }; - - const ThreadScrollToBottom: FC = () => { - return ( - - - - - - ); - }; - - const ThreadWelcome: FC = () => { - return ( - -
-
-

- {trans("chat.welcomeMessage")} -

-
- -
-
- ); - }; - - const ThreadWelcomeSuggestions: FC = () => { - return ( -
- - - {trans("chat.suggestionWeather")} - - - - - {trans("chat.suggestionAssistant")} - - -
- ); - }; - - const Composer: FC<{ placeholder?: string; showAttachments?: boolean }> = ({ - placeholder = trans("chat.composerPlaceholder"), - showAttachments = true - }) => { - return ( - - {showAttachments && ( - <> - - - - )} - - - - ); - }; - - const ComposerAction: FC = () => { - return ( - <> - - - - - - - - - - - - - - - - ); - }; - - const UserMessage: FC<{ showAttachments?: boolean }> = ({ showAttachments = true }) => { - return ( - - - {showAttachments && } - -
- -
- - -
- ); - }; - - const UserActionBar: FC = () => { - return ( - - - - - - - - ); - }; - - const EditComposer: FC = () => { - return ( - - - -
- - - - - - -
-
- ); - }; - - const AssistantMessage: FC = () => { - return ( - -
- -
- - - - -
- ); - }; - - const AssistantActionBar: FC = () => { - return ( - - - - - - - - - - - - - ); - }; - - const BranchPicker: FC = ({ - className, - ...rest - }) => { - return ( - - - - - - - - / - - - - - - - - ); - }; - - const CircleStopIcon = () => { - return ( - - - - ); - }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/tooltip-icon-button.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/tooltip-icon-button.tsx deleted file mode 100644 index d2434babff..0000000000 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/assistant-ui/tooltip-icon-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ComponentPropsWithoutRef, forwardRef } from "react"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "../ui/tooltip"; -import { Button } from "../ui/button"; -import { cn } from "../../utils/cn"; - -export type TooltipIconButtonProps = ComponentPropsWithoutRef & { - tooltip: string; - side?: "top" | "bottom" | "left" | "right"; -}; - -export const TooltipIconButton = forwardRef< - HTMLButtonElement, - TooltipIconButtonProps ->(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { - return ( - - - - - - {tooltip} - - - ); -}); - -TooltipIconButton.displayName = "TooltipIconButton"; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ui/button.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ui/button.tsx deleted file mode 100644 index 945783c696..0000000000 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ui/button.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "../../utils/cn"; - -const buttonVariants = cva("aui-button", { - variants: { - variant: { - default: "aui-button-primary", - outline: "aui-button-outline", - ghost: "aui-button-ghost", - }, - size: { - default: "aui-button-medium", - icon: "aui-button-icon", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, -}); - -const Button = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean; - } ->(({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - - return ( - - ); -}); - -Button.displayName = "Button"; - -export { Button, buttonVariants }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts b/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts index 3ea69fafd9..424666669a 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts @@ -1,88 +1,65 @@ // client/packages/lowcoder/src/comps/comps/chatComp/handlers/messageHandlers.ts -import { MessageHandler, MessageResponse, N8NHandlerConfig, QueryHandlerConfig, ChatMessage } from "../types/chatTypes"; -import { CompAction, routeByNameAction, executeQueryAction } from "lowcoder-core"; -import { getPromiseAfterDispatch } from "util/promiseUtils"; +import { AIAssistantMessageHandler, MessageHandler, QueryHandlerConfig, ChatMessage } from "../types/chatTypes"; +import { routeByNameAction, executeQueryAction } from "lowcoder-core"; +import { getPromiseAfterDispatch } from "util/promiseUtils"; +import { buildAutomatorPayload } from "../../preLoadComp/actions/automator"; +import { + getTextFromThreadContent, + toAssistantMessage, +} from "../utils/assistantMessages"; + +function buildAutomatorQueryArgs( + payload: ReturnType +) { + const ai = { + mode: "automator" as const, + messages: payload.messages, + tools: payload.tools, + }; + + return { + ai: { + value: ai, + }, + }; +} // ============================================================================ -// N8N HANDLER (for Bottom Panel) -// ============================================================================ - -export class N8NHandler implements MessageHandler { - constructor(private config: N8NHandlerConfig) {} - - async sendMessage(message: ChatMessage, sessionId?: string): Promise { - const { modelHost, systemPrompt, streaming } = this.config; - - if (!modelHost) { - throw new Error("Model host is required for N8N calls"); - } - - try { - const response = await fetch(modelHost, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - sessionId, - message: message.text, - systemPrompt: systemPrompt || "You are a helpful assistant.", - streaming: streaming || false - }) - }); - - if (!response.ok) { - throw new Error(`N8N call failed: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - if (data.output) { - const { explanation, actions } = JSON.parse(data.output); - return { content: explanation, actions }; - } - // Extract content from various possible response formats - const content = data.response || data.message || data.content || data.text || String(data); - - return { content }; - } catch (error) { - throw new Error(`N8N call failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } -} - -// ============================================================================ -// QUERY HANDLER (for Canvas Components) +// QUERY HANDLER // ============================================================================ export class QueryHandler implements MessageHandler { constructor(private config: QueryHandlerConfig) {} - async sendMessage(message: ChatMessage, sessionId?: string): Promise { - const { chatQuery, dispatch} = this.config; - - // If no query selected or dispatch unavailable, return mock response - if (!chatQuery || !dispatch) { - await new Promise((res) => setTimeout(res, 500)); - return { content: "(mock) You typed: " + message.text }; - } + async sendMessage(message: ChatMessage): Promise { + const { chatQuery, dispatch} = this.config; + + if (!chatQuery) { + throw new Error("Select a query before sending a message"); + } + + if (!dispatch) { + throw new Error("Query dispatch is unavailable"); + } try { + console.log("Executing query:", chatQuery); const result: any = await getPromiseAfterDispatch( dispatch, routeByNameAction( chatQuery, executeQueryAction({ - // Pass the full message object so attachments are available in queries - args: { - message: { value: message }, // Full ChatMessage object with attachments - prompt: { value: message.text }, // Keep backward compatibility - }, + // Pass the full message object so attachments are available in queries + args: { + message: { value: message }, + prompt: { value: getTextFromThreadContent(message.content) }, + }, }) ) ); - - return result.message + console.log("Query result:", result); + return toAssistantMessage(result); } catch (e: any) { throw new Error(e?.message || "Query execution failed"); } @@ -90,37 +67,89 @@ export class QueryHandler implements MessageHandler { } // ============================================================================ -// MOCK HANDLER (for testing/fallbacks) -// ============================================================================ - -export class MockHandler implements MessageHandler { - constructor(private delay: number = 1000) {} +// AI ASSISTANT QUERY HANDLER (bottom panel) +// ---------------------------------------------------------------------------- +// This handler owns the Lowcoder side of the Automator flow: +// 1. snapshot the current editor state, +// 2. build the system prompt, tools, catalogs, and live context, +// 3. pass that payload to the selected user query, +// 4. accept an Assistant UI `ThreadMessageLike` assistant message. +// +// Provider-specific parsing belongs in the selected query/backend bridge. +// ============================================================================ + +export class AIAssistantQueryHandler implements AIAssistantMessageHandler { + constructor(private config: QueryHandlerConfig) {} + + async sendMessage( + _message: ChatMessage, + _sessionId: string | undefined, + conversationHistory: ChatMessage[] + ): Promise { + const { chatQuery, dispatch, getEditorState } = this.config; + const history = conversationHistory; + + // Conversation history in the OpenAI {role, content} shape. + const rawHistory = history.map((msg) => ({ + role: msg.role, + content: getTextFromThreadContent(msg.content), + })); + + if (!chatQuery) { + throw new Error("Select an Automator query before sending a message"); + } + + if (!dispatch) { + throw new Error("Automator dispatch is unavailable"); + } + + if (!getEditorState) { + throw new Error("Automator editor state is unavailable"); + } + + const editorState = getEditorState(); + const payload = buildAutomatorPayload({ + history: rawHistory, + editorState, + }); + + try { + console.log("[Automator] running query:", chatQuery, { + contextComponents: payload.context.components.length, + contextQueries: payload.context.queries.length, + messageCount: payload.messages.length, + }); - async sendMessage(message: ChatMessage): Promise { - await new Promise(resolve => setTimeout(resolve, this.delay)); - return { content: `Mock response: ${message.text}` }; + const result: any = await getPromiseAfterDispatch( + dispatch, + routeByNameAction( + chatQuery, + executeQueryAction({ + args: buildAutomatorQueryArgs(payload), + }) + ) + ); + + return toAssistantMessage(result); + } catch (e: any) { + throw new Error(e?.message || "AI assistant query execution failed"); + } } } -// ============================================================================ -// HANDLER FACTORY (creates the right handler based on type) -// ============================================================================ - -export function createMessageHandler( - type: "n8n" | "query" | "mock", - config: N8NHandlerConfig | QueryHandlerConfig -): MessageHandler { - switch (type) { - case "n8n": - return new N8NHandler(config as N8NHandlerConfig); - - case "query": - return new QueryHandler(config as QueryHandlerConfig); - - case "mock": - return new MockHandler(); - - default: - throw new Error(`Unknown message handler type: ${type}`); - } -} \ No newline at end of file +// ============================================================================ +// HANDLER FACTORY (creates the right handler based on type) +// ============================================================================ + +export function createMessageHandler( + type: "query", + config: QueryHandlerConfig +): MessageHandler { + switch (type) { + case "query": + return new QueryHandler(config); + + default: + throw new Error(`Unknown message handler type: ${type}`); + } +} diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts index d24e0ce84f..0a8035a30c 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -1,12 +1,17 @@ -import { CompleteAttachment } from "@assistant-ui/react"; - -export interface ChatMessage { - id: string; - role: "user" | "assistant"; - text: string; - timestamp: number; - attachments?: CompleteAttachment[]; - } +import type { ThreadMessageLike } from "@assistant-ui/react"; + +export type ChatMessageContent = Exclude; + +export type ChatMessage = Omit< + ThreadMessageLike, + "id" | "role" | "content" | "createdAt" | "attachments" +> & { + id: string; + role: "user" | "assistant"; + content: ChatMessageContent; + createdAt: Date; + attachments?: ThreadMessageLike["attachments"]; + }; export interface ChatThread { threadId: string; @@ -39,31 +44,28 @@ export interface ChatMessage { // MESSAGE HANDLER INTERFACE (new clean abstraction) // ============================================================================ - export interface MessageHandler { - sendMessage(message: ChatMessage, sessionId?: string): Promise; - // Future: sendMessageStream?(message: ChatMessage): AsyncGenerator; - } - - export interface MessageResponse { - content: string; - metadata?: any; - actions?: any[]; - } + export interface MessageHandler { + sendMessage(message: ChatMessage, sessionId?: string): Promise; + // Future: sendMessageStream?(message: ChatMessage): AsyncGenerator; + } + + export interface AIAssistantMessageHandler { + sendMessage(message: ChatMessage, sessionId: string | undefined, conversationHistory: ChatMessage[]): Promise; + } // ============================================================================ // CONFIGURATION TYPES (simplified) // ============================================================================ - export interface N8NHandlerConfig { - modelHost: string; - systemPrompt?: string; - streaming?: boolean; - } - export interface QueryHandlerConfig { chatQuery: string; dispatch: any; - } + /** + * Snapshot accessor for the live editor state. The handler calls this + * lazily on every send so it always has the *current* canvas state. + */ + getEditorState?: () => any; + } // ============================================================================ // COMPONENT PROPS (what each component actually needs) @@ -93,8 +95,6 @@ export interface ChatCoreProps { // Bottom Panel Props (simplified, no styling controls) export interface ChatPanelProps { tableName: string; - modelHost: string; - systemPrompt?: string; - streaming?: boolean; + chatQuery: string; onMessageUpdate?: (message: string) => void; } diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.ts new file mode 100644 index 0000000000..7c9d5e86a3 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/assistantMessages.ts @@ -0,0 +1,118 @@ +import type { + AppendMessage, + CompleteAttachment, + TextMessagePart, + ThreadAssistantMessagePart, + ThreadMessageLike, + ThreadUserMessagePart, +} from "@assistant-ui/react"; + +import type { ChatMessage, ChatMessageContent } from "../types/chatTypes"; + +export const generateMessageId = () => Math.random().toString(36).substr(2, 9); + +export const getTextFromThreadContent = ( + content: ThreadMessageLike["content"] +) => { + if (typeof content === "string") return content; + + return content + .filter((part) => part.type === "text") + .map((part) => part.text) + .join("\n") + .trim(); +}; + +export const generateThreadTitle = (message: ChatMessage) => { + const text = getTextFromThreadContent(message.content) + .replace(/\s+/g, " ") + .trim(); + + if (!text) return ""; + if (text.length <= 50) return text; + + const clipped = text.slice(0, 50).replace(/\s+\S*$/, "").trim(); + return `${clipped || text.slice(0, 50).trim()}...`; +}; + +export const shouldGenerateThreadTitle = ( + existingTitle: string | undefined, + defaultTitle: string, + existingMessageCount: number +) => { + return ( + existingMessageCount === 0 && + (!existingTitle || existingTitle.trim() === defaultTitle.trim()) + ); +}; + +export const getTextFromAppendMessage = (message: AppendMessage) => { + const textPart = message.content.find( + (part): part is TextMessagePart => part.type === "text" + ); + return textPart?.text?.trim() ?? ""; +}; + +export const createUserMessage = ( + text: string, + attachments: CompleteAttachment[] = [] +): ChatMessage => { + const content: ThreadUserMessagePart[] = text + ? [{ type: "text", text }] + : []; + + return { + id: generateMessageId(), + role: "user", + content, + createdAt: new Date(), + ...(attachments.length && { attachments }), + }; +}; + +export const createAssistantErrorMessage = (text: string): ChatMessage => ({ + id: generateMessageId(), + role: "assistant", + content: [{ type: "text", text }], + createdAt: new Date(), +}); + +export const toChatMessage = (message: ThreadMessageLike): ChatMessage => { + if (message.role === "system") { + throw new Error("System messages are not stored in chat threads"); + } + + const content = + typeof message.content === "string" + ? ([{ type: "text", text: message.content }] as ChatMessageContent) + : (message.content as ChatMessageContent); + + return { + ...message, + id: message.id ?? generateMessageId(), + role: message.role, + content, + createdAt: message.createdAt ?? new Date(), + }; +}; + +export const toAssistantMessage = (message: ThreadMessageLike): ChatMessage => { + const chatMessage = toChatMessage(message); + if (chatMessage.role !== "assistant") { + throw new Error("Query must return an assistant message"); + } + return chatMessage; +}; + +export const getAutomatorActionsFromMessage = (message: ChatMessage) => { + const toolPart = message.content.find( + (part): part is Extract => + part.type === "tool-call" && + part.toolName === "execute_automator_actions" + ); + + if (!toolPart) return []; + + const actions = (toolPart.args as any)?.actions; + return Array.isArray(actions) ? actions : []; +}; diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts index 9ff22d4364..3e4c23e1c8 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts @@ -3,14 +3,14 @@ import type { PendingAttachment, CompleteAttachment, Attachment, - ThreadUserContentPart + ThreadUserMessagePart } from "@assistant-ui/react"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB export const universalAttachmentAdapter: AttachmentAdapter = { - accept: "*/*", + accept: "*", async add({ file }): Promise { if (file.size > MAX_FILE_SIZE) { @@ -38,7 +38,7 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" async send(attachment: PendingAttachment): Promise { const isImage = attachment.contentType?.startsWith("image/"); - let content: ThreadUserContentPart[]; + let content: ThreadUserMessagePart[]; try { content = isImage @@ -93,4 +93,4 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" function getAttachmentType(mime: string): "image" | "file" { return mime.startsWith("image/") ? "image" : "file"; } - \ No newline at end of file + diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/storageFactory.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/storageFactory.ts index c641dbbefc..ae2a033586 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/utils/storageFactory.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/storageFactory.ts @@ -1,7 +1,8 @@ // client/packages/lowcoder/src/comps/comps/chatComp/utils/storageFactory.ts -import alasql from "alasql"; -import { ChatMessage, ChatThread, ChatStorage } from "../types/chatTypes"; +import alasql from "alasql"; +import { ChatMessage, ChatThread, ChatStorage } from "../types/chatTypes"; +import { getTextFromThreadContent } from "./assistantMessages"; // ============================================================================ // CLEAN STORAGE FACTORY (simplified from your existing implementation) @@ -32,15 +33,22 @@ export function createChatStorage(tableName: string): ChatStorage { // Create messages table await alasql.promise(` - CREATE TABLE IF NOT EXISTS ${messagesTable} ( - id STRING PRIMARY KEY, - threadId STRING, - role STRING, - text STRING, - timestamp NUMBER, - attachments STRING - ) - `); + CREATE TABLE IF NOT EXISTS ${messagesTable} ( + id STRING PRIMARY KEY, + threadId STRING, + role STRING, + text STRING, + timestamp NUMBER, + attachments STRING, + content STRING + ) + `); + + try { + await alasql.promise(`ALTER TABLE ${messagesTable} ADD COLUMN content STRING`); + } catch (error) { + // Existing databases may already have the AUI content column. + } } catch (error) { console.error(`Failed to initialize chat database ${dbName}:`, error); @@ -104,9 +112,18 @@ export function createChatStorage(tableName: string): ChatStorage { // Insert or replace message await alasql.promise(`DELETE FROM ${messagesTable} WHERE id = ?`, [message.id]); - await alasql.promise(` - INSERT INTO ${messagesTable} VALUES (?, ?, ?, ?, ?, ?) - `, [message.id, threadId, message.role, message.text, message.timestamp, JSON.stringify(message.attachments || [])]); + await alasql.promise(` + INSERT INTO ${messagesTable} (id, threadId, role, text, timestamp, attachments, content) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, [ + message.id, + threadId, + message.role, + getTextFromThreadContent(message.content), + message.createdAt.getTime(), + JSON.stringify(message.attachments || []), + JSON.stringify(message.content), + ]); } catch (error) { console.error("Failed to save message:", error); throw error; @@ -120,9 +137,18 @@ export function createChatStorage(tableName: string): ChatStorage { // Insert all messages for (const message of messages) { - await alasql.promise(` - INSERT INTO ${messagesTable} VALUES (?, ?, ?, ?, ?, ?) - `, [message.id, threadId, message.role, message.text, message.timestamp, JSON.stringify(message.attachments || [])]); + await alasql.promise(` + INSERT INTO ${messagesTable} (id, threadId, role, text, timestamp, attachments, content) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, [ + message.id, + threadId, + message.role, + getTextFromThreadContent(message.content), + message.createdAt.getTime(), + JSON.stringify(message.attachments || []), + JSON.stringify(message.content), + ]); } } catch (error) { console.error("Failed to save messages:", error); @@ -132,18 +158,18 @@ export function createChatStorage(tableName: string): ChatStorage { async getMessages(threadId: string) { try { - const result = await alasql.promise(` - SELECT id, role, text, timestamp, attachments FROM ${messagesTable} - WHERE threadId = ? ORDER BY timestamp ASC - `, [threadId]) as any[]; - - return result.map(row => ({ - id: row.id, - role: row.role, - text: row.text, - timestamp: row.timestamp, - attachments: JSON.parse(row.attachments || '[]') - })) as ChatMessage[]; + const result = await alasql.promise(` + SELECT id, role, text, timestamp, attachments, content FROM ${messagesTable} + WHERE threadId = ? ORDER BY timestamp ASC + `, [threadId]) as any[]; + + return result.map(row => ({ + id: row.id, + role: row.role, + content: JSON.parse(row.content || "null") || [{ type: "text", text: row.text || "" }], + createdAt: new Date(row.timestamp), + attachments: JSON.parse(row.attachments || '[]') + })) as ChatMessage[]; } catch (error) { console.error("Failed to get messages:", error); return []; @@ -190,4 +216,4 @@ export function createChatStorage(tableName: string): ChatStorage { } } }; -} \ No newline at end of file +} diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx index 4738291683..8b1d4c8400 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx @@ -184,7 +184,17 @@ let JsonEditorTmpComp = (function () { return ( <>
- {children.value.propertyView({ label: trans("export.jsonEditorDesc") })} + {children.value.propertyView({ + label: trans("export.jsonEditorDesc"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "JSON Editor value", + fieldName: "value", + fieldDescription: + "JSON value edited by the JSON Editor component. Generate valid JSON that can be an object, array, string, number, boolean, or null.", + }, + })}
diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx index d1c6b917cb..f929dd68ce 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx @@ -83,7 +83,17 @@ let JsonExplorerTmpComp = (function () { return ( <>
- {children.value.propertyView({ label: trans("export.jsonEditorDesc") })} + {children.value.propertyView({ + label: trans("export.jsonEditorDesc"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "JSON Explorer value", + fieldName: "value", + fieldDescription: + "JSON object or array displayed by the JSON Explorer component. Generate valid JSON that is useful to inspect as a nested data structure.", + }, + })}
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx index 0705a745b6..d5c956b85c 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx @@ -374,6 +374,10 @@ let FormBasicComp = (function () { }) .setPropertyViewFn((children) => { const formType = children.formType.getView(); + const uiSchemaFieldDescription = + formType === "rjsf" + ? "RJSF UI schema object for configuring widgets, placeholders, field order, submit button options, and other react-jsonschema-form UI behavior. It should match the JSON Schema fields." + : "JSONForms UI schema object for configuring controls, scopes, layouts, categories, rules, and renderer options. It should match the JSON Schema fields."; return ( <> {(useContext(EditorContext).editorModeStatus === "logic" || @@ -431,6 +435,14 @@ let FormBasicComp = (function () {
), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "JSON Schema Form schema", + fieldName: "schema", + fieldDescription: + "JSON Schema object that defines the form fields, field types, required fields, validation constraints, enum options, titles, and descriptions.", + }, })} {children.uiSchema.propertyView({ key: trans("jsonSchemaForm.uiSchema"), @@ -471,10 +483,25 @@ let FormBasicComp = (function () { ), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: `${formType === "rjsf" ? "RJSF" : "JSONForms"} UI schema`, + fieldName: "uiSchema", + fieldDescription: uiSchemaFieldDescription, + }, })} {children.data.propertyView({ key: trans("jsonSchemaForm.defaultData"), label: trans("jsonSchemaForm.defaultData"), + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "JSON Schema Form default data", + fieldName: "data", + fieldDescription: + "Default form data object. Generate sample initial values that match the current JSON Schema shape, field names, and value types.", + }, })} {children.formType.getView() === "rjsf" && ( children.errorSchema.propertyView({ diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts new file mode 100644 index 0000000000..4c813a4a03 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts @@ -0,0 +1,235 @@ +// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/actionsCatalog.ts + +/** + * Machine-readable catalog of ALL actions the Automator may emit. + * + * Each entry tells the model: + * - the canonical action name (matches ACTION_REGISTRY in ChatPanelContainer) + * - the required and optional payload fields + * - one example + * + * This is the source of truth for "what the Automator can do". + */ + +export type AutomatorActionName = + | "place_component" + | "nest_component" + | "move_component" + | "resize_component" + | "delete_component" + | "delete_query" + | "rename_component" + | "set_properties" + | "set_style" + | "set_theme" + | "set_app_metadata" + | "set_canvas_setting" + | "set_global_css" + | "set_global_javascript" + | "add_event_handler" + | "publish_app" + | "align_component"; + +export interface ActionCatalogEntry { + action: AutomatorActionName; + purpose: string; + required: string[]; + optional?: string[]; + example: Record; +} + +export const ACTIONS_CATALOG: ActionCatalogEntry[] = [ + // ── Component Management ────────────────────────────────────────── + { + action: "place_component", + purpose: "Place a new component on the root canvas.", + required: ["component", "component_name", "layout", "action_parameters"], + example: { + action: "place_component", + component: "button", + component_name: "submitBtn", + layout: { x: 0, y: 0, w: 6, h: 5 }, + action_parameters: { text: "Submit" }, + }, + }, + { + action: "nest_component", + purpose: + "Create a NEW component inside an existing container. Do not use this to move or reparent an existing component; if the requested component already exists, ask/explain instead of duplicating it.", + required: ["component", "component_name", "parent_component_name", "layout", "action_parameters"], + example: { + action: "nest_component", + component: "input", + component_name: "emailInput", + parent_component_name: "loginForm.container.body.0.view", + layout: { x: 0, y: 0, w: 24, h: 6 }, + action_parameters: { + label: { text: "Email", position: "row", align: "left" }, + placeholder: "you@example.com", + }, + }, + }, + { + action: "move_component", + purpose: "Move an existing component to a new grid position.", + required: ["component_name", "layout"], + example: { + action: "move_component", + component_name: "submitBtn", + layout: { x: 6, y: 0 }, + }, + }, + { + action: "resize_component", + purpose: "Change the width/height of an existing component.", + required: ["component_name", "layout"], + example: { + action: "resize_component", + component_name: "todoTable", + layout: { w: 24, h: 30 }, + }, + }, + { + action: "delete_component", + purpose: "Remove an existing component from the canvas.", + required: ["component_name"], + example: { + action: "delete_component", + component_name: "oldButton", + }, + }, + { + action: "delete_query", + purpose: + "Delete an existing bottom-panel data query by name. This action only needs the query name and does not read the query configuration or body.", + required: ["query_name"], + example: { + action: "delete_query", + query_name: "getUsers", + }, + }, + { + action: "rename_component", + purpose: "Rename an existing component.", + required: ["component_name", "action_parameters"], + example: { + action: "rename_component", + component_name: "button1", + action_parameters: { new_name: "submitButton" }, + }, + }, + { + action: "align_component", + purpose: + "Move a component horizontally on the canvas grid (left, center, right). This positions the COMPONENT in the canvas — it does NOT change text alignment INSIDE a component. For text/content alignment, use `set_properties` with the component's `horizontalAlignment` / `verticalAlignment` layoutProperties.", + required: ["component_name", "action_parameters"], + example: { + action: "align_component", + component_name: "title1", + action_parameters: { alignment: "center" }, + }, + }, + + // ── Properties & Styling ────────────────────────────────────────── + { + action: "set_properties", + purpose: + "Update top-level properties on an existing component (text, alignment, autoHeight, type, disabled, label, options, …). Use this for behaviour and layout-style props listed in the component's `layoutProperties`. Use `set_style` for CSS-like visual props.", + required: ["component_name", "action_parameters"], + example: { + action: "set_properties", + component_name: "title1", + action_parameters: { horizontalAlignment: "center", verticalAlignment: "center" }, + }, + }, + { + action: "set_style", + purpose: + "Apply basic visual styles to a component. Pass an object grouped by explicit style namespace (`style`, `labelStyle`, `inputFieldStyle`, `headerStyle`, `bodyStyle`, etc.). Do not pass flat style keys and do not use animation styles.", + required: ["component_name", "action_parameters"], + example: { + action: "set_style", + component_name: "submitBtn", + action_parameters: { + style: { + background: "#1677ff", + text: "#ffffff", + radius: "8px", + textSize: "14px", + textWeight: "600", + padding: "8px 16px", + }, + }, + }, + }, + + // ── Events ──────────────────────────────────────────────────────── + { + action: "add_event_handler", + purpose: "Add an event handler (click, change, etc.) to a component.", + required: ["component_name", "action_parameters"], + example: { + action: "add_event_handler", + component_name: "submitBtn", + action_parameters: { event: "click", action_type: "message" }, + }, + optional: ["action_parameters.config"], + }, + + // ── App-Level Configuration ─────────────────────────────────────── + { + action: "set_theme", + purpose: "Apply a theme to the whole app.", + required: ["action_parameters"], + example: { + action: "set_theme", + action_parameters: { theme: "default" }, + }, + }, + { + action: "set_app_metadata", + purpose: "Set app title, description, or category.", + required: ["action_parameters"], + example: { + action: "set_app_metadata", + action_parameters: { title: "My Todo App", description: "A task tracker." }, + }, + }, + { + action: "set_canvas_setting", + purpose: "Update canvas grid settings (columns, row height, padding, bg).", + required: ["action_parameters"], + example: { + action: "set_canvas_setting", + action_parameters: { gridColumns: 24, gridRowHeight: 8, maxWidth: "1440" }, + }, + }, + { + action: "set_global_css", + purpose: "Set global CSS rules for the entire app.", + required: ["action_parameters"], + example: { + action: "set_global_css", + action_parameters: { code: "body { font-family: 'Inter', sans-serif; }" }, + }, + }, + { + action: "set_global_javascript", + purpose: "Set global JavaScript that runs on app load.", + required: ["action_parameters"], + example: { + action: "set_global_javascript", + action_parameters: { code: "console.log('App loaded');" }, + }, + }, + { + action: "publish_app", + purpose: "Publish the current app so end-users can access it.", + required: [], + example: { action: "publish_app" }, + }, +]; + +export const SUPPORTED_ACTIONS = new Set( + ACTIONS_CATALOG.map((a) => a.action) +); diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts new file mode 100644 index 0000000000..1aa61d12d0 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/entries.ts @@ -0,0 +1,989 @@ +import type { AutomatorComponentEntry } from "./types"; +import { + ALIGN_HORIZONTAL, + AUTO_HEIGHT, + COMMON_STYLE_KEYS, + CONTAINER_STYLE_KEYS, + DISABLED, + HIDDEN, + IMAGE_STYLE_KEYS, + INPUT_LIKE_STYLE_KEYS, + LABEL_OBJECT, + LABEL_STYLE_KEYS, + LOADING, + NAVIGATION_STYLE_KEYS, + TEXT_HORIZONTAL_ALIGNMENT, + VERTICAL_ALIGNMENT, +} from "./presets"; + +const TEXT: AutomatorComponentEntry = { + type: "text", + defaultLayout: { w: 12, h: 4 }, + required: ["text"], + optional: [ + "type", + "horizontalAlignment", + "verticalAlignment", + "autoHeight", + "contentScrollBar", + "hidden", + ], + example: { text: "## Hello", type: "markdown" }, + notes: + "Use type:'markdown' for headings, links, formatted text. For text alignment INSIDE the component, set the `horizontalAlignment` property (NOT the `align_component` action — that one moves the component on the canvas grid).", + layoutProperties: { + type: { + description: "Render mode for the value.", + enum: ["markdown", "text"], + }, + horizontalAlignment: TEXT_HORIZONTAL_ALIGNMENT, + verticalAlignment: VERTICAL_ALIGNMENT, + autoHeight: AUTO_HEIGHT, + contentScrollBar: { + description: "Show scrollbars when content overflows (only when autoHeight=fixed).", + type: "boolean", + }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS, "links"], + }, +}; + +const BUTTON: AutomatorComponentEntry = { + type: "button", + defaultLayout: { w: 6, h: 5 }, + required: ["text"], + optional: [ + "type", + "disabled", + "loading", + "form", + "prefixIcon", + "suffixIcon", + "tooltip", + "tabIndex", + "hidden", + ], + example: { text: "Submit", type: "" }, + notes: + "Set type:'submit' and form:'' to submit a form. Leave type:'' for a default click-handler button.", + layoutProperties: { + type: { + description: "'' for default click-handler button, 'submit' for a form-submit button.", + enum: ["", "submit"], + }, + disabled: DISABLED, + loading: LOADING, + hidden: HIDDEN, + prefixIcon: { + description: + "Icon path string ('/icon:solid/check') shown before the text. Empty string clears it.", + type: "string", + }, + suffixIcon: { + description: "Icon shown after the text.", + type: "string", + }, + tooltip: { description: "Hover tooltip text.", type: "string" }, + tabIndex: { description: "Tab order for keyboard navigation.", type: "number" }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + }, +}; + +const INPUT: AutomatorComponentEntry = { + type: "input", + defaultLayout: { w: 12, h: 6 }, + required: ["label", "placeholder"], + optional: [ + "value", + "validationType", + "required", + "allowClear", + "showCount", + "readOnly", + "disabled", + "hidden", + "prefixIcon", + "suffixIcon", + ], + example: { + label: { text: "Name", position: "row", align: "left" }, + placeholder: "Enter name", + allowClear: true, + }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + showCount: { description: "Show character counter.", type: "boolean" }, + allowClear: { description: "Show a clear button.", type: "boolean" }, + readOnly: { description: "Read-only field.", type: "boolean" }, + required: { description: "Mark as required for form validation.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + }, +}; + +const NUMBER_INPUT: AutomatorComponentEntry = { + type: "numberInput", + defaultLayout: { w: 12, h: 6 }, + required: ["label"], + optional: [ + "value", + "min", + "max", + "step", + "placeholder", + "disabled", + "hidden", + "readOnly", + ], + example: { + label: { text: "Quantity", position: "row" }, + value: 1, + min: 0, + max: 100, + }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + readOnly: { description: "Read-only field.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + }, +}; + +const DROPDOWN: AutomatorComponentEntry = { + type: "select", + defaultLayout: { w: 12, h: 6 }, + required: ["label", "options", "value"], + optional: ["allowClear", "disabled", "hidden", "showSearch", "placeholder"], + example: { + label: { text: "Status", position: "row" }, + options: { + optionType: "manual", + manual: { + manual: [ + { value: "pending", label: "Pending" }, + { value: "done", label: "Done" }, + ], + }, + }, + value: "pending", + }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + allowClear: { description: "Show a clear button.", type: "boolean" }, + showSearch: { description: "Enable search filter.", type: "boolean" }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS, "accent", "validate"], + labelStyle: [...LABEL_STYLE_KEYS], + childrenInputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + }, +}; + +const CHECKBOX: AutomatorComponentEntry = { + type: "checkbox", + defaultLayout: { w: 8, h: 5 }, + required: ["label"], + optional: ["value", "options", "disabled", "hidden", "layout"], + example: { + label: { text: "I agree", position: "row" }, + value: false, + }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + layout: { + description: "Group layout direction for multi-option checkboxes.", + enum: ["horizontal", "vertical", "autoColumns"], + }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS, "checkedBackground", "uncheckedBackground", "uncheckedBorder", "hoverBackground"], + }, +}; + +const FORM: AutomatorComponentEntry = { + type: "form", + isContainer: true, + defaultLayout: { w: 12, h: 30 }, + required: ["container"], + optional: ["hidden", "disabled"], + example: { + container: { + header: {}, + body: { "0": { view: {} } }, + footer: {}, + showHeader: false, + showBody: true, + showFooter: true, + autoHeight: "auto", + horizontalGridCells: 24, + scrollbars: false, + showVerticalScrollbar: false, + style: {}, + }, + }, + notes: + "Nest input/select/etc. under '.container.body.0.view'. Submit button goes under '.container.footer' with type:'submit', form:''.", + layoutProperties: { + hidden: HIDDEN, + disabled: DISABLED, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...CONTAINER_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + footerStyle: [...CONTAINER_STYLE_KEYS], + }, +}; + +const CONTAINER: AutomatorComponentEntry = { + type: "container", + isContainer: true, + defaultLayout: { w: 12, h: 20 }, + required: ["container"], + optional: ["hidden"], + example: { + container: { + header: {}, + body: { "0": { view: {} } }, + footer: {}, + showHeader: false, + showBody: true, + showFooter: false, + autoHeight: "auto", + horizontalGridCells: 24, + style: {}, + }, + }, + notes: + "Nest under '.container.body.0.view'.", + layoutProperties: { + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...CONTAINER_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + footerStyle: [...CONTAINER_STYLE_KEYS], + }, +}; + +const MODAL: AutomatorComponentEntry = { + type: "modal", + isContainer: true, + defaultLayout: { w: 12, h: 40 }, + required: ["title", "container"], + optional: ["open", "showMask", "maskClosable", "width", "hidden"], + example: { + title: "Add Item", + open: false, + container: {}, + }, + notes: + "container MUST be empty {}. Children are nested under '.container' (no body/header/footer paths).", + layoutProperties: { + open: { description: "Whether the modal is visible.", type: "boolean" }, + showMask: { description: "Render the dim background mask.", type: "boolean" }, + maskClosable: { description: "Allow closing by clicking the mask.", type: "boolean" }, + width: { description: 'Modal width, e.g. "600px" or "60%".', type: "string" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + }, +}; + +const DRAWER: AutomatorComponentEntry = { + type: "drawer", + isContainer: true, + defaultLayout: { w: 12, h: 40 }, + required: ["title", "container"], + optional: ["open", "placement", "showMask", "maskClosable", "width", "hidden"], + example: { title: "Edit", open: false, container: {} }, + notes: + "Same flat-container rule as modal. Nest under '.container'.", + layoutProperties: { + open: { description: "Whether the drawer is visible.", type: "boolean" }, + placement: { + description: "Edge from which the drawer slides in.", + enum: ["top", "right", "bottom", "left"], + }, + showMask: { description: "Render the dim background mask.", type: "boolean" }, + maskClosable: { description: "Allow closing by clicking the mask.", type: "boolean" }, + width: { description: 'Drawer width, e.g. "400px".', type: "string" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + }, +}; + +const TABLE: AutomatorComponentEntry = { + type: "table", + defaultLayout: { w: 24, h: 30 }, + required: ["columns", "data"], + optional: [ + "pagination", + "showRowGridBorder", + "showHeader", + "size", + "hidden", + "rowAutoHeight", + ], + layoutProperties: { + showHeader: { description: "Render the column header row.", type: "boolean" }, + showRowGridBorder: { description: "Outline each row.", type: "boolean" }, + size: { description: "Row density.", enum: ["small", "middle", "large"] }, + rowAutoHeight: { description: "Auto-size each row to content.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...COMMON_STYLE_KEYS], + rowStyle: [...COMMON_STYLE_KEYS], + cellStyle: [...COMMON_STYLE_KEYS], + }, + example: { + columns: [ + { + title: "Task", + dataIndex: "task", + render: { compType: "text", comp: { text: "{{currentCell}}" } }, + }, + { + title: "Status", + dataIndex: "status", + render: { compType: "text", comp: { text: "{{currentCell}}" } }, + }, + ], + data: '[{"task":"Buy groceries","status":"Pending"},{"task":"Call Alice","status":"Done"}]', + pagination: { pageSizeOptions: "[5, 10, 20, 50]" }, + showRowGridBorder: true, + }, + notes: + "`data` MUST be a stringified JSON array. Use {{currentCell}} in render unless told otherwise.", +}; + +const LIST_VIEW: AutomatorComponentEntry = { + type: "listView", + isContainer: true, + defaultLayout: { w: 24, h: 30 }, + required: ["container"], + optional: [ + "noOfRows", + "itemIndexName", + "itemDataName", + "noOfColumns", + "horizontal", + "scrollbars", + "hidden", + ], + layoutProperties: { + noOfColumns: { description: "Columns per row in the grid.", type: "number" }, + horizontal: { description: "Render rows horizontally.", type: "boolean" }, + scrollbars: { description: "Always show scrollbars.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + }, + example: { + container: {}, + noOfRows: "3", + itemIndexName: "i", + itemDataName: "currentItem", + }, + notes: + "container is the per-item template. Nest item components directly under '.container' (flat). Do NOT use body/header/footer.", +}; + +const IMAGE: AutomatorComponentEntry = { + type: "image", + defaultLayout: { w: 8, h: 12 }, + required: ["src"], + optional: [ + "autoHeight", + "placement", + "enableOverflow", + "aspectRatio", + "supportPreview", + "hidden", + "clipPath", + ], + example: { src: "https://images.unsplash.com/photo-1518770660439-4636190af475" }, + notes: "src MUST be a real, publicly accessible URL.", + layoutProperties: { + autoHeight: AUTO_HEIGHT, + placement: { + description: "Where the image sits inside the cell.", + enum: [ + "top", + "bottom", + "left", + "right", + "top-left", + "top-right", + "bottom-left", + "bottom-right", + ], + }, + enableOverflow: { description: "Crop image to fit instead of contain.", type: "boolean" }, + aspectRatio: { description: 'CSS aspect-ratio (e.g. "16 / 9").', type: "string" }, + supportPreview: { description: "Allow click-to-preview at full size.", type: "boolean" }, + hidden: HIDDEN, + clipPath: { description: "CSS clip-path string.", type: "string" }, + }, + styleProperties: { + style: [...IMAGE_STYLE_KEYS], + }, +}; + +const DIVIDER: AutomatorComponentEntry = { + type: "divider", + defaultLayout: { w: 24, h: 2 }, + required: [], + optional: ["title", "align", "type", "dashed", "hidden"], + example: {}, + layoutProperties: { + title: { description: "Optional label rendered in the divider.", type: "string" }, + align: ALIGN_HORIZONTAL, + type: { description: "Orientation.", enum: ["horizontal", "vertical"] }, + dashed: { description: "Render with a dashed line.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + }, +}; + +const DATE: AutomatorComponentEntry = { + type: "date", + defaultLayout: { w: 12, h: 6 }, + required: ["label"], + optional: ["value", "format", "placeholder", "disabled", "hidden", "showTime"], + example: { + label: { text: "Due date", position: "row" }, + format: "YYYY-MM-DD", + }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + showTime: { description: "Include time picker.", type: "boolean" }, + format: { description: 'Display/parse format, e.g. "YYYY-MM-DD".', type: "string" }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS, "accent", "validate"], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + }, +}; + +const SWITCH: AutomatorComponentEntry = { + type: "switch", + defaultLayout: { w: 6, h: 5 }, + required: ["label"], + optional: ["value", "disabled", "hidden"], + example: { + label: { text: "Enabled", position: "row" }, + value: true, + }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + }, + styleProperties: { + style: ["handle", "unchecked", "checked", "margin", "padding"], + labelStyle: [...LABEL_STYLE_KEYS], + }, +}; + +const TEXT_AREA: AutomatorComponentEntry = { + type: "textArea", + defaultLayout: { w: 12, h: 8 }, + required: ["label"], + optional: [ + "placeholder", + "value", + "autoHeight", + "disabled", + "hidden", + "readOnly", + "showCount", + "allowClear", + ], + example: { + label: { text: "Description", position: "row" }, + placeholder: "Enter description...", + }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + autoHeight: AUTO_HEIGHT, + disabled: DISABLED, + hidden: HIDDEN, + readOnly: { description: "Read-only field.", type: "boolean" }, + showCount: { description: "Show character counter.", type: "boolean" }, + allowClear: { description: "Show a clear button.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + }, +}; + +const PASSWORD: AutomatorComponentEntry = { + type: "password", + defaultLayout: { w: 12, h: 6 }, + required: ["label"], + optional: ["placeholder", "disabled", "hidden", "visibilityToggle"], + example: { + label: { text: "Password", position: "row" }, + placeholder: "Enter password", + }, + layoutProperties: { + label: LABEL_OBJECT, + placeholder: { description: "Placeholder text.", type: "string" }, + disabled: DISABLED, + hidden: HIDDEN, + visibilityToggle: { description: "Show the eye toggle to reveal the password.", type: "boolean" }, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS], + }, +}; + +const CHART: AutomatorComponentEntry = { + type: "chart", + defaultLayout: { w: 12, h: 20 }, + required: ["chartType", "data"], + optional: ["title", "xAxisKey", "hidden", "showLegend"], + example: { + chartType: "bar", + data: '[{"category":"A","value":30},{"category":"B","value":50},{"category":"C","value":20}]', + title: "Sales by Category", + xAxisKey: "category", + }, + notes: "chartType: 'bar', 'line', 'pie', 'scatter'. data is a stringified JSON array.", + layoutProperties: { + chartType: { + description: "Visualisation kind.", + enum: ["bar", "line", "pie", "scatter"], + }, + title: { description: "Chart title.", type: "string" }, + showLegend: { description: "Display the legend.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [ + "chartBackgroundColor", + "chartGradientColor", + "chartShadowColor", + "chartBorderColor", + "chartTextColor", + "chartTextSize", + "chartTextWeight", + "chartFontFamily", + "chartFontStyle", + "chartBorderStyle", + "chartBorderRadius", + "chartBorderWidth", + "chartOpacity", + "chartBoxShadow", + "margin", + "padding", + ], + }, +}; + +const CARD: AutomatorComponentEntry = { + type: "card", + isContainer: true, + defaultLayout: { w: 8, h: 15 }, + required: ["title"], + optional: [ + "size", + "showTitle", + "extraTitle", + "cardType", + "CoverImg", + "imgSrc", + "imgHeight", + "showMeta", + "metaTitle", + "metaDesc", + "hoverable", + "showActionIcon", + "hidden", + ], + example: { + title: "Card Title", + cardType: "common", + CoverImg: true, + imgSrc: "https://images.unsplash.com/photo-1518770660439-4636190af475", + imgHeight: "180px", + showMeta: true, + metaTitle: "Project name", + metaDesc: "Short project description", + }, + notes: + "Nest content inside '.container'. For the built-in card cover image, use `set_properties` on the card with `imgSrc` (not `src`) and set `CoverImg: true`; `src` is only for standalone Image components.", + layoutProperties: { + size: { description: "Card density.", enum: ["default", "small"] }, + showTitle: { description: "Render the title bar.", type: "boolean" }, + extraTitle: { description: "Text rendered in the card header extra link.", type: "string" }, + cardType: { description: "Card mode. Built-in cover/meta fields require common.", enum: ["common", "custom"] }, + CoverImg: { description: "Show the built-in cover image.", type: "boolean" }, + imgSrc: { description: "Built-in card cover image URL. Use this for card cover image edits.", type: "string" }, + imgHeight: { description: 'Built-in card cover image height, e.g. "180px" or "auto".', type: "string" }, + showMeta: { description: "Show built-in card meta title and description.", type: "boolean" }, + metaTitle: { description: "Built-in card meta title.", type: "string" }, + metaDesc: { description: "Built-in card meta description.", type: "string" }, + hoverable: { description: "Lift on hover.", type: "boolean" }, + showActionIcon: { description: "Show built-in card action icons.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS, "IconColor", "activateColor"], + headerStyle: [...COMMON_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + }, +}; + +const TABBED_CONTAINER: AutomatorComponentEntry = { + type: "tabbedContainer", + isContainer: true, + defaultLayout: { w: 24, h: 30 }, + required: ["container"], + optional: ["tabs", "tabPosition", "showHeader", "hidden"], + example: { container: {} }, + notes: "Nest content per tab. Tabs are managed via properties.", + layoutProperties: { + tabPosition: { + description: "Tab bar placement.", + enum: ["top", "right", "bottom", "left"], + }, + showHeader: { description: "Show the tabs bar.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...CONTAINER_STYLE_KEYS], + headerStyle: [...COMMON_STYLE_KEYS], + bodyStyle: [...CONTAINER_STYLE_KEYS], + tabsStyle: [...COMMON_STYLE_KEYS], + }, +}; + +const VIDEO: AutomatorComponentEntry = { + type: "video", + defaultLayout: { w: 12, h: 15 }, + required: ["src"], + optional: ["controls", "autoPlay", "loop", "muted", "hidden"], + example: { src: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", controls: true }, + notes: "src must be a real URL. Set layout.h >= 10.", + layoutProperties: { + controls: { description: "Show native player controls.", type: "boolean" }, + autoPlay: { description: "Auto-play on mount (often requires muted=true).", type: "boolean" }, + loop: { description: "Loop the video.", type: "boolean" }, + muted: { description: "Start muted.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: ["margin", "padding"], + }, +}; + +const AVATAR: AutomatorComponentEntry = { + type: "avatar", + defaultLayout: { w: 6, h: 6 }, + required: ["icon", "iconSize"], + optional: ["src", "avatarLabel", "avatarCatption", "shape", "hidden"], + example: { + icon: "/icon:solid/user", + iconSize: "40", + shape: "circle", + avatarLabel: "John Doe", + }, + layoutProperties: { + shape: { description: "Avatar shape.", enum: ["circle", "square"] }, + iconSize: { description: "Icon pixel size as a string, e.g. '40'.", type: "string" }, + hidden: HIDDEN, + }, + styleProperties: { + style: ["background", "fill"], + avatarLabelStyle: [...COMMON_STYLE_KEYS], + avatarContainerStyle: [...CONTAINER_STYLE_KEYS], + }, +}; + +const PROGRESS: AutomatorComponentEntry = { + type: "progress", + defaultLayout: { w: 12, h: 4 }, + required: ["value"], + optional: ["showInfo", "hidden"], + example: { value: "75" }, + layoutProperties: { + showInfo: { description: "Display the percentage label.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: ["text", "textSize", "textWeight", "fontFamily", "fontStyle", "radius", "margin", "padding", "lineHeight", "track", "fill", "success"], + }, +}; + +const RATING: AutomatorComponentEntry = { + type: "rating", + defaultLayout: { w: 8, h: 5 }, + required: ["label"], + optional: ["value", "max", "allowHalf", "disabled", "hidden"], + example: { + label: { text: "Rating", position: "row" }, + value: "3", + max: "5", + }, + layoutProperties: { + label: LABEL_OBJECT, + allowHalf: { description: "Allow half-star ratings.", type: "boolean" }, + disabled: DISABLED, + hidden: HIDDEN, + }, + styleProperties: { + style: ["checked", "unchecked", "margin", "padding"], + labelStyle: [...LABEL_STYLE_KEYS], + }, +}; + +const SLIDER: AutomatorComponentEntry = { + type: "slider", + defaultLayout: { w: 12, h: 5 }, + required: ["label"], + optional: ["value", "min", "max", "step", "disabled", "hidden", "vertical"], + example: { + label: { text: "Volume", position: "row" }, + value: "50", + min: "0", + max: "100", + }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + vertical: { description: "Render vertically.", type: "boolean" }, + }, + styleProperties: { + style: ["fill", "thumb", "thumbBorder", "track", "margin", "padding"], + labelStyle: [...LABEL_STYLE_KEYS], + }, +}; + +const NAVIGATION: AutomatorComponentEntry = { + type: "navigation", + defaultLayout: { w: 24, h: 5 }, + required: ["items"], + optional: ["logoUrl", "horizontalAlignment", "hidden"], + example: { + items: [ + { label: "Home", hidden: false }, + { label: "About", hidden: false }, + ], + }, + layoutProperties: { + horizontalAlignment: ALIGN_HORIZONTAL, + hidden: HIDDEN, + logoUrl: { description: "Optional logo image URL.", type: "string" }, + }, + styleProperties: { + style: [...NAVIGATION_STYLE_KEYS], + }, +}; + +const TIMELINE: AutomatorComponentEntry = { + type: "timeline", + defaultLayout: { w: 12, h: 15 }, + required: ["value"], + optional: ["mode", "reverse", "hidden"], + example: { + value: '[{"title":"Step 1","subTitle":"Started"},{"title":"Step 2","subTitle":"In Progress"}]', + }, + notes: "value must be a stringified JSON array of timeline entries.", + layoutProperties: { + mode: { + description: "Layout mode for entries.", + enum: ["left", "alternate", "right"], + }, + reverse: { description: "Reverse entry order.", type: "boolean" }, + hidden: HIDDEN, + }, + styleProperties: { + style: [ + "background", + "titleColor", + "subTitleColor", + "labelColor", + "margin", + "padding", + "radius", + ], + }, +}; + +const STEP: AutomatorComponentEntry = { + type: "step", + defaultLayout: { w: 24, h: 6 }, + required: ["value", "options"], + optional: ["initialValue", "direction", "size", "hidden"], + layoutProperties: { + direction: { + description: "Step bar orientation.", + enum: ["horizontal", "vertical"], + }, + size: { description: "Step density.", enum: ["default", "small"] }, + hidden: HIDDEN, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + }, + example: { + value: "1", + initialValue: "1", + options: { + optionType: "manual", + manual: { + manual: [ + { value: "1", label: "Step 1" }, + { value: "2", label: "Step 2" }, + { value: "3", label: "Step 3" }, + ], + }, + }, + }, + notes: "Step values must be numbers starting from 1.", +}; + +const RADIO: AutomatorComponentEntry = { + type: "radio", + defaultLayout: { w: 12, h: 5 }, + required: ["label", "options"], + optional: ["value", "disabled", "hidden", "layout"], + example: { + label: { text: "Priority", position: "row" }, + options: { + optionType: "manual", + manual: { + manual: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High" }, + ], + }, + }, + value: "medium", + }, + layoutProperties: { + label: LABEL_OBJECT, + disabled: DISABLED, + hidden: HIDDEN, + layout: { + description: "Group layout direction.", + enum: ["horizontal", "vertical", "autoColumns"], + }, + }, + styleProperties: { + style: [...COMMON_STYLE_KEYS], + labelStyle: [...LABEL_STYLE_KEYS], + inputFieldStyle: [...INPUT_LIKE_STYLE_KEYS, "checkedBackground", "uncheckedBackground", "uncheckedBorder", "hoverBackground"], + }, +}; + +const CHAT: AutomatorComponentEntry = { + type: "chat", + defaultLayout: { w: 12, h: 20 }, + required: [], + optional: ["chatQuery", "tableName", "placeholder"], + example: { + tableName: "LC_AI", + placeholder: "Ask anything...", + }, + notes: "AI chat component for embedding a conversational assistant in the app.", +}; + +const CHAT_BOX: AutomatorComponentEntry = { + type: "chatBox", + defaultLayout: { w: 12, h: 24 }, + required: [], + optional: ["messages", "controller", "placeholder"], + example: {}, + notes: "Chat UI for displaying messages and sending user input. Pair with chatController for realtime typing/presence.", +}; + +const CHAT_CONTROLLER: AutomatorComponentEntry = { + type: "chatController", + defaultLayout: { w: 12, h: 5 }, + required: [], + optional: ["roomId"], + example: {}, + notes: "Realtime chat controller hook. Use with chatBox for presence and typing indicators.", +}; + +export const AUTOMATOR_COMPONENTS: AutomatorComponentEntry[] = [ + TEXT, + BUTTON, + INPUT, + NUMBER_INPUT, + TEXT_AREA, + PASSWORD, + DROPDOWN, + CHECKBOX, + RADIO, + SWITCH, + SLIDER, + RATING, + DATE, + FORM, + CONTAINER, + MODAL, + DRAWER, + TABLE, + LIST_VIEW, + CARD, + TABBED_CONTAINER, + IMAGE, + VIDEO, + AVATAR, + CHART, + PROGRESS, + NAVIGATION, + TIMELINE, + STEP, + DIVIDER, + CHAT, + CHAT_BOX, + CHAT_CONTROLLER, +]; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/index.ts new file mode 100644 index 0000000000..b6df97a977 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/index.ts @@ -0,0 +1,18 @@ +import { AUTOMATOR_COMPONENTS } from "./entries"; +import type { AutomatorComponentEntry } from "./types"; + +/** Returns the curated set of components Automator is allowed to use. */ +export function getAutomatorComponents(): AutomatorComponentEntry[] { + return [...AUTOMATOR_COMPONENTS]; +} + +export const AUTOMATOR_COMPONENT_TYPES: string[] = AUTOMATOR_COMPONENTS.map( + (component) => component.type +); + +export { AUTOMATOR_COMPONENTS } from "./entries"; +export type { + AutomatorComponentEntry, + AutomatorLayoutPropertyDescriptor, + AutomatorStylePropertyMap, +} from "./types"; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/presets.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/presets.ts new file mode 100644 index 0000000000..8adf1ff28d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/presets.ts @@ -0,0 +1,144 @@ +import type { AutomatorLayoutPropertyDescriptor } from "./types"; + +// ── Style key presets ──────────────────────────────────────────────────────── +// Mirror the field lists from `comps/controls/styleControlConstants.tsx` so the +// model knows what keys it can pass to `set_style`. Keep these compact — they +// are inlined into the system prompt. + +export const COMMON_STYLE_KEYS = [ + "background", + "text", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "lineHeight", +] as const; + +export const CONTAINER_STYLE_KEYS = [ + "background", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "boxShadow", + "boxShadowColor", + "opacity", +] as const; + +export const INPUT_LIKE_STYLE_KEYS = [ + "background", + "boxShadow", + "boxShadowColor", + "text", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "placeholder", + "accent", + "validate", +] as const; + +export const LABEL_STYLE_KEYS = [ + "background", + "label", + "textTransform", + "textDecoration", + "textSize", + "textWeight", + "fontFamily", + "fontStyle", + "borderStyle", + "borderWidth", + "margin", + "padding", + "placeholder", + "accent", + "validate", +] as const; + +export const IMAGE_STYLE_KEYS = [ + "margin", + "padding", + "border", + "borderStyle", + "borderWidth", + "radius", + "opacity", + "boxShadow", + "boxShadowColor", +] as const; + +export const NAVIGATION_STYLE_KEYS = [ + "background", + "border", + "borderStyle", + "borderWidth", + "radius", + "margin", + "padding", + "accent", +] as const; + +// ── Layout property presets ───────────────────────────────────────────────── + +export const TEXT_HORIZONTAL_ALIGNMENT: AutomatorLayoutPropertyDescriptor = { + description: "Horizontal text alignment inside the component.", + enum: ["left", "center", "right", "justify"], +}; + +export const ALIGN_HORIZONTAL: AutomatorLayoutPropertyDescriptor = { + description: "Horizontal alignment.", + enum: ["left", "center", "right"], +}; + +export const VERTICAL_ALIGNMENT: AutomatorLayoutPropertyDescriptor = { + description: "Vertical alignment.", + enum: ["flex-start", "center", "flex-end"], +}; + +export const AUTO_HEIGHT: AutomatorLayoutPropertyDescriptor = { + description: "Whether the component auto-sizes its height to its content.", + enum: ["auto", "fixed"], +}; + +export const HIDDEN: AutomatorLayoutPropertyDescriptor = { + description: "Hide the component at runtime.", + type: "boolean", +}; + +export const DISABLED: AutomatorLayoutPropertyDescriptor = { + description: "Disable the component at runtime.", + type: "boolean", +}; + +export const LOADING: AutomatorLayoutPropertyDescriptor = { + description: "Show a loading indicator on the component.", + type: "boolean", +}; + +export const LABEL_OBJECT: AutomatorLayoutPropertyDescriptor = { + description: + "Field label config: { text, position: 'row'|'column', align: 'left'|'center'|'right', width: number, hidden?: boolean, tooltip?: string }.", + type: "object", + example: { text: "Email", position: "row", align: "left" }, +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/types.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/types.ts new file mode 100644 index 0000000000..a30d120d4d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/automatorComponents/types.ts @@ -0,0 +1,44 @@ +/** + * Component instructions exposed to the Automator model. + * + * These are curated references, not a dump of every Lowcoder component. If a + * component appears here, Automator is allowed to place it and has enough shape + * information to configure it with reasonable confidence. + */ +export interface AutomatorLayoutPropertyDescriptor { + /** Human-readable hint shown to the model. */ + description?: string; + /** Allowed string values when this property is a fixed enum. */ + enum?: readonly string[]; + /** Primitive type when the value is not enum-restricted. */ + type?: "string" | "number" | "boolean" | "object"; + /** Sample value the model can imitate verbatim. */ + example?: unknown; +} + +/** + * Map of style namespace -> list of style keys that can be passed to + * `set_style`. The executor expects values grouped by explicit namespace. + */ +export type AutomatorStylePropertyMap = Record; + +export interface AutomatorComponentEntry { + /** Component type used by Lowcoder action executors. */ + type: string; + /** Whether the component can have children nested under `.container`. */ + isContainer?: boolean; + /** Default grid layout (w / h) for sensible initial sizing. */ + defaultLayout: { w: number; h: number }; + /** Required property keys for `action_parameters`. */ + required: string[]; + /** Optional property keys worth knowing about. */ + optional?: string[]; + /** Realistic example `action_parameters` payload. */ + example: Record; + /** Notes the model should heed. */ + notes?: string; + /** Top-level UI / behavior properties for `set_properties`. */ + layoutProperties?: Record; + /** Style properties grouped by style namespace, used by `set_style`. */ + styleProperties?: AutomatorStylePropertyMap; +} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts new file mode 100644 index 0000000000..3d5638c676 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts @@ -0,0 +1,202 @@ +// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/editorSnapshot.ts + +import type { EditorState } from "@lowcoder-ee/comps/editorState"; +/** + * A compact, JSON-serialisable view of the live editor state. + * + * This is what we hand to the LLM as `EDITOR_CONTEXT`, so the model can + * reason about what already exists on the canvas before emitting actions + * (avoids name collisions, layout overlap, duplicated queries, etc.). + * + * Keep this small. We intentionally drop heavy fields like full property + * trees, event handlers, deep style objects — those would balloon tokens + * and rarely help the model. If a deeper inspection is needed later, + * model can ask the user. + */ + +export interface ComponentSnapshot { + name: string; + type: string; + layout?: { x?: number; y?: number; w?: number; h?: number }; + /** Trimmed property hints (label text / placeholder / button text…) */ + hints?: Record; +} + +export interface QuerySnapshot { + name: string; + type: string; +} + +export interface EditorSnapshot { + app: { + title?: string; + description?: string; + themeId?: string; + }; + canvas: { + gridColumns?: number; + gridRowHeight?: number; + gridRowCount?: number | string; + maxWidth?: string; + }; + selected: string | null; + components: ComponentSnapshot[]; + queries: QuerySnapshot[]; + tempStates: QuerySnapshot[]; + transformers: QuerySnapshot[]; +} + +const HINT_KEYS = new Set([ + "text", + "title", + "label", + "placeholder", + "src", + "value", + "options", + "columns", + "open", +]); + +function pickHints(data: Record | undefined): Record | undefined { + if (!data || typeof data !== "object") return undefined; + const out: Record = {}; + for (const k of Object.keys(data)) { + if (!HINT_KEYS.has(k)) continue; + const v = (data as any)[k]; + // truncate long strings, drop functions + if (typeof v === "string") { + out[k] = v.length > 120 ? v.slice(0, 120) + "…" : v; + } else if (typeof v === "number" || typeof v === "boolean") { + out[k] = v; + } else if (v != null && typeof v === "object") { + // shallow-copy small objects (e.g. label.text) + try { + const json = JSON.stringify(v); + if (json.length <= 200) out[k] = JSON.parse(json); + } catch { + /* ignore */ + } + } + } + return Object.keys(out).length > 0 ? out : undefined; +} + +function safe(fn: () => T, fallback: T): T { + try { + return fn(); + } catch { + return fallback; + } +} + +/** + * Build a snapshot from the live `EditorState`. Always returns a valid + * object — never throws — so the orchestrator can call this freely without + * try/catch. + */ +export function buildEditorSnapshot(editorState: EditorState | null | undefined): EditorSnapshot { + if (!editorState) { + return { + app: {}, + canvas: {}, + selected: null, + components: [], + queries: [], + tempStates: [], + transformers: [], + }; + } + + const settings = safe(() => editorState.getAppSettings(), {} as any); + + const components = safe( + () => + editorState.uiCompInfoList().map((info: any): ComponentSnapshot => { + // The layout x/y/w/h lives on the rootComp's layout map, keyed by + // the same key as `getAllUICompMap`. We don't have a direct lookup + // here without scanning, so we leave layout undefined for now and + // let the model rely on `name` + `type`. + return { + name: info.name, + type: info.type, + hints: pickHints(info.data), + }; + }), + [] + ); + + // Try to enrich with layout positions from the root grid. + safe(() => { + const uiComp: any = editorState.getUIComp(); + const compChildren = uiComp?.children?.comp?.children; + const layoutObj = compChildren?.layout?.getView?.() ?? {}; + const items = compChildren?.items?.children ?? {}; + const byName: Record = {}; + for (const [key, layout] of Object.entries(layoutObj)) { + const item: any = items[key]; + const name = item?.children?.name?.getView?.(); + if (!name) continue; + byName[name] = { + x: layout?.x, + y: layout?.y, + w: layout?.w, + h: layout?.h, + }; + } + for (const c of components) { + if (byName[c.name]) { + c.layout = byName[c.name]; + } + } + }, undefined); + + const queries: QuerySnapshot[] = safe( + () => + editorState.queryCompInfoList().map((q: any) => ({ + name: q.name, + type: q.type, + })), + [] + ); + const tempStates: QuerySnapshot[] = safe( + () => + editorState.getTempStateCompInfoList().map((q: any) => ({ + name: q.name, + type: q.type, + })), + [] + ); + const transformers: QuerySnapshot[] = safe( + () => + editorState.getTransformerCompInfoList().map((q: any) => ({ + name: q.name, + type: q.type, + })), + [] + ); + + const selected = safe(() => { + const sel = Array.from(editorState.selectedCompNames || []); + return sel.length > 0 ? sel[0] : null; + }, null); + + return { + app: { + title: settings?.title, + description: settings?.description, + themeId: settings?.themeId, + }, + canvas: { + gridColumns: settings?.gridColumns, + gridRowHeight: settings?.gridRowHeight, + gridRowCount: settings?.gridRowCount, + maxWidth: settings?.maxWidth, + }, + selected, + components, + queries, + tempStates, + transformers, + }; +} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts new file mode 100644 index 0000000000..3e4a1feec9 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts @@ -0,0 +1,34 @@ +// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/index.ts + +export { AUTOMATOR_SYSTEM_PROMPT, composeSystemMessage } from "./systemPrompt"; +export { + ACTIONS_CATALOG, + SUPPORTED_ACTIONS, + type AutomatorActionName, + type ActionCatalogEntry, +} from "./actionsCatalog"; +export { + AUTOMATOR_COMPONENTS, + AUTOMATOR_COMPONENT_TYPES, + getAutomatorComponents, + type AutomatorComponentEntry, + type AutomatorLayoutPropertyDescriptor, + type AutomatorStylePropertyMap, +} from "./automatorComponents"; +export { + buildEditorSnapshot, + type EditorSnapshot, + type ComponentSnapshot, + type QuerySnapshot, +} from "./editorSnapshot"; +export { + buildAutomatorPayload, + type LLMMessage, + type OrchestratorInput, + type OrchestratorOutput, +} from "./orchestrator"; +export { + buildToolDefinitions, + TOOL_NAME, + type OpenAIToolDefinition, +} from "./toolDefinitions"; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts new file mode 100644 index 0000000000..6caf2933bd --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts @@ -0,0 +1,80 @@ +// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/orchestrator.ts + +import type { EditorState } from "@lowcoder-ee/comps/editorState"; +import { ACTIONS_CATALOG } from "./actionsCatalog"; +import { buildEditorSnapshot, EditorSnapshot } from "./editorSnapshot"; +import { + getAutomatorComponents, + type AutomatorComponentEntry, +} from "./automatorComponents"; +import { composeSystemMessage } from "./systemPrompt"; +import { buildToolDefinitions, OpenAIToolDefinition } from "./toolDefinitions"; + +/** + * A "chat message" in the OpenAI-compatible shape (role + content). Almost + * every LLM HTTP API understands this — OpenAI, Ollama, Together, Anthropic + * (with a small mapping), Groq, etc. + * + * The Automator emits messages in this exact shape inside the `messages` + * argument that gets passed to the user-defined Lowcoder query. + */ +export interface LLMMessage { + role: "system" | "user" | "assistant"; + content: string; +} + +export interface OrchestratorInput { + /** The conversation history so far (already includes the latest user msg). */ + history: { role: "user" | "assistant"; content: string }[]; + /** Live editor state — used to build the EDITOR_CONTEXT block. */ + editorState: EditorState | null | undefined; +} + +export interface OrchestratorOutput { + /** Full message array including the synthesised system message. */ + messages: LLMMessage[]; + /** OpenAI-compatible tool definitions for function calling. */ + tools: OpenAIToolDefinition[]; + /** The composed system message string (also exposed for power users). */ + system: string; + /** The editor context snapshot (also exposed separately). */ + context: EditorSnapshot; + /** The actions catalog passed to the model. */ + actionsCatalog: typeof ACTIONS_CATALOG; + /** The curated Automator component instructions passed to the model. */ + automatorComponents: AutomatorComponentEntry[]; +} + +/** + * Build everything the user-defined Lowcoder query needs to call the LLM + * with full Lowcoder context. Pure function — does not mutate state, does + * not call the network. + */ +export function buildAutomatorPayload(input: OrchestratorInput): OrchestratorOutput { + const { history, editorState } = input; + + const context = buildEditorSnapshot(editorState); + const automatorComponents = getAutomatorComponents(); + + const system = composeSystemMessage({ + actionsCatalog: ACTIONS_CATALOG, + automatorComponents, + editorContext: context, + }); + + const tools = buildToolDefinitions(automatorComponents.map((component) => component.type)); + + const messages: LLMMessage[] = [{ role: "system", content: system }]; + for (const m of history) { + messages.push({ role: m.role, content: m.content }); + } + + return { + messages, + tools, + system, + context, + actionsCatalog: ACTIONS_CATALOG, + automatorComponents, + }; +} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts new file mode 100644 index 0000000000..46e2884b92 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts @@ -0,0 +1,210 @@ +// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/systemPrompt.ts + +/** + * Lean system prompt for the Lowcoder Automator. + * + * Design goals (vs the legacy 4.7K-line `Latest_prompt.md`): + * - Keep the *rules* short and stable (this file). + * - Inject the *allowed actions* and the *live editor context* dynamically + * at request time so the model only ever sees what is relevant. + * - Force a strict, parseable JSON output shape: `{ explanation, actions }`. + * + * The orchestrator combines this prompt with: + * - the actions catalog (what the model is allowed to emit) + * - Automator components (curated component instructions and examples) + * - the live editor snapshot (existing components, queries, canvas grid) + * + * before sending it to the user-defined Lowcoder query that proxies the LLM. + */ + +export const AUTOMATOR_SYSTEM_PROMPT = ` +You are the Lowcoder Automator — an embedded assistant inside the Lowcoder +visual app builder. Your job is to translate natural-language requests from a +human builder into structured UI actions that the runtime will execute on the +canvas. + +# How to respond + +The conversation history may include older user requests and older assistant +explanations. Treat them as background only. The latest user message is the +only instruction you should execute now. + +Do NOT replay, repeat, combine, or re-emit actions for previous turns. Previous +changes are already represented in EDITOR_CONTEXT. Return only the actions +needed for the latest user message. + +Use \`place_component\` / \`nest_component\` only when the latest user message +clearly asks to create, add, place, or insert a new component. For follow-up +edits like "make it", "set it", "change this", or "move the component", update +the existing selected or named component from EDITOR_CONTEXT instead of creating +a duplicate. + +\`nest_component\` also creates a NEW component. Do not use it to move an +existing component into a container. If the user asks to put an existing +component inside a container and there is no action for reparenting it, explain +that limitation instead of creating a duplicate component. + +You have a tool called \`execute_automator_actions\`. Use it when you are +ready to modify the canvas or supported bottom-panel resources. When the +request is ambiguous or you need clarification, respond with plain text +instead — do NOT call the tool with an empty actions array. + +If the user explicitly says "go ahead", "do it", "build it", "implement", +or similar approval after a clarification round, call the tool. + +# How to use the live context + +The system message includes a JSON block titled "EDITOR_CONTEXT". It +contains: + - canvas: grid columns, row height, max width + - selected: currently selected component name (may be null) + - components: list of UI components already on the canvas, each with + { name, type, layout: { x, y, w, h }, parent?, container? } + - queries: list of queries / temp states / transformers already defined + - theme: active theme id + +Use this context to: + - reuse component names that already exist + - place new components without overlapping existing ones + - reference existing queries instead of creating duplicates + - delete an existing query by name with \`delete_query\` when explicitly asked + - generate unique, descriptive component names + +# How to use the action catalog + +You will also see a JSON block titled "ACTIONS_CATALOG" listing the EXACT +set of actions you may emit, with their required and optional fields. The +"AUTOMATOR_COMPONENTS" block lists the curated Lowcoder component types you +may place or nest. You MUST NOT use any action or component type that is not +listed there. If something is not possible with the available Automator +components, explain why in plain text. + +# Layout rules (short) + +- Canvas grid columns default to 24. Stay within \`canvas.gridColumns\`. +- Stack components top-to-bottom by increasing \`y\`. Each unit of \`h\` is one + grid row (default ~8px). +- For nested containers (modal, drawer, listView, grid, tabbedContainer), + use a flat empty \`container: {}\` and add children with \`nest_component\` + using \`parent_component_name = ".container"\`. +- For regular containers, you may target \`container.body.0.view\`, + \`container.header\`, or \`container.footer\`. +- Populate data-driven components (table, listView, grid) with 3+ realistic + sample rows. Stringify JSON for the \`data\` field of \`table\`. + +# Styling & layout edits + +There are TWO families of UI edits, and each has its own action: + +1. **\`set_properties\`** — top-level UI / behaviour properties exposed as + direct children of the component. Use this for things controlled by the + component's own controls (alignment, autoHeight, type, label, placeholder, + options, disabled, hidden, loading, placement, …). For each component the + \`layoutProperties\` field in AUTOMATOR_COMPONENTS lists the exact keys and + their allowed values. + +2. **\`set_style\`** — basic visual / CSS-like properties living inside the + component's style namespaces (\`style\`, \`labelStyle\`, \`inputFieldStyle\`, + \`headerStyle\`, \`bodyStyle\`, …). Always group values by explicit + namespace. Do NOT pass flat style keys. + + Correct: + \`{ "style": { "background": "#1677ff", "text": "#ffffff" } }\` + + Correct for an input label: + \`{ "labelStyle": { "label": "#1677ff", "textSize": "14px" } }\` + + Incorrect: + \`{ "background": "#1677ff", "text": "#ffffff" }\` + + For each component the \`styleProperties\` field in AUTOMATOR_COMPONENTS + lists which keys live in which namespace. + + Common style-key vocabulary: + - text/colour: \`text\` (foreground), \`label\`, \`background\`, \`links\`, \`accent\` + - typography: \`textSize\`, \`textWeight\`, \`fontFamily\`, \`fontStyle\`, + \`textTransform\`, \`textDecoration\`, \`lineHeight\` + - box model: \`margin\`, \`padding\`, \`border\`, \`borderStyle\`, + \`borderWidth\`, \`radius\`, \`opacity\`, \`boxShadow\`, \`boxShadowColor\` + - input hints: \`placeholder\`, \`validate\` + +3. **\`align_component\`** — moves the COMPONENT to the left/center/right of + the canvas grid. It does NOT change text or content alignment inside the + component. For "center the text" / "right-align this label" use + \`set_properties\` with \`horizontalAlignment\`. + +# Common UI recipes + +- Center text inside a Text component: + set_properties { horizontalAlignment: "center" } +- Larger heading text: + set_style { style: { textSize: "24px", textWeight: "700", lineHeight: "1.3" } } +- Coloured primary button: + set_style { style: { background: "#1677ff", text: "#ffffff", radius: "8px", + padding: "8px 16px", textWeight: "600" } } +- Accent input border + larger label: + set_style { inputFieldStyle: { border: "#1677ff", + borderWidth: "2px", radius: "6px" } } + set_style { labelStyle: { textSize: "14px", textWeight: "600" } } +- Soft card with shadow: + set_style { style: { background: "#ffffff", radius: "12px", border: "#e5e7eb", + borderWidth: "1px", padding: "16px", + boxShadow: "0 4px 12px", boxShadowColor: "rgba(0,0,0,0.08)" } } +- Hide / disable a component: + set_properties { hidden: true } + set_properties { disabled: true } + +# UX defaults + +- Apps that show data: title (text) → filters (input/dropdown) → primary + action (button) → table/listView. Edit/Create flows go in a modal/drawer. +- Login/signup pages: image (logo) → title (text) → form on the canvas. + Avoid wrapping in modals unless explicitly requested. +- Use the simplest component that satisfies the request. Avoid over-nesting. + +# Reminders + +- All field names match the Automator component instructions exactly + (snake_case where shown). +- Every action MUST include \`action\` and (when relevant) \`component\` and + \`component_name\`. +- Component names must be unique across the app. If reusing an existing + component referenced in EDITOR_CONTEXT, use its existing name. +- Prefer the per-component \`layoutProperties\` / \`styleProperties\` listed in + AUTOMATOR_COMPONENTS over invented keys. If a property is not listed and you + are unsure it exists, ask the user instead of guessing. +`.trim(); + +/** + * Build the final system message string by combining the static prompt + * with the dynamic actions catalog and live editor snapshot. + * + * Kept as a single string so it works with any LLM (OpenAI, Claude, + * Ollama, etc.) — they all accept a single `system` message. + */ +export function composeSystemMessage(args: { + actionsCatalog: unknown; + automatorComponents: unknown; + editorContext: unknown; +}): string { + const { actionsCatalog, automatorComponents, editorContext } = args; + + return [ + AUTOMATOR_SYSTEM_PROMPT, + "", + "ACTIONS_CATALOG:", + "```json", + JSON.stringify(actionsCatalog, null, 2), + "```", + "", + "AUTOMATOR_COMPONENTS:", + "```json", + JSON.stringify(automatorComponents, null, 2), + "```", + "", + "EDITOR_CONTEXT:", + "```json", + JSON.stringify(editorContext, null, 2), + "```", + ].join("\n"); +} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts new file mode 100644 index 0000000000..176360a65e --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts @@ -0,0 +1,127 @@ +// client/packages/lowcoder/src/comps/comps/preLoadComp/actions/automator/toolDefinitions.ts + +/** + * Generates OpenAI-compatible tool (function-calling) definitions from + * the ACTIONS_CATALOG. + * + * Instead of asking the model to emit raw JSON inside its text content + * (fragile, needs custom parsing), we register a single tool — + * `execute_automator_actions` — that the model **calls** when it wants + * to mutate the canvas. The API guarantees `tool_calls[].function.arguments` + * is valid JSON, so no balanced-brace extraction or fence-stripping needed. + * + * When the model needs clarification it simply responds with text (no tool + * call), which naturally replaces the old `"actions": []` convention. + * + * The tool definition is provider-agnostic: OpenAI, Groq, Together, and + * Ollama all accept the same `tools` shape. Other providers can map this + * schema in the selected query/backend bridge. + */ + +import { ACTIONS_CATALOG } from "./actionsCatalog"; + +export interface OpenAIToolDefinition { + type: "function"; + function: { + name: string; + description: string; + parameters: Record; + }; +} + +function buildActionItemSchema(componentTypes?: string[]): Record { + return { + type: "object", + properties: { + action: { + type: "string", + enum: ACTIONS_CATALOG.map((a) => a.action), + description: "The action to perform on the canvas.", + }, + component: { + type: "string", + ...(componentTypes && componentTypes.length > 0 ? { enum: componentTypes } : {}), + description: + "Component type as registered in Lowcoder. Required for place_component and nest_component.", + }, + component_name: { + type: "string", + description: "Unique name for the component on the canvas.", + }, + query_name: { + type: "string", + description: "Name of the bottom-panel data query. Required for delete_query.", + }, + parent_component_name: { + type: "string", + description: + "Parent container path for nest_component (e.g. 'form1.container.body.0.view').", + }, + layout: { + type: "object", + properties: { + x: { type: "number", description: "Grid column position (0-based)." }, + y: { type: "number", description: "Grid row position." }, + w: { type: "number", description: "Width in grid columns." }, + h: { type: "number", description: "Height in grid rows." }, + }, + description: "Grid layout position and size.", + }, + action_parameters: { + type: "object", + description: + "Action-specific parameters (properties, styles, event config, etc.). Shape depends on the action and component type — see AUTOMATOR_COMPONENTS in the system prompt.", + }, + }, + required: ["action"], + }; +} + +/** + * Build the OpenAI `tools` array to pass alongside `messages` in the + * chat-completions request. Currently returns a single tool; the array + * wrapper keeps the door open for future per-action tools if we want + * tighter per-action schemas. + */ +export function buildToolDefinitions(componentTypes?: string[]): OpenAIToolDefinition[] { + const actionSummary = ACTIONS_CATALOG.map( + (a) => ` - ${a.action}: ${a.purpose}` + ).join("\n"); + + return [ + { + type: "function", + function: { + name: "execute_automator_actions", + description: [ + "Execute one or more Lowcoder Automator actions on the canvas.", + "Call this tool when you want to place, configure, style, move,", + "resize, delete, or otherwise modify components or queries in the app.", + "Do NOT call this tool when you need clarification — just respond", + "with text instead.", + "", + "Available actions:", + actionSummary, + ].join("\n"), + parameters: { + type: "object", + properties: { + explanation: { + type: "string", + description: + "Brief markdown summary of what you are doing and why.", + }, + actions: { + type: "array", + description: "Ordered list of actions to execute on the canvas.", + items: buildActionItemSchema(componentTypes), + }, + }, + required: ["explanation", "actions"], + }, + }, + }, + ]; +} + +export const TOOL_NAME = "execute_automator_actions"; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts index 6653b7712b..5bd20ffc88 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts @@ -1,6 +1,6 @@ import { message } from "antd"; +import merge from "lodash/merge"; import { ActionConfig, ActionExecuteParams } from "../types"; -import { getEditorComponentInfo } from "../utils"; export const configureComponentAction: ActionConfig = { key: 'configure-components', @@ -20,51 +20,35 @@ export const configureComponentAction: ActionConfig = { } }, execute: async (params: ActionExecuteParams) => { - const { actionValue: name, actionValue, actionPayload, editorState } = params; - const { component_name: selectedEditorComponent, action_parameters: compProperties } = actionPayload; - // const { onEvent, ...compProperties } = action_parameters; - // const { name, ...otherProps } = actionPayload; - - try { - // const componentInfo = getEditorComponentInfo(editorState, name); - - // if (!componentInfo) { - // message.error(`Component "${selectedEditorComponent}" not found`); - // return; - // } + const { actionPayload, editorState } = params; + const componentName = actionPayload?.component_name || actionPayload?.component || params.selectedEditorComponent; + const compProperties = actionPayload?.action_parameters; - // const { componentKey: parentKey, items } = componentInfo; - - // if (!parentKey) { - // message.error(`Parent component "${selectedEditorComponent}" not found in layout`); - // return; - // } + if (!componentName) { + message.error("No component name provided for set_properties"); + return; + } - // const parentItem = items[parentKey]; - // if (!parentItem) { - // message.error(`Parent component "${selectedEditorComponent}" not found in items`); - // return; - // } - const parentItem = editorState.getUICompByName(selectedEditorComponent); - if (!parentItem) { - message.error(`Parent component "${selectedEditorComponent}" not found in items`); + if (!compProperties || typeof compProperties !== "object") { + message.error("No properties provided for set_properties"); + return; + } + + try { + const comp = editorState.getUICompByName(componentName); + if (!comp) { + message.error(`Component "${componentName}" not found`); return; } - const itemComp = parentItem.children.comp; - const itemData = itemComp.toJsonValue(); - const config = { - ...itemData, - ...compProperties - }; + const itemComp = comp.children.comp; + const config = merge({}, itemComp.toJsonValue(), compProperties); itemComp.dispatchChangeValueAction(config); - console.log('Configuring component:', selectedEditorComponent, 'with config:', config); - message.info(`Configure action for component "${selectedEditorComponent}"`); - - // TODO: Implement actual configuration logic + message.success(`Properties updated on "${componentName}"`); } catch (error) { - message.error('Invalid configuration format'); + console.error("Error setting properties:", error); + message.error("Failed to set component properties"); } } -}; \ No newline at end of file +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts index e6489646ee..587451ddee 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts @@ -87,7 +87,7 @@ export const addComponentAction: ActionConfig = { comp: compInitialValue, }; - const currentLayout = uiComp.children.comp.children.layout.getView(); + const currentLayout = simpleContainer.children.layout.getView(); const layoutInfo = manifest?.layoutInfo || defaultLayout(selectedComponent as UICompType); let itemPos = 0; @@ -107,7 +107,7 @@ export const addComponentAction: ActionConfig = { }; await getPromiseAfterDispatch( - uiComp.children.comp.dispatch, + simpleContainer.dispatch, wrapActionExtraInfo( multiChangeAction({ layout: changeValueAction({ @@ -641,4 +641,4 @@ export const resizeComponentAction: ActionConfig = { message.error('Failed to resize component. Please try again.'); } } -}; \ No newline at end of file +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts index dbe6297a0b..3eb5841b86 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts @@ -1,113 +1,57 @@ import { message } from "antd"; +import merge from "lodash/merge"; import { ActionConfig, ActionExecuteParams } from "../types"; -import { getEditorComponentInfo } from "../utils"; - -// Fallback constant style object to apply -// This wil be replaced by a JSON object returned by the AI model. -const FALLBACK_STYLE_OBJECT = { - fontSize: "10px", - fontWeight: "500", - color: "#333333", - backgroundColor: "#ffffff", - padding: "8px", - borderRadius: "4px", - border: "1px solid #ddd" -}; export const applyStyleAction: ActionConfig = { - key: 'apply-style', - label: 'Apply style to component', - category: 'styling', + key: "apply-style", + label: "Apply style to component", + category: "styling", requiresEditorComponentSelection: true, requiresStyle: true, requiresInput: true, - inputPlaceholder: 'Enter CSS styles (JSON format)', - inputType: 'textarea', + inputPlaceholder: "Enter namespaced styles as JSON", + inputType: "json", validation: (value: string) => { - if (!value.trim()) return 'Styles are required' - else return null; + if (!value.trim()) return "Styles are required"; + try { + JSON.parse(value); + return null; + } catch { + return "Invalid JSON format"; + } }, execute: async (params: ActionExecuteParams) => { - const { selectedEditorComponent, actionValue, editorState } = params; - - if (!selectedEditorComponent || !editorState) { - message.error('Component and editor state are required'); + const { actionPayload, editorState } = params; + const componentName = + actionPayload?.component_name || params.selectedEditorComponent; + const stylePatch = { ...(actionPayload?.action_parameters || {}) }; + delete stylePatch.animationStyle; + + if (!componentName) { + message.error("No component name provided for set_style"); + return; + } + + if (!editorState) { + message.error("Editor state is required"); return; } - // A fallback constant is currently used to style the component. - // This is a temporary solution and will be removed once we integrate the AI model with the component styling. try { - let styleObject: Record = {}; - let usingFallback = false; - - try { - if (typeof actionValue === 'string') { - styleObject = JSON.parse(actionValue); - } else { - styleObject = actionValue; - } - } catch (e) { - styleObject = FALLBACK_STYLE_OBJECT; - usingFallback = true; - } - - const comp = editorState.getUICompByName(selectedEditorComponent); - + const comp = editorState.getUICompByName(componentName); if (!comp) { - message.error(`Component "${selectedEditorComponent}" not found`); + message.error(`Component "${componentName}" not found`); return; } - const appliedStyles: string[] = []; - - for (const [styleKey, styleValue] of Object.entries(styleObject)) { - try { - const { children } = comp.children.comp; - const compType = comp.children.compType.getView(); - - // This method is used in LeftLayersContent.tsx to style the component. - if (!children.style) { - if (children[compType]?.children?.style?.children?.[styleKey]) { - children[compType].children.style.children[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else if (children[compType]?.children?.[styleKey]) { - children[compType].children[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else { - console.warn(`Style property ${styleKey} not found in component ${selectedEditorComponent}`); - } - } else { - if (children.style.children?.[styleKey]) { - children.style.children[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else if (children.style[styleKey]) { - children.style[styleKey].dispatchChangeValueAction(styleValue); - appliedStyles.push(styleKey); - } else { - console.warn(`Style property ${styleKey} not found in style object`); - } - } - } catch (error) { - console.error(`Error applying style ${styleKey}:`, error); - } - } + const itemComp = comp.children.comp; + const config = merge({}, itemComp.toJsonValue(), stylePatch); + itemComp.dispatchChangeValueAction(config); - if (appliedStyles.length > 0) { - editorState.setSelectedCompNames(new Set([selectedEditorComponent]), "applyStyle"); - - if (usingFallback) { - message.success(`Applied ${appliedStyles.length} fallback style(s) to component "${selectedEditorComponent}": ${appliedStyles.join(', ')}`); - } else { - message.success(`Applied ${appliedStyles.length} style(s) to component "${selectedEditorComponent}": ${appliedStyles.join(', ')}`); - } - } else { - message.warning('No styles were applied. Check if the component supports styling.'); - } - + message.success(`Styles updated on "${componentName}"`); } catch (error) { - console.error('Error applying styles:', error); - message.error('Failed to apply styles. Please try again.'); + console.error("Error setting styles:", error); + message.error("Failed to set component styles"); } - } -}; \ No newline at end of file + }, +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/queryManagement.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/queryManagement.ts new file mode 100644 index 0000000000..52ad6dcaa2 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/queryManagement.ts @@ -0,0 +1,49 @@ +import { message } from "antd"; +import { ActionConfig, ActionExecuteParams } from "../types"; + +function getQueryName(params: ActionExecuteParams): string { + const { actionPayload, actionValue, selectedEditorComponent } = params; + const actionParameters = actionPayload?.action_parameters || {}; + return ( + actionPayload?.query_name || + actionPayload?.queryName || + actionParameters.query_name || + actionParameters.queryName || + selectedEditorComponent || + actionValue || + "" + ); +} + +export const deleteQueryAction: ActionConfig = { + key: "delete-query", + label: "Delete query", + category: "query-management", + requiresInput: false, + execute: async (params: ActionExecuteParams) => { + const { editorState } = params; + const queryName = getQueryName(params); + + if (!editorState) { + message.error("Editor state is required"); + return; + } + + if (!queryName) { + message.error("Query name is required"); + return; + } + + const queriesComp = editorState.getQueriesComp?.(); + const queryExists = queriesComp + ?.getView?.() + ?.some((query: any) => query?.children?.name?.getView?.() === queryName); + + if (!queryExists) { + message.error(`Query "${queryName}" not found`); + return; + } + + queriesComp.delete(queryName); + }, +}; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx index e9de70f2b5..1167d77013 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx @@ -176,4 +176,4 @@ export class GlobalCSSComp extends CodeTextControl implements RunAndClearable import("pages/editor/editorView"), @@ -174,13 +175,16 @@ const RootView = React.memo((props: RootViewProps) => { - {Object.keys(comp.children.queries.children).map((key) => ( -
{comp.children.queries.children[key].getView()}
- ))} - - - - + + {Object.keys(comp.children.queries.children).map((key) => ( +
{comp.children.queries.children[key].getView()}
+ ))} + + + + + {!readOnly && !isUserViewMode && !isModuleRoot && } +
diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx index e6dec7da0a..2e2dc73f26 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx @@ -509,6 +509,14 @@ export function compTablePropertyView
{comp.children.data.propertyView({ label: dataLabel, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Table data", + fieldName: "data", + fieldDescription: + "JSON array of row objects for the Table component. Generate rows with consistent object keys so table columns can be created from the data shape.", + }, })}
)} diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx index dbe4edcf88..acf68cc140 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tablePropertyView.tsx @@ -498,6 +498,14 @@ export function compTablePropertyView
{comp.children.data.propertyView({ label: dataLabel, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Table Lite data", + fieldName: "data", + fieldDescription: + "JSON array of row objects for the Table Lite component. Generate rows with consistent object keys so table columns can be created from the data shape.", + }, })}
)} diff --git a/client/packages/lowcoder/src/comps/comps/temporaryStateComp.tsx b/client/packages/lowcoder/src/comps/comps/temporaryStateComp.tsx index 42fb8727d8..b00b3fe382 100644 --- a/client/packages/lowcoder/src/comps/comps/temporaryStateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/temporaryStateComp.tsx @@ -35,6 +35,14 @@ const TemporaryStateItemCompBase = new MultiCompBuilder( label: trans("temporaryState.value"), tooltip: trans("temporaryState.valueTooltip"), placement: "bottom", + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: `${children.name.getView()}.value`, + fieldName: "value", + fieldDescription: + "Initial JSON value for this temporary state. Supports any JSON type (object, array, string, number, boolean, null).", + }, extraChildren: QueryTutorials.tempState && ( <>
{trans("temporaryState.documentationText")}
{trans("temporaryState.docLink")} diff --git a/client/packages/lowcoder/src/comps/comps/transformerListComp.tsx b/client/packages/lowcoder/src/comps/comps/transformerListComp.tsx index 0bed05a865..b3124f7434 100644 --- a/client/packages/lowcoder/src/comps/comps/transformerListComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/transformerListComp.tsx @@ -40,6 +40,14 @@ const TransformerItemCompBase = new MultiCompBuilder( placement: "bottom", styleName: "medium", width: "100%", + enableAIHelp: true, + aiHelp: { + targetKind: "javascript", + label: `${children.name.getView()}.script`, + fieldName: "script", + fieldDescription: + "Transformer body: a JavaScript function that receives upstream data and returns the transformed value.", + }, })} {QueryTutorials.transformer && ( <>
{trans("transformer.documentationText")} diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeUtils.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeUtils.tsx index 4648a4cfaa..edf158e6bf 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeUtils.tsx @@ -93,7 +93,18 @@ export function useTree(props: RecordConstructorToView>; export const treeDataPropertyView = (children: TreeCommonComp) => - children.treeData.propertyView({ label: trans("tree.treeData"), tooltip: treeDataTooltip }); + children.treeData.propertyView({ + label: trans("tree.treeData"), + tooltip: treeDataTooltip, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Tree data", + fieldName: "treeData", + fieldDescription: + "JSON array of tree nodes. Each node must include label and value, and can include children, disabled, selectable, checkable, disableCheckbox, and isLeaf.", + }, + }); export const valuePropertyView = (children: TreeCommonComp) => { return children.defaultValue.propertyView({ label: trans("tree.value") }); diff --git a/client/packages/lowcoder/src/comps/controls/actionSelector/runScript.tsx b/client/packages/lowcoder/src/comps/controls/actionSelector/runScript.tsx index 082477c18c..b60362726c 100644 --- a/client/packages/lowcoder/src/comps/controls/actionSelector/runScript.tsx +++ b/client/packages/lowcoder/src/comps/controls/actionSelector/runScript.tsx @@ -31,6 +31,14 @@ export class RunScriptAction extends RunScriptTmpAction { layout: "vertical", styleName: "medium", showLineNum: false, + enableAIHelp: true, + aiHelp: { + targetKind: "javascript", + label: "Event handler script", + fieldName: "script", + fieldDescription: + "JavaScript to run when this event fires. Has access to all Lowcoder component and query references.", + }, })} ); diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index 496c7beada..8c1d3bc416 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -300,15 +300,37 @@ export function mapOptionsControl( } ) .setControlItemData({ filterText: label }) - .setPropertyViewFn((children) => ( - <> - {children.data.propertyView({ label })} - - {children.mapData.getPropertyView()} - {OptionTip} - - - )) + .setPropertyViewFn((children) => { + const mapSchema = (() => { + try { + const json = children.mapData.toJsonValue(); + const keys = Object.keys(json as Record); + return keys.length > 0 ? keys.join(", ") : "label, value"; + } catch { + return "label, value"; + } + })(); + + return ( + <> + {children.data.propertyView({ + label, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Map mode data", + fieldName: "data", + fieldDescription: + `JSON array of objects. Each object is mapped to a component option via {{item}}. The mapped fields are: ${mapSchema}. Generate rows with keys matching those fields.`, + }, + })} + + {children.mapData.getPropertyView()} + {OptionTip} + + + ); + }) .build(); return class extends TmpOptionControl { diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 8edfee318f..ff70e68a46 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -2530,9 +2530,7 @@ export const ChatMessagesStyle = [ { name: "userMessageBackground", label: trans("style.userMessageBackground"), - depTheme: "primary", - depType: DEP_TYPE.SELF, - transformer: toSelf, + color: "#e5e7eb", }, { name: "userMessageText", @@ -2556,23 +2554,10 @@ export const ChatMessagesStyle = [ ] as const; export const ChatInputStyle = [ - { - name: "inputBackground", - label: trans("style.inputBackground"), - color: "#ffffff", - }, { name: "inputText", label: trans("style.inputText"), - depName: "inputBackground", - depType: DEP_TYPE.CONTRAST_TEXT, - transformer: contrastText, - }, - { - name: "inputBorder", - label: trans("style.inputBorder"), - depName: "inputBackground", - transformer: backgroundToBorder, + color: "#1f2937", }, ] as const; diff --git a/client/packages/lowcoder/src/comps/queries/esQuery.tsx b/client/packages/lowcoder/src/comps/queries/esQuery.tsx index 902332bbdb..a9146119de 100644 --- a/client/packages/lowcoder/src/comps/queries/esQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/esQuery.tsx @@ -278,6 +278,15 @@ const EsQueryPropertyView = (props: { label: "Body", placement: "bottom", styleName: "medium", + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "Elasticsearch request body", + queryType: "Elasticsearch", + fieldName: "dsl", + fieldDescription: + "Elasticsearch Query DSL as JSON. Use query, aggs, sort, size, _source, and other Elasticsearch request body parameters.", + }, })} ); diff --git a/client/packages/lowcoder/src/comps/queries/httpQuery/alasqlQuery.tsx b/client/packages/lowcoder/src/comps/queries/httpQuery/alasqlQuery.tsx index 0c2a4bf07e..3a0fe8a064 100644 --- a/client/packages/lowcoder/src/comps/queries/httpQuery/alasqlQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/httpQuery/alasqlQuery.tsx @@ -130,6 +130,15 @@ const PropertyView = (props: { comp: InstanceType; datasourc styleName: "medium", language: "sql", enableMetaCompletion: true, + enableAIHelp: true, + aiHelp: { + targetKind: "sql", + label: "AlaSQL query", + queryType: "AlaSQL", + fieldName: "sql", + fieldDescription: + "AlaSQL SQL query for in-browser data. Supports SELECT, INSERT, UPDATE, DELETE, JOIN, and AlaSQL-specific extensions.", + }, })} diff --git a/client/packages/lowcoder/src/comps/queries/httpQuery/graphqlQuery.tsx b/client/packages/lowcoder/src/comps/queries/httpQuery/graphqlQuery.tsx index 2ccba05399..479eab163b 100644 --- a/client/packages/lowcoder/src/comps/queries/httpQuery/graphqlQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/httpQuery/graphqlQuery.tsx @@ -3,7 +3,7 @@ import { Input } from "components/Input"; import { KeyValueList } from "components/keyValueList"; import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query"; import { simpleMultiComp } from "comps/generators/multi"; -import { ReactNode } from "react"; +import { ReactNode, useContext } from "react"; import { JSONValue } from "../../../util/jsonTypes"; import { keyValueListControl } from "../../controls/keyValueListControl"; import { ParamsJsonControl, ParamsStringControl } from "../../controls/paramsControl"; @@ -16,6 +16,7 @@ import { HttpPathPropertyView, } from "./httpQueryConstants"; import { SimpleNameComp } from "@lowcoder-ee/comps/comps/simpleNameComp"; +import { CompNameContext } from "comps/editorState"; interface VariablesControlParams { // variables: string[]; todo support parse variables @@ -108,6 +109,7 @@ function parseVariables(value: JSONValue): string[] { const PropertyView = (props: { comp: InstanceType; datasourceId: string }) => { const { comp } = props; const { children } = comp; + const queryName = useContext(CompNameContext); return ( <> @@ -119,6 +121,17 @@ const PropertyView = (props: { comp: InstanceType; datasour {children.body.propertyView({ styleName: "medium", width: "100%", + enableAIHelp: true, + aiHelp: { + targetKind: "component-field", + label: queryName ? `${queryName}.graphql` : "GraphQL query", + queryType: "GRAPHQL", + queryName, + fieldName: "body", + fieldDescription: + "GraphQL query document. Help write, explain, or improve the GraphQL operation and variables usage.", + targetId: queryName ? `${queryName}.graphql` : undefined, + }, })} diff --git a/client/packages/lowcoder/src/comps/queries/httpQuery/httpQuery.tsx b/client/packages/lowcoder/src/comps/queries/httpQuery/httpQuery.tsx index 2fd50921eb..4f45ff77b6 100644 --- a/client/packages/lowcoder/src/comps/queries/httpQuery/httpQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/httpQuery/httpQuery.tsx @@ -1,9 +1,11 @@ import { Dropdown, ValueFromOption } from "components/Dropdown"; import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query"; +import { CompNameContext } from "comps/editorState"; import { valueComp, withDefault } from "comps/generators"; import { trans } from "i18n"; import { includes } from "lodash"; import { CompAction, MultiBaseComp } from "lowcoder-core"; +import { useContext } from "react"; import { keyValueListControl } from "../../controls/keyValueListControl"; import { ParamsJsonControl, ParamsStringControl } from "../../controls/paramsControl"; import { withTypeAndChildrenAbstract } from "../../generators/withType"; @@ -114,8 +116,11 @@ type ChildrenType = InstanceType extends MultiBaseComp { - switch (children.bodyType.getView() as BodyTypeValue) { +const showBodyConfig = (children: ChildrenType, queryName?: string) => { + const bodyType = children.bodyType.getView() as BodyTypeValue; + const method = children.httpMethod.getView(); + + switch (bodyType) { case "application/x-www-form-urlencoded": return children.bodyFormData.propertyView({}); case "multipart/form-data": @@ -129,7 +134,21 @@ const showBodyConfig = (children: ChildrenType) => { }); case "application/json": case "text/plain": - return children.body.propertyView({ styleName: "medium", width: "100%" }); + return children.body.propertyView({ + styleName: "medium", + width: "100%", + language: bodyType === "application/json" ? "json" : undefined, + enableAIHelp: true, + aiHelp: { + targetKind: bodyType === "application/json" ? "json" : "component-field", + label: queryName ? `${queryName}.body` : "HTTP request body", + queryType: "HTTP", + queryName, + fieldName: "body", + fieldDescription: `HTTP ${method} request body (${bodyType}). Help generate, explain, or improve the request payload.`, + targetId: queryName ? `${queryName}.body` : undefined, + }, + }); default: return <>; } @@ -144,6 +163,7 @@ const HttpQueryPropertyView = (props: { }) => { const { comp, supportHttpMethods, supportBodyTypes } = props; const { children, dispatch } = comp; + const queryName = useContext(CompNameContext); return ( <> @@ -201,7 +221,7 @@ const HttpQueryPropertyView = (props: { - {showBodyConfig(children)} + {showBodyConfig(children, queryName)} ); diff --git a/client/packages/lowcoder/src/comps/queries/httpQuery/sseHttpQuery.tsx b/client/packages/lowcoder/src/comps/queries/httpQuery/sseHttpQuery.tsx index 2271f582ef..e210f07d80 100644 --- a/client/packages/lowcoder/src/comps/queries/httpQuery/sseHttpQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/httpQuery/sseHttpQuery.tsx @@ -1,10 +1,12 @@ // SSEHTTPQUERY.tsx -import { Dropdown, ValueFromOption } from "components/Dropdown"; -import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query"; -import { valueComp, withDefault } from "comps/generators"; -import { trans } from "i18n"; -import { includes } from "lodash"; -import { CompAction, MultiBaseComp } from "lowcoder-core"; +import { Dropdown, ValueFromOption } from "components/Dropdown"; +import { QueryConfigItemWrapper, QueryConfigLabel, QueryConfigWrapper } from "components/query"; +import { CompNameContext } from "comps/editorState"; +import { valueComp, withDefault } from "comps/generators"; +import { trans } from "i18n"; +import { includes } from "lodash"; +import { CompAction, MultiBaseComp } from "lowcoder-core"; +import { useContext } from "react"; import { keyValueListControl } from "../../controls/keyValueListControl"; import { ParamsJsonControl, ParamsStringControl } from "../../controls/paramsControl"; import { withTypeAndChildrenAbstract } from "../../generators/withType"; @@ -115,8 +117,11 @@ type ChildrenType = InstanceType extends MultiBaseComp { - switch (children.bodyType.getView() as BodyTypeValue) { +const showBodyConfig = (children: ChildrenType, queryName?: string) => { + const bodyType = children.bodyType.getView() as BodyTypeValue; + const method = children.httpMethod.getView(); + + switch (bodyType) { case "application/x-www-form-urlencoded": return children.bodyFormData.propertyView({}); case "multipart/form-data": @@ -128,9 +133,23 @@ const showBodyConfig = (children: ChildrenType) => { example: "{{ {data: file1.value[0], name: file1.files[0].name} }}", }), }); - case "application/json": - case "text/plain": - return children.body.propertyView({ styleName: "medium", width: "100%" }); + case "application/json": + case "text/plain": + return children.body.propertyView({ + styleName: "medium", + width: "100%", + language: bodyType === "application/json" ? "json" : undefined, + enableAIHelp: true, + aiHelp: { + targetKind: bodyType === "application/json" ? "json" : "component-field", + label: queryName ? `${queryName}.body` : "SSE HTTP request body", + queryType: "SSE_HTTP", + queryName, + fieldName: "body", + fieldDescription: `Server-Sent Events HTTP ${method} request body (${bodyType}). Help generate, explain, or improve the streaming request payload.`, + targetId: queryName ? `${queryName}.body` : undefined, + }, + }); default: return <>; } @@ -143,8 +162,9 @@ const SseHttpQueryPropertyView = (props: { supportHttpMethods?: HttpMethodValue[]; supportBodyTypes?: BodyTypeValue[]; }) => { - const { comp, supportHttpMethods, supportBodyTypes } = props; - const { children, dispatch } = comp; + const { comp, supportHttpMethods, supportBodyTypes } = props; + const { children, dispatch } = comp; + const queryName = useContext(CompNameContext); return ( <> @@ -204,10 +224,10 @@ const SseHttpQueryPropertyView = (props: { }} /> - - - {showBodyConfig(children)} - + + + {showBodyConfig(children, queryName)} + Streaming Options @@ -219,4 +239,4 @@ const SseHttpQueryPropertyView = (props: { ); -}; \ No newline at end of file +}; diff --git a/client/packages/lowcoder/src/comps/queries/jsQuery.tsx b/client/packages/lowcoder/src/comps/queries/jsQuery.tsx index 24c3689157..c92f18004a 100644 --- a/client/packages/lowcoder/src/comps/queries/jsQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/jsQuery.tsx @@ -6,6 +6,8 @@ import { DocLink } from "lowcoder-design"; import { getGlobalSettings } from "comps/utils/globalSettings"; import { trans } from "i18n"; import { QUERY_EXECUTION_ERROR, QUERY_EXECUTION_OK } from "../../constants/queryConstants"; +import { useContext } from "react"; +import { CompNameContext } from "comps/editorState"; export const JSQuery = (function () { const childrenMap = { @@ -34,19 +36,31 @@ export const JSQuery = (function () { } }; }) - .setPropertyViewFn((children) => { - return ( - <> - {children.script.propertyView({ - placeholder: "return 1 + 1", - placement: "bottom", - styleName: "medium", - })} - {QueryTutorials.js && ( - {trans("query.jsQueryDocLink")} - )} - - ); - }) + .setPropertyViewFn((children) => ) .build(); })(); + +function JSQueryPropertyView({ children }: { children: any }) { + const queryName = useContext(CompNameContext); + return ( + <> + {children.script.propertyView({ + placeholder: "return 1 + 1", + placement: "bottom", + styleName: "medium", + language: "javascript", + enableAIHelp: true, + aiHelp: { + targetKind: "javascript", + label: queryName ? `${queryName}.script` : "JS Query script", + queryType: "JS", + queryName, + targetId: queryName ? `${queryName}.script` : undefined, + }, + })} + {QueryTutorials.js && ( + {trans("query.jsQueryDocLink")} + )} + + ); +} diff --git a/client/packages/lowcoder/src/comps/queries/mongoQuery.tsx b/client/packages/lowcoder/src/comps/queries/mongoQuery.tsx index 6a2b3612db..3492141df0 100644 --- a/client/packages/lowcoder/src/comps/queries/mongoQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/mongoQuery.tsx @@ -42,6 +42,15 @@ const QueryField = withPropertyViewFn(ParamsJsonControl, (comp) => }`, styleName: "medium", enableMetaCompletion: true, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "MongoDB query filter", + queryType: "MongoDB", + fieldName: "query", + fieldDescription: + "MongoDB query/filter document. Use MongoDB query operators ($gte, $in, $regex, etc.) to match documents.", + }, }) ); @@ -99,6 +108,15 @@ const CommandMap: Record< }]`, styleName: "medium", enableMetaCompletion: true, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "MongoDB insert documents", + queryType: "MongoDB", + fieldName: "documents", + fieldDescription: + "JSON array of documents to insert into the collection.", + }, }) ), }), @@ -113,6 +131,15 @@ const CommandMap: Record< }`, styleName: "medium", enableMetaCompletion: true, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "MongoDB update operators", + queryType: "MongoDB", + fieldName: "update", + fieldDescription: + "MongoDB update document. Use update operators ($set, $inc, $push, $unset, etc.) to modify matched documents.", + }, }) ), limit: LimitDropdownField, @@ -146,6 +173,15 @@ const CommandMap: Record< ]`, styleName: "medium", enableMetaCompletion: true, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "MongoDB aggregation pipeline", + queryType: "MongoDB", + fieldName: "arrayPipelines", + fieldDescription: + "MongoDB aggregation pipeline stages as a JSON array. Use stages like $match, $group, $sort, $project, $lookup, $unwind, etc.", + }, }) ), limit: LimitInputField, @@ -161,6 +197,15 @@ const CommandMap: Record< ]`, styleName: "medium", enableMetaCompletion: true, + enableAIHelp: true, + aiHelp: { + targetKind: "json", + label: "MongoDB raw command", + queryType: "MongoDB", + fieldName: "command", + fieldDescription: + "Raw MongoDB command as JSON. Supports any valid MongoDB database command.", + }, }) ), }), diff --git a/client/packages/lowcoder/src/comps/queries/redisQuery.tsx b/client/packages/lowcoder/src/comps/queries/redisQuery.tsx index 530553c7ef..0d8b3b142b 100644 --- a/client/packages/lowcoder/src/comps/queries/redisQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/redisQuery.tsx @@ -149,6 +149,15 @@ const CommandMap: Record< placement: "bottom", placeholder: `INCR counter`, styleName: "medium", + enableAIHelp: true, + aiHelp: { + targetKind: "component-field", + label: "Redis raw command", + queryType: "Redis", + fieldName: "command", + fieldDescription: + "Raw Redis command string (e.g. GET key, SET key value, INCR counter, HGETALL hash). Supports any Redis CLI command.", + }, }) ), }), diff --git a/client/packages/lowcoder/src/comps/queries/sqlQuery/SQLQuery.tsx b/client/packages/lowcoder/src/comps/queries/sqlQuery/SQLQuery.tsx index b451cd0e1d..6993f9e099 100644 --- a/client/packages/lowcoder/src/comps/queries/sqlQuery/SQLQuery.tsx +++ b/client/packages/lowcoder/src/comps/queries/sqlQuery/SQLQuery.tsx @@ -23,6 +23,7 @@ import { ColumnNameDropdown } from "./columnNameDropdown"; import React, { useContext } from "react"; import { QueryContext } from "util/context/QueryContext"; import SupaDemoDisplay from "comps/utils/supademoDisplay"; +import { CompNameContext } from "comps/editorState"; const AllowMultiModifyComp = withPropertyViewFn(BoolPureControl, (comp) => comp.propertyView({ @@ -172,6 +173,7 @@ const regexp = new RegExp("(\\s|^)(update|insert|delete|drop)(\\s|$)", "i"); const SQLQueryPropertyView = (props: { comp: InstanceType }) => { const { children } = props.comp; const context = useContext(QueryContext); + const queryName = useContext(CompNameContext); return ( <> @@ -182,6 +184,15 @@ const SQLQueryPropertyView = (props: { comp: InstanceType }) => styleName: "medium", language: "sql", enableMetaCompletion: true, + enableAIHelp: true, + aiHelp: { + targetKind: "sql", + label: queryName ? `${queryName}.sql` : "SQL Query", + datasourceId: context?.datasourceId, + queryType: context?.resourceType, + queryName, + targetId: queryName ? `${queryName}.sql` : undefined, + }, }) ) : ( <> diff --git a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx index a7693b7bc8..ea45ffa359 100644 --- a/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx +++ b/client/packages/lowcoder/src/pages/editor/bottom/BottomPanel.tsx @@ -1,20 +1,27 @@ import { BottomContent } from "pages/editor/bottom/BottomContent"; -import { ResizableBox, ResizeCallbackData } from "react-resizable"; -import styled from "styled-components"; -import * as React from "react"; -import { useMemo, useState } from "react"; -import { getPanelStyle, savePanelStyle } from "util/localStorageUtil"; +import { ResizableBox, ResizeCallbackData } from "react-resizable"; +import styled from "styled-components"; +import * as React from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { + getPanelStyle, + getSelectedAIQueryName, + savePanelStyle, + saveSelectedAIQueryName, +} from "util/localStorageUtil"; import { BottomResultPanel } from "../../../components/resultPanel/BottomResultPanel"; import { AppState } from "../../../redux/reducers"; import { getUser } from "../../../redux/selectors/usersSelectors"; -import { connect } from "react-redux"; -import { Layers } from "constants/Layers"; -import Flex from "antd/es/flex"; -import type { MenuProps } from 'antd/es/menu'; -import { BuildOutlined, DatabaseOutlined } from "@ant-design/icons"; -import Menu from "antd/es/menu/menu"; -import { AIGenerate } from "lowcoder-design"; -import { ChatPanel } from "@lowcoder-ee/comps/comps/chatComp/components/ChatPanel"; +import { connect } from "react-redux"; +import { Layers } from "constants/Layers"; +import Flex from "antd/es/flex"; +import type { MenuProps } from 'antd/es/menu'; +import { DatabaseOutlined } from "@ant-design/icons"; +import Menu from "antd/es/menu/menu"; +import Select from "antd/es/select"; +import { AIGenerate } from "lowcoder-design"; +import { ChatPanel } from "@lowcoder-ee/comps/comps/chatComp/components/ChatPanel"; +import { EditorContext } from "comps/editorState"; type MenuItem = Required['items'][number]; @@ -34,11 +41,12 @@ const StyledResizableBox = styled(ResizableBox)` } `; -const StyledMenu = styled(Menu)` - width: 40px; - padding: 6px 0; - - .ant-menu-item { +const StyledMenu = styled(Menu)` + flex: 0 0 40px; + width: 40px; + padding: 6px 0; + + .ant-menu-item { height: 30px; line-height: 30px; } @@ -60,7 +68,31 @@ const ChatTitle = styled.h3` color: #222222; `; -const preventDefault = (e: any) => { +const QuerySelectorWrapper = styled.div` + display: flex; + align-items: center; + gap: 12px; +`; + +const QueryLabel = styled.span` + font-size: 12px; + color: #8b8fa3; + white-space: nowrap; +`; + +const PanelBody = styled.div` + display: flex; + height: 100%; + min-width: 0; +`; + +const PanelContent = styled.div` + flex: 1 1 0; + min-width: 0; + height: 100%; +`; + +const preventDefault = (e: any) => { e.preventDefault(); }; @@ -81,9 +113,26 @@ function Bottom(props: any) { setBottomHeight(data.size.height); removeListener(); }; + + const [bottomHeight, setBottomHeight] = useState(panelStyle.bottom.h); + const [currentOption, setCurrentOption] = useState("data"); + const [selectedQuery, setSelectedQuery] = useState(() => getSelectedAIQueryName()); + + const editorState = useContext(EditorContext); + + useEffect(() => { + if (currentOption === "ai") { + setSelectedQuery(getSelectedAIQueryName()); + } + }, [currentOption]); - const [bottomHeight, setBottomHeight] = useState(panelStyle.bottom.h); - const [currentOption, setCurrentOption] = useState("data"); + const queryOptions = useMemo(() => { + if (!editorState) return []; + return editorState.queryCompInfoList().map((info) => ({ + label: info.name, + value: info.name, + })); + }, [editorState]); const items: MenuItem[] = [ { key: 'data', icon: , label: 'Data Queries' }, @@ -98,36 +147,54 @@ function Bottom(props: any) { height={panelStyle.bottom.h} resizeHandles={["n"]} minConstraints={[680, 285]} - maxConstraints={[Infinity, clientHeight - 48 - 40]} // - app_header - right_header + maxConstraints={[Infinity, clientHeight - 48 - 40]} onResizeStart={addListener} onResizeStop={resizeStop} > - - + { - setCurrentOption(key); - }} - /> - { currentOption === "data" && } - { currentOption === "ai" && ( - - - Lowcoder AI Assistant - - - - )} - - + setCurrentOption(key); + }} + /> + + {currentOption === "data" ? ( + + ) : ( + + + Lowcoder Automator + + Query: +