diff --git a/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml b/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml new file mode 100644 index 000000000..13f64a674 --- /dev/null +++ b/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml @@ -0,0 +1,55 @@ +# Builds client/packages/lowcoder-sdk-webpack-bundle and deploys its dist/ folder to Netlify. +# +# Deploy uses --no-build so Netlify CLI does not run the site UI "build command" (e.g. expo). +# The webpack bundle is built in the prior CI step. +# +# Repository secrets (Netlify: Site settings → General → Site details → Site ID; +# User settings → Applications → Personal access tokens): +# NETLIFY_AUTH_TOKEN — Netlify personal access token +# NETLIFY_SITE_ID — Site API ID for the Netlify site + +name: Deploy SDK Webpack Bundle to Netlify + +on: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: deploy-sdk-webpack-netlify-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.x + cache: yarn + cache-dependency-path: client/yarn.lock + + - name: Install dependencies + uses: borales/actions-yarn@v4.2.0 + with: + cmd: install + dir: client + + - name: Build lowcoder-sdk-webpack-bundle + uses: borales/actions-yarn@v4.2.0 + with: + cmd: workspace lowcoder-sdk-webpack-bundle build + dir: client + + - name: Deploy dist to Netlify + working-directory: client/packages/lowcoder-sdk-webpack-bundle + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SDK_SITE_ID }} + run: npx --yes netlify-cli deploy --prod --dir=dist --no-build diff --git a/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx b/client/packages/lowcoder-comps/src/comps/barChartComp/barChartPropertyView.tsx index 5f3d41879..4c2e78e6d 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 44eda032b..b5a003a90 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 b6694e910..33f1fe78a 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 058a35d8d..cfa91b27f 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 754bab376..fe6722c5a 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 b3b0f61aa..0f6cad18c 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 cab358d44..1e25310e0 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 51ab7e517..f7159560f 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 aefed93a7..ac16783c4 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 bbcebf358..e8f5ce15c 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 5a67d8ecf..ab8b8a98c 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 ea143725c..5cbca382d 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 3106c44d7..7e00d1412 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 626a491c0..2f0363020 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 b59b6f3df..6f1f8dfe8 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 396217531..af820bc65 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 77ac49eb0..6ad26f52c 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 22564c189..6ebdeaf51 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 f0e5b9986..0533af43c 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 f4cc5ca43..54b5eaff7 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 2632f4349..e1c4797a1 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 7c2899bfc..542327bee 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 02630eefe..44fedbc32 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/ThemeSettingsSelector.tsx b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx index f2004d43a..67cbb9c20 100644 --- a/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx +++ b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx @@ -254,7 +254,7 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { }; const gridPaddingInputBlur = (padding: string) => { - let result = 20; + let result = 0; if (padding !== '') { result = Number(padding); } 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 000000000..e44a73e95 --- /dev/null +++ b/client/packages/lowcoder/src/components/ai-helper/AIHelperModal.tsx @@ -0,0 +1,214 @@ +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 { 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]); + + 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 a45e5fe14..000000000 --- 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 d2434babf..000000000 --- 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 945783c69..000000000 --- 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 3ea69fafd..8978d0d2f 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,64 @@ // 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, + ...payload, + }; + + 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 +66,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 d24e0ce84..0a8035a30 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 000000000..c5c6c9353 --- /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 resultActions = (toolPart.result as any)?.actions; + return Array.isArray(resultActions) ? resultActions : []; +}; 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 9ff22d436..3e4c23e1c 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 c641dbbef..ae2a03358 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/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index c6aae7ad2..e26bf4ab6 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -281,9 +281,6 @@ const DatePickerTmpCmp = new UICompBuilder(childrenMap, (props) => { props.onEvent ); }} - onPanelChange={() => { - handleDateChange("", props.value.onChange, noop); - }} onFocus={() => props.onEvent("focus")} onBlur={() => props.onEvent("blur")} suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx index 5a7e188c8..9fcbc09b7 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx @@ -55,7 +55,7 @@ const StyledAntdSelect = styled(AntdSelect)` export interface DataUIViewProps extends DateCompViewProps { value?: DatePickerProps['value']; onChange: DatePickerProps['onChange']; - onPanelChange: () => void; + onPanelChange?: () => void; onClickDateTimeZone:(value:any)=>void; tabIndex?: number; $disabledStyle?: DisabledInputStyleType; diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index 8ae653ffa..bd4016c16 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -11,11 +11,11 @@ import { AppSelectComp } from "comps/comps/layout/appSelectComp"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; import { ConstructorToComp, ConstructorToDataType } from "lowcoder-core"; import { CanvasContainer } from "comps/comps/gridLayoutComp/canvasView"; -import { CanvasContainerID } from "constants/domLocators"; -import { PreviewContainerID } from "constants/domLocators"; +import { CanvasContainerID, PreviewContainerID } from "constants/domLocators"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { Layers } from "constants/Layers"; import { ExternalEditorContext } from "util/context/ExternalEditorContext"; +import { EditorContext } from "comps/editorState"; import { default as Skeleton } from "antd/es/skeleton"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; @@ -47,6 +47,21 @@ const TabBarItem = React.lazy(() => ); const EventOptions = [clickEvent] as const; +/** Mobile nav editor: tab bar uses position:absolute bottom; this root is the containing block */ +const MobileNavCanvasRoot = styled(CanvasContainer)` + position: relative; +`; + +/** Strip shared EditorContainer defaults (16px padding + scrollbar-gutter: stable) for mobile nav */ +const MobileNavEditorContainer = styled(EditorContainer)` + padding: 0; + padding-right: 0; + scrollbar-gutter: auto; + overflow-x: auto; + overflow-y: auto; + background: transparent; +`; + const AppViewContainer = styled.div` position: absolute; width: 100%; @@ -221,17 +236,17 @@ const TabBarWrapper = styled.div<{ $readOnly: boolean, $canvasBg: string, $tabBarHeight: string, - $maxWidth: number, $verticalAlignment: string; }>` + box-sizing: border-box; max-width: inherit; background: ${(props) => (props.$canvasBg)}; margin: 0 auto; - position: fixed; + position: ${(props) => (props.$readOnly ? "fixed" : "absolute")}; bottom: 0; left: 0; right: 0; - width: ${(props) => props.$readOnly ? "100%" : `${props.$maxWidth - 30}px`}; + width: 100%; z-index: ${Layers.tabBar}; padding-bottom: env(safe-area-inset-bottom, 0); @@ -389,7 +404,6 @@ function convertTreeData(data: any) { function TabBarView(props: TabBarProps & { tabBarHeight: string; - maxWidth: number; verticalAlignment: string; showSeparator: boolean; navIconSize: string; @@ -404,7 +418,6 @@ function TabBarView(props: TabBarProps & { $readOnly={props.readOnly} $canvasBg={canvasBg} $tabBarHeight={props.tabBarHeight} - $maxWidth={props.maxWidth} $verticalAlignment={props.verticalAlignment} > { const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas; const onEvent = comp.children.onEvent.getView(); + // Pull app-level Theme / Canvas Settings (managed via the left-sidebar + // "Canvas" pane and shared with normal apps + modules). Mobile nav already + // owns its own maxWidth + grid behaviour, so we only consume the + // background + padding subset here. + const editorState = useContext(EditorContext); + const appSettings = editorState?.getAppSettings(); + const canvasBg = appSettings?.gridBg; + const canvasBgImage = appSettings?.gridBgImage; + const canvasBgImageRepeat = appSettings?.gridBgImageRepeat || "no-repeat"; + const canvasBgImageSize = appSettings?.gridBgImageSize || "cover"; + const canvasBgImagePosition = appSettings?.gridBgImagePosition || "center"; + const canvasBgImageOrigin = appSettings?.gridBgImageOrigin || "padding-box"; + const canvasPaddingX = appSettings?.gridPaddingX ?? 0; + const canvasPaddingY = appSettings?.gridPaddingY ?? 0; + + const canvasBackgroundStyle: React.CSSProperties = { + background: "#FFFFFF", + }; + if (canvasBg) { + canvasBackgroundStyle.background = canvasBg; + } + if (canvasBgImage) { + canvasBackgroundStyle.backgroundImage = `url('${canvasBgImage}')`; + canvasBackgroundStyle.backgroundRepeat = canvasBgImageRepeat; + canvasBackgroundStyle.backgroundSize = canvasBgImageSize; + canvasBackgroundStyle.backgroundPosition = canvasBgImagePosition; + canvasBackgroundStyle.backgroundOrigin = canvasBgImageOrigin; + } + const canvasContentPadding = `${canvasPaddingY}px ${canvasPaddingX}px`; + const getContainer = useCallback(() => document.querySelector(`#${PreviewContainerID}`) || document.querySelector(`#${CanvasContainerID}`) || @@ -702,7 +745,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { currentTab.children.app.getView()) || ( ); } @@ -712,7 +755,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { currentTab.children.action.getView()) || ( ) }, [tabIndex, tabViews, dataOptionType]); @@ -769,7 +812,6 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { tabItemActiveStyle={navItemActiveStyle} tabBarHeight={tabBarHeight} navIconSize={navIconSize} - maxWidth={maxWidth} verticalAlignment={verticalAlignment} showSeparator={showSeparator} /> @@ -870,8 +912,12 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { if (readOnly) { return ( - - {appView} + + {appView} {menuMode === MobileMode.Hamburger ? ( <> {hamburgerButton} @@ -885,8 +931,12 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { } return ( - - {appView} + + {appView} {menuMode === MobileMode.Hamburger ? ( <> {hamburgerButton} @@ -895,7 +945,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { ) : ( tabBarView )} - + ); }); diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 4a7e2b355..66f23635c 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -6,6 +6,7 @@ import MainContent from "components/layout/MainContent"; import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp"; import { menuPropertyView } from "comps/comps/navComp/components/MenuItemList"; import { registerLayoutMap } from "comps/comps/uiComp"; +import { EditorContext } from "comps/editorState"; import { MultiCompBuilder, withDefault, withViewFn } from "comps/generators"; import { withDispatchHook } from "comps/generators/withDispatchHook"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; @@ -14,7 +15,7 @@ import { TopHeaderHeight } from "constants/style"; import { Section, controlItem, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; import { StringControl, jsonControl } from "comps/controls/codeControl"; @@ -381,6 +382,21 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const dataOptionType = comp.children.dataOptionType.getView(); const onEvent = comp.children.onEvent.getView(); + // Pull app-level Theme / Canvas Settings (managed via the left-sidebar + // "Canvas" pane and shared with normal apps + modules). For aggregation + // apps the grid sizing fields are intentionally hidden in the settings UI; + // we only consume the background + padding subset here. + const editorState = useContext(EditorContext); + const appSettings = editorState?.getAppSettings(); + const canvasBg = appSettings?.gridBg; + const canvasBgImage = appSettings?.gridBgImage; + const canvasBgImageRepeat = appSettings?.gridBgImageRepeat || "no-repeat"; + const canvasBgImageSize = appSettings?.gridBgImageSize || "cover"; + const canvasBgImagePosition = appSettings?.gridBgImagePosition || "center"; + const canvasBgImageOrigin = appSettings?.gridBgImageOrigin || "padding-box"; + const canvasPaddingX = appSettings?.gridPaddingX ?? 0; + const canvasPaddingY = appSettings?.gridPaddingY ?? 0; + // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); @@ -685,8 +701,25 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { /> ); + // Build canvas background style (color + optional image), driven by the + // shared app-level Canvas Settings. + const canvasBackgroundStyle: React.CSSProperties = {}; + if (canvasBg) { + canvasBackgroundStyle.background = canvasBg; + } + if (canvasBgImage) { + canvasBackgroundStyle.backgroundImage = `url('${canvasBgImage}')`; + canvasBackgroundStyle.backgroundRepeat = canvasBgImageRepeat; + canvasBackgroundStyle.backgroundSize = canvasBgImageSize; + canvasBackgroundStyle.backgroundPosition = canvasBgImagePosition; + canvasBackgroundStyle.backgroundOrigin = canvasBgImageOrigin; + } + let content = ( - + {(navPosition === 'top') && (
{ navMenu } @@ -697,7 +730,15 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { {navMenu} )} - {pageView} + + {pageView} + {(navPosition === 'bottom') && (