You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ju...@apache.org on 2025/05/21 17:58:45 UTC

(superset) 01/01: [DRAFT] chart assistant using LLM

This is an automated email from the ASF dual-hosted git repository.

justinpark pushed a commit to branch chart-assistant--airbnb
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 037be49185dbf982d549f0bb97d971b2246c1347
Author: justinpark <ju...@gmail.com>
AuthorDate: Wed May 21 10:58:23 2025 -0700

    [DRAFT] chart assistant using LLM
---
 .../src/shared-controls/sharedControls.tsx         |   6 +
 .../src/chart/components/SuperChartCore.tsx        |  51 +++-
 .../src/BoxPlot/EchartsBoxPlot.tsx                 |  10 +-
 .../src/BoxPlot/controlPanel.ts                    |   1 +
 .../src/BoxPlot/transformProps.ts                  |  10 +-
 .../src/Timeseries/EchartsTimeseries.tsx           |   2 +
 .../src/Timeseries/Regular/Line/controlPanel.tsx   |   1 +
 .../src/Timeseries/transformProps.ts               |  26 ++-
 .../plugin-chart-echarts/src/components/Echart.tsx |  18 +-
 .../plugins/plugin-chart-echarts/src/types.ts      |   2 +
 .../SqlLab/components/AIAssistantModal/Content.tsx | 259 +++++++++++++++++++++
 .../components/AIAssistantModal/LiveText.tsx       | 124 ++++++++++
 .../components/AIAssistantModal/PromptInput.tsx    |  81 +++++++
 .../components/AIAssistantModal/ResultCard.tsx     | 138 +++++++++++
 .../SqlLab/components/AIAssistantModal/index.tsx   |  35 +++
 .../src/components/AsyncAceEditor/index.tsx        |   4 +-
 .../src/components/Chart/ChartRenderer.jsx         |   5 +-
 .../components/Drawer/index.tsx}                   |  39 ++--
 .../src/components/Icons/AntdEnhanced.tsx          |   4 +
 .../explore/components/ControlPanelsContainer.tsx  |   8 +
 .../explore/components/ExploreChartPanel/index.jsx |  69 +++++-
 21 files changed, 853 insertions(+), 40 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
index 5baa8c688c..c77ce9b5ec 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
@@ -381,6 +381,11 @@ const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
   ),
 };
 
