From 2ddbd3309ebfd0e16ce6d4edec2f238c8a178b94 Mon Sep 17 00:00:00 2001 From: Arpit Date: Thu, 11 May 2023 15:04:48 +0530 Subject: [PATCH] =?UTF-8?q?Feature=20-=20ToolJet=20Copilot=20=F0=9F=9A=80?= =?UTF-8?q?=20(#6074)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add support for Copilot assistance * clean up * fixes workspace settings crash * refactor and resolved review comments * api endpoint should be inferred from env * copilot style fixes * copilot style fixed * beta tag fro copilot settings: workspace * fire toast for unauthorised recommendation request * include the previous code with the newly generated response * scoping apikeys to orgs * controller updates for copilot * copilot org key updated * disable toggle for new workspaces * disable toggle for new workspaces * fixes: multi-workspace toggle updates * uninstall unsued packages * fixes button state for copilot in transformations * updated the urls --- frontend/assets/images/icons/copilot.svg | 9 + frontend/assets/images/icons/flash.svg | 3 + frontend/assets/images/icons/padlock2.svg | 3 + frontend/assets/images/icons/portal-close.svg | 17 +- frontend/assets/images/icons/portal-open.svg | 17 +- .../src/CopilotSettings/ApiKeyContainer.jsx | 77 ++++++ .../src/CopilotSettings/CopilotSetting.jsx | 230 ++++++++++++++++++ frontend/src/CopilotSettings/index.js | 3 + .../src/Editor/CodeBuilder/CodeHinter.jsx | 11 +- frontend/src/Editor/CodeBuilder/utils.js | 54 ++++ .../Editor/QueryManager/Transformation.jsx | 175 ++++++++++--- frontend/src/ManageOrgVars/ManageOrgVars.jsx | 6 +- .../src/OrganizationSettingsPage/index.jsx | 6 +- frontend/src/_components/Portal/Portal.jsx | 58 +++-- frontend/src/_hooks/use-portal.jsx | 2 + frontend/src/_services/copilot.service.js | 30 +++ frontend/src/_services/index.js | 1 + frontend/src/_styles/components.scss | 16 ++ frontend/src/_styles/queryManager.scss | 2 + frontend/src/_styles/theme.scss | 4 + frontend/src/_ui/LeftSidebar/Button.jsx | 14 +- server/package-lock.json | 4 + server/src/app.module.ts | 2 + server/src/controllers/copilot.controller.ts | 36 +++ server/src/dto/copilot.dto.ts | 20 ++ server/src/modules/copilot/copilot.module.ts | 14 ++ server/src/services/copilot.service.ts | 62 +++++ 27 files changed, 789 insertions(+), 87 deletions(-) create mode 100644 frontend/assets/images/icons/copilot.svg create mode 100644 frontend/assets/images/icons/flash.svg create mode 100644 frontend/assets/images/icons/padlock2.svg create mode 100644 frontend/src/CopilotSettings/ApiKeyContainer.jsx create mode 100644 frontend/src/CopilotSettings/CopilotSetting.jsx create mode 100644 frontend/src/CopilotSettings/index.js create mode 100644 frontend/src/_services/copilot.service.js create mode 100644 server/src/controllers/copilot.controller.ts create mode 100644 server/src/dto/copilot.dto.ts create mode 100644 server/src/modules/copilot/copilot.module.ts create mode 100644 server/src/services/copilot.service.ts diff --git a/frontend/assets/images/icons/copilot.svg b/frontend/assets/images/icons/copilot.svg new file mode 100644 index 0000000000..1d675333cb --- /dev/null +++ b/frontend/assets/images/icons/copilot.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/assets/images/icons/flash.svg b/frontend/assets/images/icons/flash.svg new file mode 100644 index 0000000000..43c6d949ad --- /dev/null +++ b/frontend/assets/images/icons/flash.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/assets/images/icons/padlock2.svg b/frontend/assets/images/icons/padlock2.svg new file mode 100644 index 0000000000..7f69c86803 --- /dev/null +++ b/frontend/assets/images/icons/padlock2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/assets/images/icons/portal-close.svg b/frontend/assets/images/icons/portal-close.svg index cf6d2a465e..bf3cba1f41 100644 --- a/frontend/assets/images/icons/portal-close.svg +++ b/frontend/assets/images/icons/portal-close.svg @@ -1,14 +1,3 @@ - - - - - - + + + \ No newline at end of file diff --git a/frontend/assets/images/icons/portal-open.svg b/frontend/assets/images/icons/portal-open.svg index 3a13b0397e..88acba83ad 100644 --- a/frontend/assets/images/icons/portal-open.svg +++ b/frontend/assets/images/icons/portal-open.svg @@ -1,14 +1,3 @@ - - - - - - + + + \ No newline at end of file diff --git a/frontend/src/CopilotSettings/ApiKeyContainer.jsx b/frontend/src/CopilotSettings/ApiKeyContainer.jsx new file mode 100644 index 0000000000..6e8032e4d6 --- /dev/null +++ b/frontend/src/CopilotSettings/ApiKeyContainer.jsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from 'react'; +import { Button } from '@/_ui/LeftSidebar'; +import { Alert } from '@/_ui/Alert/'; + +export const ApiKeyContainer = ({ + copilotApiKey = '', + handleOnSave, + isLoading = false, + darkMode, + isCopilotEnabled, +}) => { + const [inputValue, setInputValue] = useState(copilotApiKey); + + const handleOnchange = (e) => { + setInputValue(e.target.value); + }; + + useEffect(() => { + setInputValue(copilotApiKey); + }, [copilotApiKey]); + + return ( +
+
+ + + API KEY + +
+ +
+
+ +
+
+ +
+ +

Don't have an API key?

+
+ ToolJet Copilot + is currently in beta and provided on request. + Join our waitlist to be notified when API keys become available, or sign up for beta access to get started + today. +
+
+ +
+
+
+
+ ); +}; diff --git a/frontend/src/CopilotSettings/CopilotSetting.jsx b/frontend/src/CopilotSettings/CopilotSetting.jsx new file mode 100644 index 0000000000..f4940befa3 --- /dev/null +++ b/frontend/src/CopilotSettings/CopilotSetting.jsx @@ -0,0 +1,230 @@ +import React, { useEffect, useState } from 'react'; +import { ApiKeyContainer } from './ApiKeyContainer'; +import { copilotService, orgEnvironmentVariableService, authenticationService } from '@/_services'; +import { toast } from 'react-hot-toast'; +import { CustomToggleSwitch } from '@/Editor/QueryManager/CustomToggleSwitch'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import { Button } from '@/_ui/LeftSidebar'; +import { useLocalStorageState } from '@/_hooks/use-local-storage'; + +export const CopilotSetting = () => { + const { current_organization_id } = authenticationService.currentSessionValue; + const [copilotApiKey, setCopilotApiKey] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [state, setState] = useLocalStorageState(`copilotEnabled-${current_organization_id}`, false); + const [copilotWorkspaceVarId, set] = useState(null); + + const saveCopilotApiKey = async (apikey) => { + setIsLoading(true); + const isCopilotApiKeyPresent = await validateApiKey(apikey); + + return setTimeout(() => { + if (isCopilotApiKeyPresent === true && !copilotWorkspaceVarId) { + return orgEnvironmentVariableService + .create(`copilot_api_key-${current_organization_id}`, apikey, 'server', false) + .then(() => { + setCopilotApiKey(apikey); + toast.success('Copilot API key saved successfully'); + }) + .catch((err) => { + console.log(err); + return toast.error('Something went wrong'); + }) + .finally(() => setIsLoading(false)); + } + + if (isCopilotApiKeyPresent === true && copilotWorkspaceVarId) { + return orgEnvironmentVariableService + .update(copilotWorkspaceVarId, `copilot_api_key-${current_organization_id}`, apikey) + .then(() => { + setCopilotApiKey(apikey); + toast.success('Copilot API key saved successfully'); + }) + .catch((err) => { + console.log(err); + return toast.error('Something went wrong'); + }) + .finally(() => setIsLoading(false)); + } + + return toast.error('API key is not valid') && setIsLoading(false); + }, 400); + }; + + const handleCopilotToggle = () => { + setState((prevState) => !prevState); + }; + + const validateApiKey = (apiKey) => { + return new Promise((resolve, reject) => { + copilotService + .validateCopilotAPIKey(apiKey) + .then(({ status }) => { + if (status === 'ok') { + return resolve(true); + } + + return resolve(false); + }) + .catch((err) => { + return reject(err); + }); + }); + }; + + useEffect(() => { + if (!state) { + return; + } + + orgEnvironmentVariableService.getVariables().then((data) => { + const isCopilotApiKeyPresent = data.variables.some( + (variable) => variable.variable_name === `copilot_api_key-${current_organization_id}` + ); + + const shouldUpdate = isCopilotApiKeyPresent; + if (shouldUpdate) { + const copilotVariableId = data.variables.find( + (variable) => variable.variable_name === `copilot_api_key-${current_organization_id}` + )?.id; + const key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; + set(copilotVariableId); + setCopilotApiKey(key); + } + }); + + return () => { + setCopilotApiKey(''); + set(null); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const darkMode = localStorage.getItem('darkMode') === 'true'; + + return ( +
+
+
+ +
+
+ +
+
+
+
+
+
+ ); +}; + +const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode }) => { + return ( +
+
+
+

Copilot

+ beta +
+
+ +
+
+
+
+
+
+ + + + Enable Copilot + + Turn on Copilot functionality in your workspace + + +
+
+ {children} +
+
+
+ ); +}; + +const EducativeLebel = ({ darkMode }) => { + const title = () => { + return ( + <> + Learn about ToolJet AI copilot + + ); + }; + + const popoverForRecommendation = ( + +
+ AI copilot +
+

ToolJet x OpenAI

+

+ AI copilot helps you write your queries + faster. It uses OpenAI's GPT-3.5 to suggest queries based on your data. +

+ + +
+
+
+ ); + + return ( +
+ + + + + + + + +
+ ); +}; diff --git a/frontend/src/CopilotSettings/index.js b/frontend/src/CopilotSettings/index.js new file mode 100644 index 0000000000..4536731bed --- /dev/null +++ b/frontend/src/CopilotSettings/index.js @@ -0,0 +1,3 @@ +import { CopilotSetting } from './CopilotSetting'; + +export { CopilotSetting }; diff --git a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx index e088521ee0..2729c35c48 100644 --- a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx +++ b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx @@ -68,6 +68,7 @@ export function CodeHinter({ component, popOverCallback, cyLabel = '', + callgpt = () => null, }) { const darkMode = localStorage.getItem('darkMode') === 'true'; const options = { @@ -318,6 +319,7 @@ export function CodeHinter({ callback={handleToggle} icon="portal-open" tip="Pop out code editor into a new window" + transformation={componentName === 'transformation'} /> )} { +const PopupIcon = ({ callback, icon, tip, transformation = false }) => { + const size = transformation ? 20 : 12; + return (
{ { e.stopPropagation(); callback(); diff --git a/frontend/src/Editor/CodeBuilder/utils.js b/frontend/src/Editor/CodeBuilder/utils.js index aeddc40fcd..0e5db85e68 100644 --- a/frontend/src/Editor/CodeBuilder/utils.js +++ b/frontend/src/Editor/CodeBuilder/utils.js @@ -1,4 +1,40 @@ import _ from 'lodash'; +import { copilotService } from '@/_services/copilot.service'; +import { toast } from 'react-hot-toast'; + +export async function getRecommendation(currentContext, query, lang = 'javascript') { + const words = query.split(' '); + let results = []; + + function arrayToObject(arr) { + return _.reduce( + arr, + (result, { key, value }) => { + if (!result.hasOwnProperty(key)) { + result[key] = value; + } + return result; + }, + {} + ); + } + + try { + words.forEach((word) => { + results = results.concat(searchQuery(word, currentContext)); + }); + + const context = JSON.stringify(arrayToObject(results)); + + const { data } = await copilotService.getCopilotRecommendations({ context, query, lang }); + + return query + '\n' + data; + } catch ({ error, data }) { + const errorMessage = data?.message.includes('Unauthorized') ? 'Invalid Copilot API Key' : 'Something went wrong'; + toast.error(errorMessage); + return query; + } +} function getResult(suggestionList, query) { const result = suggestionList.filter((key) => key.includes(query)); @@ -249,3 +285,21 @@ export function handleChange(editor, onChange, ignoreBraces = false, currentStat keystrokeCaller(); } } + +function searchQuery(query, obj) { + const lcQuery = query.toLowerCase(); + let results = []; + + for (const key in obj) { + const value = obj[key]; + if (value !== null && typeof value === 'object') { + results = results?.concat(searchQuery(lcQuery, value)); + } else { + if (key?.toLowerCase()?.includes(lcQuery) || value?.toString()?.toLowerCase()?.includes(lcQuery)) { + results.push({ key, value }); + } + } + } + + return results; +} diff --git a/frontend/src/Editor/QueryManager/Transformation.jsx b/frontend/src/Editor/QueryManager/Transformation.jsx index 1896aff857..e2f321ab4f 100644 --- a/frontend/src/Editor/QueryManager/Transformation.jsx +++ b/frontend/src/Editor/QueryManager/Transformation.jsx @@ -5,6 +5,7 @@ import 'codemirror/addon/hint/show-hint'; import 'codemirror/addon/search/match-highlighter'; import 'codemirror/addon/hint/show-hint.css'; import { CodeHinter } from '../CodeBuilder/CodeHinter'; +import { getRecommendation } from '../CodeBuilder/utils'; import { Popover, OverlayTrigger } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import Select from '@/_ui/Select'; @@ -12,9 +13,13 @@ import { useLocalStorageState } from '@/_hooks/use-local-storage'; import _ from 'lodash'; import { CustomToggleSwitch } from './CustomToggleSwitch'; import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles'; +import { Button } from '@/_ui/LeftSidebar'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import { authenticationService } from '@/_services'; export const Transformation = ({ changeOption, currentState, options, darkMode, queryId }) => { const { t } = useTranslation(); + const { current_organization_id } = authenticationService.currentSessionValue; const [lang, setLang] = React.useState(options?.transformationLanguage ?? 'javascript'); @@ -33,6 +38,17 @@ return [row for row in data if row['amount'] > 1000] const [state, setState] = useLocalStorageState('transformation', defaultValue); + const [fetchingRecommendation, setFetchingRecommendation] = useState(false); + const isCopilotEnabled = localStorage.getItem(`copilotEnabled-${current_organization_id}`) === 'true'; + + const handleCallToGPT = async () => { + setFetchingRecommendation(true); + const query = state[lang]; + const recommendation = await getRecommendation(currentState, query, lang); + setFetchingRecommendation(false); + changeOption('transformation', recommendation); + }; + function toggleEnableTransformation() { setEnableTransformation((prev) => !prev); changeOption('enableTransformation', !enableTransformation); @@ -136,22 +152,45 @@ return [row for row in data if row['amount'] > 1000] ); - return ( -
-
-
- +
+ AI copilot +
+

ToolJet x OpenAI

+

+ AI copilot helps you write your queries + faster. It uses OpenAI's GPT-3.5 to suggest queries based on your data. +

+ +
- - {t('editor.queryManager.transformation.transformations', 'Transformations')} - - +
+ + ); + + const EducativeLebel = () => { + const title = () => { + return ( + <> + Powered by AI copilot + + ); + }; + return ( +
+ + + + 1000]
+ ); + }; + + return ( +
+
+
+
+ +
+ + {t('editor.queryManager.transformation.transformations', 'Transformations')} + + + + + + +
+ + +


{enableTransformation && (
-
-
- Language +
+
+
+ Language +
+ { - setLang(value); - changeOption('transformationLanguage', value); - changeOption('transformation', state[value]); - }} - placeholder={t('globals.select', 'Select') + '...'} - styles={computeSelectStyles(darkMode, 140)} - useCustomStyles={true} - /> + +
+ +
+ + {!isCopilotEnabled && ( + + )}
1000] theme={darkMode ? 'monokai' : 'base16-light'} lineNumbers={true} height={'300px'} - className="query-hinter mt-3" + className="query-hinter" ignoreBraces={true} onChange={(value) => changeOption('transformation', value)} componentName={`transformation`} cyLabel={'transformation-input'} + callgpt={handleCallToGPT} />
)} diff --git a/frontend/src/ManageOrgVars/ManageOrgVars.jsx b/frontend/src/ManageOrgVars/ManageOrgVars.jsx index 028017d084..40d09de3b7 100644 --- a/frontend/src/ManageOrgVars/ManageOrgVars.jsx +++ b/frontend/src/ManageOrgVars/ManageOrgVars.jsx @@ -5,6 +5,7 @@ import { toast } from 'react-hot-toast'; import VariablesTable from './VariablesTable'; // eslint-disable-next-line import/no-unresolved import { withTranslation } from 'react-i18next'; +import _ from 'lodash'; import ManageOrgVarsDrawer from './ManageOrgVarsDrawer'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; class ManageOrgVarsComponent extends React.Component { @@ -73,8 +74,11 @@ class ManageOrgVarsComponent extends React.Component { }); orgEnvironmentVariableService.getVariables().then((data) => { + const variables = _.cloneDeep(data.variables)?.filter( + ({ variable_name }) => !/copilot_api_key/.test(variable_name) + ); this.setState({ - variables: data.variables, + variables: variables, isLoading: false, }); }); diff --git a/frontend/src/OrganizationSettingsPage/index.jsx b/frontend/src/OrganizationSettingsPage/index.jsx index df9df2fb5e..23a8b7b206 100644 --- a/frontend/src/OrganizationSettingsPage/index.jsx +++ b/frontend/src/OrganizationSettingsPage/index.jsx @@ -6,6 +6,7 @@ import { ManageGroupPermissions } from '@/ManageGroupPermissions'; import { ManageSSO } from '@/ManageSSO'; import { ManageOrgVars } from '@/ManageOrgVars'; import { authenticationService } from '@/_services'; +import { CopilotSetting } from '@/CopilotSettings'; import { BreadCrumbContext } from '../App/App'; import FolderList from '@/_ui/FolderList/FolderList'; import { OrganizationList } from '../_components/OrganizationManager/List'; @@ -15,7 +16,7 @@ export function OrganizationSettings(props) { const [selectedTab, setSelectedTab] = useState(admin ? 'Users & permissions' : 'manageEnvVars'); const { updateSidebarNAV } = useContext(BreadCrumbContext); - const sideBarNavs = ['Users', 'Groups', 'SSO', 'Workspace variables']; + const sideBarNavs = ['Users', 'Groups', 'SSO', 'Workspace variables', 'Copilot']; const defaultOrgName = (groupName) => { switch (groupName) { case 'Users': @@ -26,6 +27,8 @@ export function OrganizationSettings(props) { return 'manageSSO'; case 'Workspace variables': return 'manageEnvVars'; + case 'Copilot': + return 'manageCopilot'; default: return groupName; } @@ -78,6 +81,7 @@ export function OrganizationSettings(props) { {selectedTab === 'manageGroups' && } {selectedTab === 'manageSSO' && } {selectedTab === 'manageEnvVars' && } + {selectedTab === 'manageCopilot' && }
diff --git a/frontend/src/_components/Portal/Portal.jsx b/frontend/src/_components/Portal/Portal.jsx index 81f52ad16c..cc97fa3da7 100644 --- a/frontend/src/_components/Portal/Portal.jsx +++ b/frontend/src/_components/Portal/Portal.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { ReactPortal } from './ReactPortal.js'; import { Rnd } from 'react-rnd'; +import { Button } from '@/_ui/LeftSidebar'; const Portal = ({ children, ...restProps }) => { - const { isOpen, trigger, styles, className, componentName, dragResizePortal } = restProps; + const { isOpen, trigger, styles, className, componentName, dragResizePortal, callgpt } = restProps; const [name, setName] = React.useState(componentName); const handleClose = (e) => { e.stopPropagation(); @@ -43,6 +44,7 @@ const Portal = ({ children, ...restProps }) => { styles={styles} componentName={name} dragResizePortal={dragResizePortal} + callgpt={callgpt} > {children} @@ -55,7 +57,18 @@ const Container = ({ children, ...restProps }) => { return {children}; }; -const Modal = ({ children, handleClose, portalStyles, styles, componentName, darkMode, dragResizePortal }) => { +const Modal = ({ children, handleClose, portalStyles, styles, componentName, darkMode, dragResizePortal, callgpt }) => { + const [loading, setLoading] = React.useState(false); + + const handleCallGpt = () => { + setLoading(true); + + callgpt().then(() => setLoading(false)); + }; + + const isCopilotEnabled = localStorage.getItem('copilotEnabled') === 'true'; + const includeGPT = ['Runjs', 'Runpy', 'transformation'].includes(componentName) && isCopilotEnabled; + const renderModalContent = () => (
- {componentName ?? 'Editor'} + + {componentName ?? 'Editor'} +
- +
+ )} + + + +
{ optionalProps = {}, selectors = {}, dragResizePortal = false, + callgpt, } = restProps; const renderCustomComponent = ({ component, ...restProps }) => { @@ -36,6 +37,7 @@ const usePortal = ({ children, ...restProps }) => { trigger={callback} componentName={componentName} dragResizePortal={dragResizePortal} + callgpt={callgpt} >
{React.cloneElement(children, { ...styleProps })} diff --git a/frontend/src/_services/copilot.service.js b/frontend/src/_services/copilot.service.js new file mode 100644 index 0000000000..b1cb01e4f1 --- /dev/null +++ b/frontend/src/_services/copilot.service.js @@ -0,0 +1,30 @@ +import config from 'config'; +import { authHeader, handleResponse } from '@/_helpers'; + +export const copilotService = { + getCopilotRecommendations, + validateCopilotAPIKey, +}; + +async function getCopilotRecommendations(options) { + const body = { + query: options.query, + context: options.context, + language: options.lang, + }; + + const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; + + const { data } = await fetch(`${config.apiUrl}/copilot`, requestOptions).then(handleResponse); + + return data || {}; +} + +function validateCopilotAPIKey(key) { + const body = { + secretKey: key, + }; + + const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; + return fetch(`${config.apiUrl}/copilot/api-key`, requestOptions).then(handleResponse); +} diff --git a/frontend/src/_services/index.js b/frontend/src/_services/index.js index 7f3daa3fac..b79127baf3 100644 --- a/frontend/src/_services/index.js +++ b/frontend/src/_services/index.js @@ -19,3 +19,4 @@ export * from './marketplace.service'; export * from './tooljetDatabase.service'; export * from './globalDatasource.service'; export * from './app_environment.service'; +export * from './copilot.service'; diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss index 78d088b326..0d9873c0fa 100644 --- a/frontend/src/_styles/components.scss +++ b/frontend/src/_styles/components.scss @@ -42,6 +42,7 @@ $btn-dark-color: #FFFFFF; &:hover { background: lighten($btn-dark-bg, 10%); + border-color: #D7DBDF !important; } img { @@ -242,6 +243,11 @@ $btn-dark-color: #FFFFFF; border: 1px solid #FFF1E7 !important; } +.copilot-alert { + background-color: #F8F9FA !important; + border: 1px solid #E6E8EB !important; +} + .page-handle-edit-container { height: 60px; width: 100%; @@ -325,4 +331,14 @@ $btn-dark-color: #FFFFFF; #popover-change-scope { border: 1px solid rgba(101, 109, 119, 0.16); box-shadow: 0px 3px 2px rgba(0, 0, 0, 0.25); +} + +.tj-badge { + background: #F0F4FF; + color: #3E63DD; + font-family: 'Inter', sans-serif; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 20px; } \ No newline at end of file diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss index c8a457cff4..4b6159f5b0 100644 --- a/frontend/src/_styles/queryManager.scss +++ b/frontend/src/_styles/queryManager.scss @@ -1250,6 +1250,7 @@ $border-radius: 4px; border: 1px solid $color-light-slate-07 !important; border-radius: 0; border-radius: 6px 0 0 6px; + height: 32px; span { color: $color-light-slate-11; @@ -1467,6 +1468,7 @@ $border-radius: 4px; border-style: solid; border-color: #cccccc; border-width: 1px 0 1px 1px !important; + height: 32px; span { color: $color-dark-slate-11; diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 59a16197bf..c34070ddc3 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -7250,6 +7250,7 @@ tbody { font-weight: 500; height: 28px; cursor: pointer; + white-space: nowrap; .query-btn-svg-wrapper { width: 16px !important; @@ -10494,6 +10495,9 @@ tbody { } } +.theme-dark .card-container { + background-color: #121212 !important +} .version-select { .react-select__menu { .react-select__menu-list { diff --git a/frontend/src/_ui/LeftSidebar/Button.jsx b/frontend/src/_ui/LeftSidebar/Button.jsx index 9770738872..fdf5fd355d 100644 --- a/frontend/src/_ui/LeftSidebar/Button.jsx +++ b/frontend/src/_ui/LeftSidebar/Button.jsx @@ -17,8 +17,8 @@ const Button = ({ disabled = false, isLoading = false, }) => { - const baseHeight = size === 'sm' ? 28 : 40; - const baseWidth = size === 'sm' ? 92 : 150; + const baseHeight = size === 'sm' ? 28 : size === 'md' ? 36 : 40; + const baseWidth = size === 'sm' ? 92 : size === 'md' ? 100 : 150; const diabledStyles = { ...defaultDisabledStyles, @@ -57,7 +57,7 @@ const Content = ({ title = null, iconSrc = null, direction = 'left', dataCy }) = ) : typeof title === 'function' ? ( title() ) : ( - + {title} ); @@ -66,12 +66,14 @@ const Content = ({ title = null, iconSrc = null, direction = 'left', dataCy }) = return content; }; -const UnstyledButton = ({ children, onClick, classNames = '', styles = {}, disabled = false }) => { +const UnstyledButton = ({ children, onClick, classNames = '', styles = {}, disabled = false, darkMode = false }) => { + const cursorNotPointer = onClick === undefined && { cursor: 'default' }; + return (
{children} diff --git a/server/package-lock.json b/server/package-lock.json index 4823e56a19..ae65bfd39f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -137,11 +137,13 @@ "@tooljet-plugins/n8n": "file:packages/n8n", "@tooljet-plugins/notion": "file:packages/notion", "@tooljet-plugins/openapi": "file:packages/openapi", + "@tooljet-plugins/oracledb": "file:packages/oracledb", "@tooljet-plugins/postgresql": "file:packages/postgresql", "@tooljet-plugins/redis": "file:packages/redis", "@tooljet-plugins/restapi": "file:packages/restapi", "@tooljet-plugins/rethinkdb": "file:packages/rethinkdb", "@tooljet-plugins/s3": "file:packages/s3", + "@tooljet-plugins/saphana": "file:packages/saphana", "@tooljet-plugins/sendgrid": "file:packages/sendgrid", "@tooljet-plugins/slack": "file:packages/slack", "@tooljet-plugins/smtp": "file:packages/smtp", @@ -14890,11 +14892,13 @@ "@tooljet-plugins/n8n": "file:packages/n8n", "@tooljet-plugins/notion": "file:packages/notion", "@tooljet-plugins/openapi": "file:packages/openapi", + "@tooljet-plugins/oracledb": "file:packages/oracledb", "@tooljet-plugins/postgresql": "file:packages/postgresql", "@tooljet-plugins/redis": "file:packages/redis", "@tooljet-plugins/restapi": "file:packages/restapi", "@tooljet-plugins/rethinkdb": "file:packages/rethinkdb", "@tooljet-plugins/s3": "file:packages/s3", + "@tooljet-plugins/saphana": "file:packages/saphana", "@tooljet-plugins/sendgrid": "file:packages/sendgrid", "@tooljet-plugins/slack": "file:packages/slack", "@tooljet-plugins/smtp": "file:packages/smtp", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index cf54ededac..489452226f 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -38,6 +38,7 @@ import { EventsModule } from './events/events.module'; import { GroupPermissionsModule } from './modules/group_permissions/group_permissions.module'; import { TooljetDbModule } from './modules/tooljet_db/tooljet_db.module'; import { PluginsModule } from './modules/plugins/plugins.module'; +import { CopilotModule } from './modules/copilot/copilot.module'; import * as path from 'path'; import * as fs from 'fs'; import { AppEnvironmentsModule } from './modules/app_environments/app_environments.module'; @@ -97,6 +98,7 @@ const imports = [ PluginsModule, EventsModule, AppEnvironmentsModule, + CopilotModule, ]; if (process.env.SERVE_CLIENT !== 'false') { diff --git a/server/src/controllers/copilot.controller.ts b/server/src/controllers/copilot.controller.ts new file mode 100644 index 0000000000..deda4084f2 --- /dev/null +++ b/server/src/controllers/copilot.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Body, Post, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard'; +import { User } from 'src/decorators/user.decorator'; +import { CopilotRequestDto } from '@dto/copilot.dto'; +import { CopilotService } from '@services/copilot.service'; +import { OrgEnvironmentVariablesService } from '@services/org_environment_variables.service'; + +@Controller('copilot') +export class CopilotController { + constructor( + private orgEnvironmentVariablesService: OrgEnvironmentVariablesService, + private copilotService: CopilotService + ) {} + + @UseGuards(JwtAuthGuard) + @Post() + async getRecomendations(@User() user, @Body() body: CopilotRequestDto) { + const userId = user.id; + + const workspaceEnvs = await this.orgEnvironmentVariablesService.fetchVariables(user.organizationId); + + const copilotApiKeyId = workspaceEnvs.find((env) => env.variableName.includes('copilot_api_key')); + + const { value } = copilotApiKeyId + ? await this.orgEnvironmentVariablesService.fetch(user.organizationId, copilotApiKeyId.id) + : null; + + return await this.copilotService.getCopilotRecommendations(body, userId, user.organizationId, value); + } + + @UseGuards(JwtAuthGuard) + @Post('api-key') + async validateCopilotAPIKey(@User() user, @Body() body: { secretKey: string }) { + return await this.copilotService.validateCopilotAPIKey(user.id, body.secretKey); + } +} diff --git a/server/src/dto/copilot.dto.ts b/server/src/dto/copilot.dto.ts new file mode 100644 index 0000000000..ef0be958f7 --- /dev/null +++ b/server/src/dto/copilot.dto.ts @@ -0,0 +1,20 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CopilotRequestDto { + @IsString() + @IsNotEmpty() + query: string; + + @IsString() + @IsNotEmpty() + context: string; + + @IsNotEmpty() + language: 'javascript' | 'python'; +} + +export class AddUpdateCopilitAPIKeyDto { + @IsString() + @IsNotEmpty() + key: string; +} diff --git a/server/src/modules/copilot/copilot.module.ts b/server/src/modules/copilot/copilot.module.ts new file mode 100644 index 0000000000..ae37e92afb --- /dev/null +++ b/server/src/modules/copilot/copilot.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { CopilotController } from '@controllers/copilot.controller'; +import { CopilotService } from '@services/copilot.service'; +import { OrgEnvironmentVariablesService } from '@services/org_environment_variables.service'; +import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.entity'; +import { EncryptionService } from '@services/encryption.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + controllers: [CopilotController], + imports: [TypeOrmModule.forFeature([OrgEnvironmentVariable])], + providers: [CopilotService, OrgEnvironmentVariablesService, EncryptionService], +}) +export class CopilotModule {} diff --git a/server/src/services/copilot.service.ts b/server/src/services/copilot.service.ts new file mode 100644 index 0000000000..bce7357fde --- /dev/null +++ b/server/src/services/copilot.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; +import { CopilotRequestDto } from '@dto/copilot.dto'; +import { EncryptionService } from '@services/encryption.service'; +import got from 'got'; + +type ICopilotOptions = CopilotRequestDto; + +@Injectable() +export class CopilotService { + constructor(private encryptionService: EncryptionService) {} + async getCopilotRecommendations( + copilotOptions: ICopilotOptions, + userId: string, + orgnaizationId: string, + encryptedAPIKey: string + ) { + const { query, context, language } = copilotOptions; + + const decryptedAPIkey = await this.encryptionService.decryptColumnValue( + 'org_environment_variables', + orgnaizationId, + encryptedAPIKey + ); + + const response = await got(`${process.env.COPILOT_API_ENDPOINT}/copilot`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': decryptedAPIkey, + }, + body: JSON.stringify({ + query: query, + context: context, + language: language, + userId: userId, + }), + }); + + return { + data: JSON.parse(response.body), + status: response.statusCode, + }; + } + + async validateCopilotAPIKey(userId: string, secretKey: string) { + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ userId: userId, action: 'get', apiKey: secretKey }), + }; + + const response = await fetch(`${process.env.COPILOT_API_ENDPOINT}/api-key`, options); + const { isValid } = await response.json(); + + return { + statusCode: response.status, + status: isValid ? 'ok' : 'invalid', + }; + } +}