+const echart_options: SharedControlConfig<'HiddenControl'> = {
+  type: 'HiddenControl',
+  renderTrigger: true,
+};
+
 export default {
   metrics: dndAdhocMetricsControl,
   metric: dndAdhocMetricControl,
@@ -421,4 +426,5 @@ export default {
   temporal_columns_lookup,
   currency_format,
   sort_by_metric,
+  echart_options,
 };
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChartCore.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChartCore.tsx
index f980ed13cf..1050af75ba 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChartCore.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChartCore.tsx
@@ -84,6 +84,48 @@ export default class SuperChartCore extends PureComponent<Props, {}> {
    */
   container?: HTMLElement | null;
 
+  preSelector = createSelector(
+    [
+      (input: {
+        chartProps: ChartProps;
+        preTransformProps?: PreTransformProps;
+      }) => input.chartProps,
+      input => input.preTransformProps,
+    ],
+    (chartProps, pre = IDENTITY) => {
+      console.log('preselector');
+      return { chartProps: pre(chartProps) };
+    },
+  );
+
+  transformSelector = createSelector(
+    [
+      (input: { chartProps: ChartProps; transformProps?: TransformProps }) =>
+        input.chartProps,
+      input => input.transformProps,
+    ],
+    (chartProps, transform = IDENTITY) => {
+      console.log('transformselector');
+      return {
+        chartProps: transform(chartProps),
+      };
+    },
+  );
+
+  postSelector = createSelector(
+    [
+      (input: {
+        chartProps: ChartProps;
+        postTransformProps?: PostTransformProps;
+      }) => input.chartProps,
+      input => input.postTransformProps,
+    ],
+    (chartProps, post = IDENTITY) => {
+      console.log('postselector');
+      return post(chartProps);
+    },
+  );
+
   /**
    * memoized function so it will not recompute
    * and return previous value
@@ -156,10 +198,11 @@ export default class SuperChartCore extends PureComponent<Props, {}> {
 
     return (
       <Chart
-        {...this.processChartProps({
-          chartProps,
-          preTransformProps,
-          transformProps,
+        {...this.postSelector({
+          ...this.transformSelector({
+            ...this.preSelector({ chartProps, preTransformProps }),
+            transformProps,
+          }),
           postTransformProps,
         })}
       />
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx
index d293361f1d..078f08c94c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx
@@ -21,7 +21,14 @@ import { allEventHandlers } from '../utils/eventHandlers';
 import { BoxPlotChartTransformedProps } from './types';
 
 export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) {
-  const { height, width, echartOptions, selectedValues, refs } = props;
+  const {
+    height,
+    width,
+    echartOptions,
+    customEchartOptions,
+    selectedValues,
+    refs,
+  } = props;
 
   const eventHandlers = allEventHandlers(props);
 
@@ -31,6 +38,7 @@ export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) {
       height={height}
       width={width}
       echartOptions={echartOptions}
+      customEchartOptions={customEchartOptions}
       eventHandlers={eventHandlers}
       selectedValues={selectedValues}
     />
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts
index e2208892f7..6dba063710 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts
@@ -155,6 +155,7 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        ['echart_options'],
       ],
     },
   ],
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts
index 511d083d47..c19d83cdac 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts
@@ -49,7 +49,7 @@ export default function transformProps(
   const {
     width,
     height,
-    formData,
+    formData: { echartOptions: _echartOptions, ...formData },
     hooks,
     filterState,
     queriesData,
@@ -286,11 +286,19 @@ export default function transformProps(
     series,
   };
 
+  let customEchartOptions;
+  try {
+    customEchartOptions = JSON.parse(_echartOptions);
+  } catch (e) {
+    // skip
+  }
+
   return {
     formData,
     width,
     height,
     echartOptions,
+    customEchartOptions,
     setDataMask,
     emitCrossFilters,
     labelMap,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
index b91db0b4c1..a5e9a6f53d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
@@ -43,6 +43,7 @@ export default function EchartsTimeseries({
   height,
   width,
   echartOptions,
+  customEchartOptions,
   groupby,
   labelMap,
   selectedValues,
@@ -269,6 +270,7 @@ export default function EchartsTimeseries({
         height={height - extraControlHeight}
         width={width}
         echartOptions={echartOptions}
+        customEchartOptions={customEchartOptions}
         eventHandlers={eventHandlers}
         zrEventHandlers={zrEventHandlers}
         selectedValues={selectedValues}
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
index 8bf40c06c8..f31f05c5e4 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
@@ -247,6 +247,7 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        ['echart_options'],
       ],
     },
   ],
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index 0fb392a12d..346278976e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 /* eslint-disable camelcase */
-import { invert } from 'lodash';
+import { invert, assignIn } from 'lodash';
 import {
   AnnotationLayer,
   AxisType,
@@ -115,7 +115,7 @@ export default function transformProps(
     height,
     filterState,
     legendState,
-    formData,
+    formData: { echartOptions: _echartOptions, ...formData },
     hooks,
     queriesData,
     datasource,
@@ -670,8 +670,30 @@ export default function transformProps(
     focusedSeries = seriesName;
   };
 
+  let customEchartOptions;
+  try {
+    customEchartOptions = new Function('return ' + _echartOptions)();
+    // JSON.parse(_echartOptions, function (key, value) {
+    //   if (
+    //     typeof value === 'string' &&
+    //     (value.indexOf('function') === 0 || value.match(/^\([^)]*\) =>/))
+    //   ) {
+    //     // For both traditional functions and arrow functions
+    //     return new Function('return ' + value)();
+    //   }
+    //   return value;
+    // });
+    // delete customEchartOptions?.series;
+  } catch (e) {
+    // skip
+    console.log(e);
+  }
+
+  console.log('customEchartOptions', _echartOptions, customEchartOptions);
+
   return {
     echartOptions,
+    customEchartOptions,
     emitCrossFilters,
     formData,
     groupby: groupBy,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
index e93327a696..2dd4914f25 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
@@ -122,6 +122,7 @@ function Echart(
     width,
     height,
     echartOptions,
+    customEchartOptions,
     eventHandlers,
     zrEventHandlers,
     selectedValues = {},
@@ -168,6 +169,8 @@ function Echart(
       if (!chartRef.current) {
         chartRef.current = init(divRef.current, null, { locale });
       }
+      // did mount
+      handleSizeChange({ width, height });
       setDidMount(true);
     });
   }, [locale]);
@@ -185,13 +188,20 @@ function Echart(
       });
 
       chartRef.current?.setOption(echartOptions, true);
+    }
+  }, [didMount, echartOptions]);
 
-      // did mount
-      handleSizeChange({ width, height });
+  useEffect(() => {
+    if (didMount && customEchartOptions) {
+      console.log('useEffect for rendering with custom', customEchartOptions);
+      chartRef.current?.setOption(echartOptions, true);
+      chartRef.current?.setOption(customEchartOptions, false);
     }
-  }, [didMount, echartOptions, eventHandlers, zrEventHandlers]);
+  }, [didMount, customEchartOptions]);
 
-  useEffect(() => () => chartRef.current?.dispose(), []);
+  useEffect(() => {
+    return () => chartRef.current?.dispose();
+  }, []);
 
   // highlighting
   useEffect(() => {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
index 294437c745..301846ab30 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
@@ -50,6 +50,7 @@ export interface EchartsProps {
   height: number;
   width: number;
   echartOptions: EChartsCoreOption;
+  customEchartOptions: EChartsCoreOption;
   eventHandlers?: EventHandlers;
   zrEventHandlers?: EventHandlers;
   selectedValues?: Record<number, string>;
@@ -124,6 +125,7 @@ export interface BaseChartProps<T extends PlainObject> extends ChartProps<T> {
 
 export interface BaseTransformedProps<F> {
   echartOptions: EChartsCoreOption;
+  customEchartOptions: EChartsCoreOption;
   formData: F;
   height: number;
   onContextMenu?: (
diff --git a/superset-frontend/src/SqlLab/components/AIAssistantModal/Content.tsx b/superset-frontend/src/SqlLab/components/AIAssistantModal/Content.tsx
new file mode 100644
index 0000000000..d1e2cead64
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/AIAssistantModal/Content.tsx
@@ -0,0 +1,259 @@
+import {
+  styled,
+  SuperChart,
+  SupersetClient,
+  useTheme,
+} from '@superset-ui/core';
+import {
+  FC,
+  useCallback,
+  useEffect,
+  useMemo,
+  useReducer,
+  useRef,
+  useState,
+} from 'react';
+import IconButton from 'src/dashboard/components/IconButton';
+import Icons from 'src/components/Icons';
+import { Input } from 'src/components/Input';
+
+import StyledModal from 'src/components/Modal';
+import { Button, Skeleton } from 'src/components';
+import { JsonEditor } from 'src/components/AsyncAceEditor';
+import ChartRenderer from 'src/components/Chart/ChartRenderer';
+import Popover from 'src/components/Popover';
+import { TimeseriesChartTransformedProps } from '@superset-ui/plugin-chart-echarts';
+import LiveText from './LiveText';
+import PromptInput from './PromptInput';
+import ResultCard from './ResultCard';
+
+const StyledInputBar = styled.div`
+  position: relative;
+  transition: height 0.25s ease-in;
+  height: 55px;
+
+  &.expanded {
+    height: 550px;
+  }
+`;
+
+const StyledIconButtonWrapper = styled.div`
+  position: absolute;
+  right: 8px;
+  top: 4px;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  flex-direction: column;
+  row-gap: 8px;
+`;
+
+const FlexContent = styled.div`
+  flex: 1 1 auto;
+`;
+
+const ChartContainer = styled.div`
+  border: 1px ${({ theme }) => theme.colors.grayscale.light2} solid;
+  border-radius: 8px;
+  position: relative;
+`;
+
+const StyledLoading = styled.div`
+  padding: 20px;
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  width: 100%;
+  height: 100%;
+`;
+
+type Action =
+  | {
+      type: 'request';
+      payload: {
+        prompt: string;
+      };
+    }
+  | {
+      type: 'success';
+      payload: {
+        optionResult: string;
+      };
+    };
+
+type State = {
+  id: number;
+  prompt: string;
+  loading: boolean;
+  optionResult: string;
+}[];
+
+const reducer = (state: State = [], action: Action) => {
+  switch (action.type) {
+    case 'request': {
+      return [
+        ...state,
+        {
+          ...action.payload,
+          id: Date.now(),
+          loading: true,
+          optionResult: '',
+        },
+      ];
+    }
+    case 'success': {
+      const index = state.length - 1;
+      return [
+        ...state.slice(0, index),
+        {
+          ...state[index],
+          loading: false,
+          ...action.payload,
+        },
+      ];
+    }
+    default: {
+      return state;
+    }
+  }
+};
+
+const Content = ({ formData, queriesResponse }) => {
+  const [prompts, dispatch] = useReducer(reducer, []);
+  const chartOptionRef = useRef();
+  const onSubmit = useCallback((prompt: string) => {
+    dispatch({ type: 'request', payload: { prompt } });
+    //     setTimeout(() => {
+    //       const result =
+    //         demoRef.current === 0
+    //           ? `{
+    //   "series": [
+    //     {
+    //       "itemStyle": {
+    //         "color": "red"
+    //       },
+    //       "id": "Michael"
+    //     },
+    //     {
+    //       "itemStyle": {
+    //         "color": "red"
+    //       },
+    //       "id": "David"
+    //     }
+    //   ]
+    // }`
+    //           : `{
+    //   "grid": { "containLabel": true, "left": 50, "right": 20, "top": 40, "bottom": 20 },
+    //   "xAxis": { "type": "time", "nameGap": 15, "nameLocation": "middle", "axisLabel": { "hideOverlap": true }, "minorTick": {}, "minInterval": 86400000 },
+    //   "yAxis": { "scale": true, "yAxisLabelRotation": 0, "type": "value", "minorTick": {}, "minorSplitLine": {}, "axisLabel": {}, "nameGap": 30, "nameLocation": "middle" },
+    //   "legend": { "orient": "horizontal", "show": true, "type": "scroll", "selector": [ "all", "inverse" ], "selectorLabel": { "fontFamily": "'Inter', Helvetica, Arial", "fontSize": 12, "color": "#666666", "borderColor": "#666666" }, "top": 0, "right": 0, "data": [ "Michael", "David", "Christopher", "Daniel", "Jennifer", "Matthew", "Robert", "Jose", "Anthony", "Jessica" ] },
+    //   "series": [
+    //     { "id": "Michael", "name": "Michael", "itemStyle": { "color": "red", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" },
+    //       "markLine": {
+    //         "data": [
+    //           {
+    //             "xAxis": "2000-01-01",
+    //             "name": "Airbnb launched",
+    //             "label": { "formatter": "Airbnb launched" }
+    //           }
+    //         ]
+    //       }
+    //     },
+    //     { "id": "David", "name": "David", "itemStyle": { "color": "red", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Christopher", "name": "Christopher", "itemStyle": { "color": "#5AC189", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Daniel", "name": "Daniel", "itemStyle": { "color": "#FF7F44", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Jennifer", "name": "Jennifer", "itemStyle": { "color": "#666666", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Matthew", "name": "Matthew", "itemStyle": { "color": "#E04355", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Robert", "name": "Robert", "itemStyle": { "color": "#FCC700", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Jose", "name": "Jose", "itemStyle": { "color": "#A868B7", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Anthony", "name": "Anthony", "itemStyle": { "color": "#3CCCCB", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } },
+    //     { "id": "Jessica", "name": "Jessica", "itemStyle": { "color": "#A38F79", "opacity": 1, "borderWidth": 0 }, "type": "line", "showSymbol": false, "symbolSize": 6, "label": { "show": false, "position": "top" } }
+    //   ],
+    //   "toolbox": { "show": false, "top": 0, "right": 5, "feature": { "dataZoom": { "title": { "zoom": "zoom area", "back": "restore zoom" } } } },
+    //   "dataZoom": []
+    // }`;
+    //       demoRef.current += 1;
+    //       dispatch({ type: 'success', payload: { optionResult: result } });
+    //     }, 4000);
+    // SupersetClient.get({
+    //   url: `https://supersetai.sandcastle.musta.ch/echarts?prompt=${prompt}`,
+    //   mode: 'cors',
+    //   credentials: 'include',
+    //   // jsonPayload: {
+    //   //   prompt,
+    //   // },
+    // }).then(({ json }) => {
+    //   console.log('result', json);
+    // });
+    SupersetClient.post({
+      url: '/chat/',
+      jsonPayload: {
+        echartOptions: JSON.stringify(chartOptionRef.current),
+        userInput: prompt,
+      },
+    }).then(({ json }) => {
+      const optionResult = json.result.startsWith('```json\n')
+        ? json.result.slice(8, -3)
+        : json.result;
+      dispatch({ type: 'success', payload: { optionResult } });
+    });
+  }, []);
+
+  const [step, setStep] = useState(0);
+
+  useEffect(() => {
+    if (prompts[prompts.length - 1]?.loading === true) {
+      document?.getElementById('prompt')?.scrollIntoView({
+        behavior: 'smooth',
+      });
+    } else {
+      document?.getElementById('prompt')?.focus();
+    }
+  }, [prompts, step]);
+
+  const postProcess = useCallback(
+    ({ echartOptions, ...options }: TimeseriesChartTransformedProps) => {
+      chartOptionRef.current = echartOptions;
+      return { echartOptions, ...options };
+    },
+    [],
+  );
+
+  return (
+    <Flex>
+      <ChartContainer>
+        <ChartRenderer
+          formData={formData}
+          queriesResponse={queriesResponse}
+          actions={{
+            chartRenderingSucceeded: () => {},
+          }}
+          chartStatus="rendered"
+          postTransformProps={postProcess}
+        />
+      </ChartContainer>
+      {prompts.map(({ id, ...props }, i) => (
+        <ResultCard
+          key={`${id}`}
+          {...props}
+          formData={formData}
+          queriesResponse={queriesResponse}
+        ></ResultCard>
+      ))}
+      {prompts.length === 0 && (
+        <LiveText
+          as="h4"
+          text="How would you like to change?"
+          onCompleted={() => setStep(1)}
+        />
+      )}
+      <PromptInput
+        onSubmit={onSubmit}
+        disabled={prompts[prompts.length - 1]?.loading === true}
+      />
+    </Flex>
+  );
+};
+
+export default Content;
diff --git a/superset-frontend/src/SqlLab/components/AIAssistantModal/LiveText.tsx b/superset-frontend/src/SqlLab/components/AIAssistantModal/LiveText.tsx
new file mode 100644
index 0000000000..0c6ddc93db
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/AIAssistantModal/LiveText.tsx
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { styled } from '@superset-ui/core';
+import { useEffect, useMemo, useRef, useState, forwardRef } from 'react';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+
+type Props = {
+  as?: keyof HTMLElementTagNameMap;
+  text: string;
+  onCompleted?: () => void;
+};
+
+function chunkText(text: string, chunkSize: number) {
+  const chunks = [];
+
+  for (let i = 0; i < text.length; i += chunkSize) {
+    chunks.push(text.slice(i, i + chunkSize));
+  }
+
+  return chunks;
+}
+
+function useAnimatedText(
+  text: string,
+  options: {
+    chunkSize?: number;
+    onCompleted?: () => void;
+  } = {},
+) {
+  const [resolvedText, setText] = useState('');
+  const chunkSize = options.chunkSize ?? 3;
+  const index = useRef(0);
+  const chunks = useMemo(() => chunkText(text, chunkSize), [text]);
+  const onCompleted = useEffectEvent(() => options.onCompleted?.());
+
+  useEffect(() => {
+    if (index.current >= chunks.length) {
+      onCompleted();
+      return;
+    }
+
+    const timer = setTimeout(
+      () => {
+        index.current += 1;
+        setText(chunks.slice(0, index.current).join(''));
+      },
+      Math.random() * 50 + 50,
+    );
+
+    return () => {
+      clearTimeout(timer);
+    };
+  }, [chunks, resolvedText]);
+
+  useEffect(() => {
+    index.current = 0;
+  }, [text]);
+
+  return resolvedText;
+}
+
+const DynamicElementComponent = forwardRef((props: any, ref) => {
+  const { as: As } = props;
+
+  return <As ref={ref} {...props} />;
+});
+
+const StyledElement = styled(DynamicElementComponent)<{ completed: boolean }>`
+  ${({ completed, theme }) =>
+    !completed &&
+    `
+    ::after {
+      content: '';
+      width: 5px;
+      height: 20px;
+      background: ${theme.colors.grayscale.light1};
+      display: inline-block;
+      animation: cursor-blink 1.5s;
+    }
+  `}
+
+  @keyframes cursor-blink {
+    0% {
+      opacity: 0;
+    }
+  }
+`;
+
+const LiveText = ({
+  as = 'span',
+  text,
+  onCompleted,
+  ...remainingProps
+}: Props) => {
+  const children = useAnimatedText(text, { onCompleted });
+
+  return (
+    <StyledElement
+      as={as}
+      {...remainingProps}
+      children={children}
+      completed={text === children}
+    />
+  );
+};
+
+export default LiveText;
diff --git a/superset-frontend/src/SqlLab/components/AIAssistantModal/PromptInput.tsx b/superset-frontend/src/SqlLab/components/AIAssistantModal/PromptInput.tsx
new file mode 100644
index 0000000000..f5447fcbdc
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/AIAssistantModal/PromptInput.tsx
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState } from 'react';
+import { useTheme } from '@superset-ui/core';
+
+import Icons from 'src/components/Icons';
+import { Input } from 'src/components/Input';
+import { Button } from 'src/components';
+
+import useEffectEvent from 'src/hooks/useEffectEvent';
+
+type Props = {
+  disabled: boolean;
+  onSubmit: (prompt: string) => void;
+};
+
+const PromptInput = ({ disabled, onSubmit }: Props) => {
+  const theme = useTheme();
+  const [prompt, setPrompt] = useState('');
+  const [hasError, setHasError] = useState(false);
+
+  const showError = hasError && prompt.length === 0;
+  const handleSubmit = useEffectEvent(() => {
+    if (!prompt) {
+      setHasError(true);
+    } else {
+      onSubmit(prompt);
+      setPrompt('');
+      setHasError(false);
+    }
+  });
+
+  return (
+    <Input
+      id="prompt"
+      variant="outlined"
+      size="large"
+      disabled={disabled}
+      onChange={({ target }) => setPrompt(target.value)}
+      onKeyDown={({ code }) => {
+        if (code === 'Enter') handleSubmit();
+      }}
+      value={prompt}
+      {...(showError && {
+        status: 'error',
+        placeholder: 'Please type a prompt to configure your chart',
+      })}
+      autoFocus
+      suffix={
+        showError ? (
+          <Icons.InfoCircleOutlined />
+        ) : (
+          <Button buttonStyle="primary" shape="circle" onClick={handleSubmit}>
+            <Icons.SendOutlined
+              iconSize="l"
+              iconColor={theme.colors.grayscale.light5}
+            />
+          </Button>
+        )
+      }
+    />
+  );
+};
+
+export default PromptInput;
diff --git a/superset-frontend/src/SqlLab/components/AIAssistantModal/ResultCard.tsx b/superset-frontend/src/SqlLab/components/AIAssistantModal/ResultCard.tsx
new file mode 100644
index 0000000000..c65c55c036
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/AIAssistantModal/ResultCard.tsx
@@ -0,0 +1,138 @@
+import { styled } from '@superset-ui/core';
+import {
+  FC,
+  useCallback,
+  useEffect,
+  useMemo,
+  useReducer,
+  useRef,
+  useState,
+} from 'react';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import { Button, Skeleton } from 'src/components';
+import { TextAreaEditor } from 'src/components/AsyncAceEditor';
+import ChartRenderer from 'src/components/Chart/ChartRenderer';
+import { TimeseriesChartTransformedProps } from '@superset-ui/plugin-chart-echarts';
+import LiveText from './LiveText';
+
+const ChartContainer = styled.div`
+  border: 1px ${({ theme }) => theme.colors.grayscale.light2} solid;
+  border-radius: 8px;
+  position: relative;
+  height: 300px;
+`;
+
+const StyledLoading = styled.div`
+  padding: 20px;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+`;
+
+const ResultCard = ({
+  formData,
+  queriesResponse,
+  loading,
+  prompt,
+  optionResult,
+}) => {
+  const editorRef = useRef(null);
+  const [overrideEchartOpt, setOverrideEchartOpt] = useState(optionResult);
+  const [showEditor, setShowEditor] = useState(false);
+  const postProcess = useCallback(
+    ({ echartOptions, ...options }: TimeseriesChartTransformedProps) => {
+      let compiled;
+      try {
+        compiled = new Function('return ' + overrideEchartOpt)();
+      } catch (e) {
+        // skip
+        console.log(e);
+      }
+      try {
+        if (!compiled) {
+          compiled = new Function('return ' + optionResult)();
+        }
+      } catch (e) {
+        // skip
+        console.log(e);
+      }
+      return {
+        ...options,
+        echartOptions,
+        customEchartOptions: compiled,
+      };
+    },
+    [optionResult, overrideEchartOpt],
+  );
+
+  const onEditorLoad = useCallback(editor => {
+    // editorRef.current = editor;
+    editorRef.current?.scrollIntoView({
+      behavior: 'smooth',
+      block: 'end',
+    });
+  }, []);
+
+  const handleEditClick = useCallback(() => {
+    if (!showEditor) {
+      setShowEditor(true);
+    } else {
+      editorRef.current?.scrollIntoView({
+        behavior: 'smooth',
+        block: 'end',
+      });
+    }
+  }, [showEditor]);
+
+  return (
+    <>
+      {loading ? <LiveText as="h4" text={prompt} /> : <h4>{prompt}</h4>}
+      <ChartContainer>
+        {loading ? (
+          <StyledLoading>
+            <Skeleton active paragraph />
+          </StyledLoading>
+        ) : (
+          <ChartRenderer
+            formData={formData}
+            postTransformProps={postProcess}
+            queriesResponse={queriesResponse}
+            actions={{
+              chartRenderingSucceeded: () => {},
+            }}
+            chartStatus="rendered"
+            width={329}
+            height={300}
+          />
+        )}
+      </ChartContainer>
+
+      {showEditor && (
+        <AutoSizer disableHeight>
+          {({ width }) => (
+            <TextAreaEditor
+              value={overrideEchartOpt || optionResult}
+              width={width}
+              height={300}
+              onChange={(val: string) => {
+                setOverrideEchartOpt(val);
+              }}
+              onLoad={onEditorLoad}
+            />
+          )}
+        </AutoSizer>
+      )}
+      {!loading && (
+        <Flex ref={editorRef}>
+          <Button onClick={handleEditClick}>Edit Chart Config</Button>
+          <Button buttonStyle="primary">Apply change</Button>
+        </Flex>
+      )}
+    </>
+  );
+};
+
+export default ResultCard;
diff --git a/superset-frontend/src/SqlLab/components/AIAssistantModal/index.tsx b/superset-frontend/src/SqlLab/components/AIAssistantModal/index.tsx
new file mode 100644
index 0000000000..29e485af35
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/AIAssistantModal/index.tsx
@@ -0,0 +1,35 @@
+import { useTheme } from '@superset-ui/core';
+import { OutPortal } from 'react-reverse-portal';
+import { FC } from 'react';
+import Icons from 'src/components/Icons';
+import { Button, Skeleton } from 'src/components';
+import Popover from 'src/components/Popover';
+
+type Props = {
+  show: boolean;
+};
+
+const AIAssistantModal: FC<Props> = ({ portalNode }) => {
+  const theme = useTheme();
+  return (
+    <Popover
+      title="AI Assistant"
+      trigger="click"
+      placement="bottomLeft"
+      content={<OutPortal node={portalNode} />}
+    >
+      <Button
+        icon={
+          <Icons.BulbOutlined
+            iconSize="s"
+            iconColor={theme.colors.primary.dark1}
+          />
+        }
+      >
+        AI Assistant
+      </Button>
+    </Popover>
+  );
+};
+
+export default AIAssistantModal;
diff --git a/superset-frontend/src/components/AsyncAceEditor/index.tsx b/superset-frontend/src/components/AsyncAceEditor/index.tsx
index c656f40086..a5ac2142cb 100644
--- a/superset-frontend/src/components/AsyncAceEditor/index.tsx
+++ b/superset-frontend/src/components/AsyncAceEditor/index.tsx
@@ -295,13 +295,15 @@ export const TextAreaEditor = AsyncAceEditor([
   'mode/json',
   'mode/html',
   'mode/javascript',
-  'theme/textmate',
+  'theme/github',
 ]);
 
 export const CssEditor = AsyncAceEditor(['mode/css', 'theme/github']);
 
 export const JsonEditor = AsyncAceEditor(['mode/json', 'theme/github']);
 
+export const JSEditor = AsyncAceEditor(['mode/javascript', 'theme/github']);
+
 /**
  * JSON or Yaml config editor.
  */
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx
index 25fc045d21..ef6e36eaa6 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.jsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx
@@ -162,7 +162,8 @@ class ChartRenderer extends Component {
         nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
         nextProps.formData.stack !== this.props.formData.stack ||
         nextProps.cacheBusterProp !== this.props.cacheBusterProp ||
-        nextProps.emitCrossFilters !== this.props.emitCrossFilters
+        nextProps.emitCrossFilters !== this.props.emitCrossFilters ||
+        nextProps.postTransformProps !== this.props.postTransformProps
       );
     }
     return false;
@@ -327,7 +328,7 @@ class ChartRenderer extends Component {
       ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
       ? { inContextMenu: this.state.inContextMenu }
       : {};
-
+    console.log('chartrenderer render()');
     return (
       <>
         {this.state.showContextMenu && (
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx b/superset-frontend/src/components/Drawer/index.tsx
similarity index 59%
copy from superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx
copy to superset-frontend/src/components/Drawer/index.tsx
index d293361f1d..8075bbe54a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx
+++ b/superset-frontend/src/components/Drawer/index.tsx
@@ -16,23 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import Echart from '../components/Echart';
-import { allEventHandlers } from '../utils/eventHandlers';
-import { BoxPlotChartTransformedProps } from './types';
+import { Drawer as AntdDrawer } from 'antd-v5';
+import { DrawerProps } from 'antd-v5/lib/drawer';
 
-export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) {
-  const { height, width, echartOptions, selectedValues, refs } = props;
+const drawerStyles = {
+  mask: {
+    background: 'rgba(0,0,0,0.2)',
+  },
+  content: {
+    background: 'white',
+  },
+  body: {
+    padding: 16,
+  },
+  header: {
+    padding: 16,
+  },
+};
 
-  const eventHandlers = allEventHandlers(props);
+const Drawer = (props: DrawerProps) => (
+  <AntdDrawer {...props} styles={drawerStyles} />
+);
 
-  return (
-    <Echart
-      refs={refs}
-      height={height}
-      width={width}
-      echartOptions={echartOptions}
-      eventHandlers={eventHandlers}
-      selectedValues={selectedValues}
-    />
-  );
-}
+export type { DrawerProps };
+
+export default Drawer;
diff --git a/superset-frontend/src/components/Icons/AntdEnhanced.tsx b/superset-frontend/src/components/Icons/AntdEnhanced.tsx
index 423e2b39ee..622fee354c 100644
--- a/superset-frontend/src/components/Icons/AntdEnhanced.tsx
+++ b/superset-frontend/src/components/Icons/AntdEnhanced.tsx
@@ -27,6 +27,7 @@ import {
   BarChartOutlined,
   BellOutlined,
   BookOutlined,
+  BulbOutlined,
   CaretDownOutlined,
   CalendarOutlined,
   CaretUpOutlined,
@@ -66,6 +67,7 @@ import {
   RightOutlined,
   SaveOutlined,
   SearchOutlined,
+  SendOutlined,
   SettingOutlined,
   StopOutlined,
   SyncOutlined,
@@ -88,6 +90,7 @@ const AntdIcons = {
   BarChartOutlined,
   BellOutlined,
   BookOutlined,
+  BulbOutlined,
   CaretDownOutlined,
   CalendarOutlined,
   CaretUpOutlined,
@@ -127,6 +130,7 @@ const AntdIcons = {
   RightOutlined,
   SaveOutlined,
   SearchOutlined,
+  SendOutlined,
   SettingOutlined,
   StopOutlined,
   SyncOutlined,
diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index 77e5580fd7..efc6189902 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -67,6 +67,7 @@ import { ExploreActions } from 'src/explore/actions/exploreActions';
 import { ChartState, ExplorePageState } from 'src/explore/types';
 import { Tooltip } from 'src/components/Tooltip';
 import Icons from 'src/components/Icons';
+import { JSEditor } from 'src/components/AsyncAceEditor';
 import ControlRow from './ControlRow';
 import Control from './Control';
 import { ExploreAlert } from './ExploreAlert';
@@ -818,6 +819,13 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
             </Collapse>
           </Tabs.TabPane>
         )}
+        <Tabs.TabPane key="echart" tab={t('Editor')}>
+          <JSEditor
+            onChange={(val: string) => {
+              props.actions.setControlValue('echart_options', val);
+            }}
+          />
+        </Tabs.TabPane>
       </ControlPanelsTabs>
       <div css={actionButtonsContainerStyles}>
         <RunQueryButton
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
index 189177019c..431d941d07 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
@@ -18,6 +18,11 @@
  */
 import { useState, useEffect, useCallback, useMemo } from 'react';
 import PropTypes from 'prop-types';
+import {
+  createHtmlPortalNode,
+  InPortal,
+  OutPortal,
+} from 'react-reverse-portal';
 import Split from 'react-split';
 import {
   css,
@@ -43,6 +48,11 @@ import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
 import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
 import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
 import { getChartRequiredFieldsMissingMessage } from 'src/utils/getChartRequiredFieldsMissingMessage';
+import { Button } from 'src/components';
+import Drawer from 'src/components/Drawer';
+import AIAssistantModal from 'src/SqlLab/components/AIAssistantModal';
+import AIAssistantContent from 'src/SqlLab/components/AIAssistantModal/Content';
+import Icons from 'src/components/Icons';
 import { DataTablesPane } from '../DataTablesPane';
 import { ChartPills } from '../ChartPills';
 import { ExploreAlert } from '../ExploreAlert';
@@ -121,6 +131,13 @@ const Styles = styled.div`
   }
 `;
 
+const FlexHeader = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: baseline;
+`;
+
 const ExploreChartPanel = ({
   chart,
   slice,
@@ -140,6 +157,7 @@ const ExploreChartPanel = ({
   can_download: canDownload,
 }) => {
   const theme = useTheme();
+  const portalNode = useMemo(() => createHtmlPortalNode(), []);
   const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
   const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR;
   const {
@@ -158,6 +176,7 @@ const ExploreChartPanel = ({
       ? false
       : getItem(LocalStorageKeys.IsDatapanelOpen, false),
   );
+  const [showAIAssistant, setShowAIAssistant] = useState(false);
 
   const [showDatasetModal, setShowDatasetModal] = useState(false);
 
@@ -363,18 +382,33 @@ const ExploreChartPanel = ({
             `}
           />
         )}
-        <ChartPills
-          queriesResponse={chart.queriesResponse}
-          chartStatus={chart.chartStatus}
-          chartUpdateStartTime={chart.chartUpdateStartTime}
-          chartUpdateEndTime={chart.chartUpdateEndTime}
-          refreshCachedQuery={refreshCachedQuery}
-          rowLimit={formData?.row_limit}
-        />
+        <FlexHeader>
+          {/* <AIAssistantModal portalNode={portalNode} /> */}
+          <Button
+            icon={
+              <Icons.BulbOutlined
+                iconSize="s"
+                iconColor={theme.colors.primary.dark1}
+              />
+            }
+            onClick={() => setShowAIAssistant(true)}
+          >
+            AI Assistant
+          </Button>
+          <ChartPills
+            queriesResponse={chart.queriesResponse}
+            chartStatus={chart.chartStatus}
+            chartUpdateStartTime={chart.chartUpdateStartTime}
+            chartUpdateEndTime={chart.chartUpdateEndTime}
+            refreshCachedQuery={refreshCachedQuery}
+            rowLimit={formData?.row_limit}
+          />
+        </FlexHeader>
         {renderChart()}
       </div>
     ),
     [
+      portalNode,
       resizeObserverRef,
       showAlertBanner,
       errorMessage,
@@ -464,6 +498,25 @@ const ExploreChartPanel = ({
           formData={formData}
         />
       )}
+      <Drawer
+        title="AI Assistant"
+        open={showAIAssistant}
+        onClose={() => setShowAIAssistant(false)}
+      >
+        <AIAssistantContent
+          formData={formData}
+          queriesResponse={chart.queriesResponse}
+          onClose={() => setShowAIAssistant(false)}
+        />
+      </Drawer>
+
+      {/* <InPortal node={portalNode}>
+        <AIAssistantContent
+          formData={formData}
+          queriesResponse={chart.queriesResponse}
+          onClose={() => setShowAIAssistant(false)}
+        />
+      </InPortal> */}
     </Styles>
   );
 };