Feature - ToolJet Copilot 🚀 (#6074)

* 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
This commit is contained in:
Arpit 2023-05-11 15:04:48 +05:30 committed by GitHub
parent f215d1df96
commit 2ddbd3309e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 789 additions and 87 deletions

View file

@ -0,0 +1,9 @@
<svg width="65" height="64" viewBox="0 0 65 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.544189" width="64" height="64" rx="12" fill="#F0F4FF" />
<path d="M45.8777 40.3335C45.8777 43.0949 43.6391 45.3335 40.8777 45.3335C38.1163 45.3335 35.8777 43.0949 35.8777 40.3335C35.8777 37.5721 38.1163 35.3335 40.8777 35.3335C43.6391 35.3335 45.8777 37.5721 45.8777 40.3335Z" fill="#3E63DD" />
<g opacity="0.4">
<path d="M35.8776 22.0001C35.8776 20.1591 37.37 18.6667 39.2109 18.6667H42.5443C44.3852 18.6667 45.8776 20.1591 45.8776 22.0001V25.3334C45.8776 27.1744 44.3852 28.6667 42.5443 28.6667H39.2109C37.37 28.6667 35.8776 27.1744 35.8776 25.3334V22.0001Z" fill="#3E63DD" />
<path d="M19.2109 22.0001C19.2109 20.1591 20.7033 18.6667 22.5443 18.6667H25.8776C27.7186 18.6667 29.2109 20.1591 29.2109 22.0001V25.3334C29.2109 27.1744 27.7186 28.6667 25.8776 28.6667H22.5443C20.7033 28.6667 19.2109 27.1744 19.2109 25.3334V22.0001Z" fill="#3E63DD" />
<path d="M19.2109 38.6667C19.2109 36.8258 20.7033 35.3334 22.5443 35.3334H25.8776C27.7186 35.3334 29.2109 36.8258 29.2109 38.6667V42.0001C29.2109 43.841 27.7186 45.3334 25.8776 45.3334H22.5443C20.7033 45.3334 19.2109 43.841 19.2109 42.0001V38.6667Z" fill="#3E63DD" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.501388 6.82365L5.1666 0.570035C5.62616 -0.0460039 6.5687 0.294916 6.5687 1.07718V4.86215C6.5687 5.31662 6.91988 5.68503 7.35309 5.68503H8.88074C9.53456 5.68503 9.90141 6.47491 9.49845 7.01506L4.83324 13.2687C4.37367 13.8847 3.43114 13.5438 3.43114 12.7615V8.97655C3.43114 8.52209 3.07995 8.15367 2.64675 8.15367H1.1191C0.465279 8.15367 0.0984327 7.3638 0.501388 6.82365Z" fill="#3E63DD"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

View file

@ -0,0 +1,3 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.83341 3.41935C3.83341 2.22274 4.80346 1.25269 6.00008 1.25269C7.1967 1.25269 8.16675 2.22274 8.16675 3.41935V4.25269H3.83341V3.41935ZM2.83341 4.29948V3.41935C2.83341 1.67045 4.25118 0.252686 6.00008 0.252686C7.74898 0.252686 9.16675 1.67045 9.16675 3.41935V4.29948C10.4005 4.53352 11.3334 5.61749 11.3334 6.91935V10.9194C11.3334 12.3921 10.1395 13.586 8.66675 13.586H3.33341C1.86066 13.586 0.666748 12.3921 0.666748 10.9194V6.91935C0.666748 5.61749 1.59965 4.53352 2.83341 4.29948ZM7.33341 8.91935C7.33341 9.65573 6.73646 10.2527 6.00008 10.2527C5.2637 10.2527 4.66675 9.65573 4.66675 8.91935C4.66675 8.18297 5.2637 7.58602 6.00008 7.58602C6.73646 7.58602 7.33341 8.18297 7.33341 8.91935Z" fill="#46A758"/>
</svg>

After

Width:  |  Height:  |  Size: 861 B

View file

@ -1,14 +1,3 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="480.221px" height="480.221px" viewBox="0 0 480.221 480.221" style="enable-background:new 0 0 480.221 480.221;"
xml:space="preserve">
<g>
<path d="M480.158,260.878v166.979c0,28.874-23.501,52.363-52.381,52.363H52.453c-28.889,0-52.39-23.489-52.39-52.363V52.938
c0-28.874,23.501-52.369,52.39-52.369h167.434c-9.011,9.244-15.004,21.45-16.316,35.003H52.447
c-9.582,0-17.378,7.791-17.378,17.366v374.92c0,9.569,7.796,17.36,17.378,17.36h375.325c9.581,0,17.372-7.791,17.372-17.36V277.169
C458.33,275.904,470.56,270.236,480.158,260.878z M399.287,230.096H284.831L470.099,44.829c10.249-10.261,10.249-26.882,0-37.131
c-10.256-10.261-26.883-10.261-37.132-0.012L247.7,192.958V78.497c0-14.499-11.757-26.262-26.259-26.262
c-7.25,0-13.816,2.932-18.569,7.689c-4.752,4.765-7.693,11.325-7.693,18.572v177.854c0,14.499,11.754,26.256,26.256,26.256h177.852
c14.505,0,26.256-11.751,26.256-26.256S413.792,230.096,399.287,230.096z"/>
</g>
</svg>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.50008 0.166748H9.50008C10.7887 0.166748 11.8334 1.21142 11.8334 2.50008V9.50008C11.8334 10.7887 10.7887 11.8334 9.50008 11.8334H2.50008C1.21142 11.8334 0.166748 10.7887 0.166748 9.50008V2.50008C0.166748 1.21142 1.21142 0.166748 2.50008 0.166748ZM6.58341 2.64591C6.34179 2.64591 6.14591 2.84179 6.14591 3.08341V5.41675C6.14591 5.65837 6.34179 5.85425 6.58341 5.85425H8.91675C9.15837 5.85425 9.35425 5.65837 9.35425 5.41675C9.35425 5.17512 9.15837 4.97925 8.91675 4.97925H7.63963L9.22611 3.39277C9.39696 3.22192 9.39696 2.94491 9.22611 2.77406C9.05525 2.6032 8.77824 2.6032 8.60739 2.77406L7.02091 4.36053V3.08341C7.02091 2.84179 6.82504 2.64591 6.58341 2.64591ZM2.64591 6.58341C2.64591 6.82504 2.84179 7.02091 3.08341 7.02091H4.36053L2.77406 8.60739C2.6032 8.77824 2.6032 9.05525 2.77406 9.22611C2.94491 9.39696 3.22192 9.39696 3.39277 9.22611L4.97925 7.63963V8.91675C4.97925 9.15837 5.17512 9.35425 5.41675 9.35425C5.65837 9.35425 5.85425 9.15837 5.85425 8.91675V6.58341C5.85425 6.34179 5.65837 6.14591 5.41675 6.14591H3.08341C2.84179 6.14591 2.64591 6.34179 2.64591 6.58341Z" fill="#121212"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,14 +1,3 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="483.252px" height="483.252px" viewBox="0 0 483.252 483.252" style="enable-background:new 0 0 483.252 483.252;"
xml:space="preserve">
<g>
<path d="M481.354,263.904v166.979c0,28.88-23.507,52.369-52.387,52.369H53.646c-28.889,0-52.393-23.489-52.393-52.369V55.969
c0-28.877,23.504-52.372,52.393-52.372h167.428c-9.014,9.247-15.004,21.45-16.319,35.007H53.64c-9.582,0-17.377,7.79-17.377,17.365
v374.914c0,9.575,7.796,17.366,17.377,17.366h375.322c9.581,0,17.378-7.791,17.378-17.366V280.199
C459.515,278.935,471.744,273.267,481.354,263.904z M277.895,52.52h114.456L207.086,237.79c-10.255,10.249-10.255,26.882,0,37.132
c10.252,10.255,26.879,10.255,37.131,0.006L429.482,89.657v114.462c0,14.502,11.756,26.256,26.261,26.256
c7.247,0,13.813-2.929,18.566-7.687c4.752-4.764,7.689-11.319,7.689-18.569V26.256C481.999,11.754,470.249,0,455.743,0H277.895
c-14.499,0-26.256,11.754-26.256,26.262C251.633,40.764,263.396,52.52,277.895,52.52z"/>
</g>
</svg>
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.54419 0H2.54419C1.43962 0 0.544189 0.895431 0.544189 2V8C0.544189 9.10457 1.43962 10 2.54419 10H8.54419C9.64876 10 10.5442 9.10457 10.5442 8V2C10.5442 0.895431 9.64876 0 8.54419 0ZM5.66919 3C5.66919 3.20711 5.83708 3.375 6.04419 3.375H6.63886L3.91919 6.09467V5.5C3.91919 5.29289 3.7513 5.125 3.54419 5.125C3.33708 5.125 3.16919 5.29289 3.16919 5.5V7C3.16919 7.20711 3.33708 7.375 3.54419 7.375H5.04419C5.2513 7.375 5.41919 7.20711 5.41919 7C5.41919 6.79289 5.2513 6.625 5.04419 6.625H4.44952L7.16919 3.90533V4.5C7.16919 4.70711 7.33708 4.875 7.54419 4.875C7.7513 4.875 7.91919 4.70711 7.91919 4.5V3C7.91919 2.79289 7.7513 2.625 7.54419 2.625H6.04419C5.83708 2.625 5.66919 2.79289 5.66919 3Z" fill="#11181C"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 863 B

View file

@ -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 (
<div className="container-xl mt-3">
<div className="row">
<small className="text-green">
<img className="encrypted-icon" src="assets/images/icons/padlock2.svg" width="12" height="12" />
<span className="text-success mx-2 font-500">API KEY</span>
</small>
<div className="mb-3 col-6">
<input
disabled={!isCopilotEnabled}
type="password"
class="form-control mt-2"
name="example-text-input"
placeholder=""
value={inputValue}
onChange={handleOnchange}
/>
</div>
<div className="col-auto mt-1">
<Button
onClick={() => handleOnSave(inputValue)}
darkMode={darkMode}
size="md"
isLoading={isLoading}
styles={{ backgroundColor: '#3E63DD', color: '#fff' }}
disabled={!isCopilotEnabled}
>
<Button.Content title={'Save'} iconSrc={'assets/images/icons/save.svg'} />
</Button>
</div>
</div>
<div className="alert-container">
<Alert svg="alert-info" cls="copilot-alert" data-cy={`copilot-alert-info`}>
<h4 class="alert-title"> Don&apos;t have an API key?</h4>
<div class="text-muted">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>ToolJet Copilot </strong>
is currently in <strong style={{ fontWeight: 700, color: '#3E63DD' }}>beta</strong> 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.
</div>
<div className="mt-2 w-25">
<Button
onClick={() => window.open('https://tooljet.com/copilot', '_blank')}
darkMode={darkMode}
size="sm"
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={' Sign up for Beta Access'} />
</Button>
</div>
</Alert>
</div>
</div>
);
};

View file

@ -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 (
<div className="wrapper org-variables-page animation-fade">
<div className="page-wrapper">
<div
className="container-xl"
style={{
padding: '4rem',
}}
>
<Container isCopilotEnabled={state} handleCopilotToggle={handleCopilotToggle}>
<div className="row">
<div className="col-12">
<ApiKeyContainer
copilotApiKey={copilotApiKey}
handleOnSave={saveCopilotApiKey}
isLoading={isLoading}
darkMode={darkMode}
isCopilotEnabled={state}
/>
</div>
</div>
</Container>
</div>
</div>
</div>
);
};
const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode }) => {
return (
<div className="card p-2 card-container">
<div className="card-header row">
<div className="col-8 d-flex">
<h3 className="card-title">Copilot</h3>
<span className="badge bg-color-primary mx-2 mt-1">beta</span>
</div>
<div className="col">
<EducativeLebel darkMode={darkMode} />
</div>
</div>
<div className="card-body">
<div className="container-fluid">
<div className="d-flex flex-fill p-3">
<div className="mb-0 d-flex">
<CustomToggleSwitch
isChecked={isCopilotEnabled}
toggleSwitchFunction={handleCopilotToggle}
action="enableTransformation"
dataCy={'copilot'}
/>
<span className="mx-2 mt-3 font-weight-400 tranformation-label" data-cy={'label-query-transformation'}>
Enable Copilot
<small className="text-muted" style={{ display: 'block' }}>
Turn on Copilot functionality in your workspace
</small>
</span>
</div>
</div>
{children}
</div>
</div>
</div>
);
};
const EducativeLebel = ({ darkMode }) => {
const title = () => {
return (
<>
Learn about ToolJet <strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong>
</>
);
};
const popoverForRecommendation = (
<Popover id="transformation-popover-container">
<div className="transformation-popover card text-center">
<img src="/assets/images/icons/copilot.svg" alt="AI copilot" height={64} width={64} />
<div className="d-flex flex-column card-body">
<h4 className="mb-2">ToolJet x OpenAI</h4>
<p className="mb-2">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong> helps you write your queries
faster. It uses OpenAI&apos;s GPT-3.5 to suggest queries based on your data.
</p>
<Button
darkMode={darkMode}
size="sm"
classNames="default-secondary-button"
styles={{ width: '100%', fontSize: '12px', fontWeight: 700, borderColor: darkMode && 'transparent' }}
onClick={() => window.open('https://docs.tooljet.com/docs/tooljet-copilot', '_blank')}
>
<Button.Content title={'Read more'} />
</Button>
</div>
</div>
</Popover>
);
return (
<div className="d-flex justify-content-end">
<Button.UnstyledButton styles={{ height: '28px' }} darkMode={false} classNames="mx-1">
<Button.Content title={title} iconSrc={'assets/images/icons/flash.svg'} direction="left" />
</Button.UnstyledButton>
<OverlayTrigger trigger="click" placement="left" overlay={popoverForRecommendation} rootClose>
<svg
width="16.7"
height="16.7"
viewBox="0 0 20 21"
fill="#3E63DD"
xmlns="http://www.w3.org/2000/svg"
style={{ cursor: 'pointer' }}
data-cy={`transformation-info-icon`}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.5C5.58172 2.5 2 6.08172 2 10.5C2 14.9183 5.58172 18.5 10 18.5C14.4183 18.5 18 14.9183 18 10.5C18 6.08172 14.4183 2.5 10 2.5ZM0 10.5C0 4.97715 4.47715 0.5 10 0.5C15.5228 0.5 20 4.97715 20 10.5C20 16.0228 15.5228 20.5 10 20.5C4.47715 20.5 0 16.0228 0 10.5ZM9 6.5C9 5.94772 9.44771 5.5 10 5.5H10.01C10.5623 5.5 11.01 5.94772 11.01 6.5C11.01 7.05228 10.5623 7.5 10.01 7.5H10C9.44771 7.5 9 7.05228 9 6.5ZM8 10.5C8 9.94771 8.44772 9.5 9 9.5H10C10.5523 9.5 11 9.94771 11 10.5V13.5C11.5523 13.5 12 13.9477 12 14.5C12 15.0523 11.5523 15.5 11 15.5H10C9.44771 15.5 9 15.0523 9 14.5V11.5C8.44772 11.5 8 11.0523 8 10.5Z"
fill="#3E63DD"
/>
</svg>
</OverlayTrigger>
</div>
);
};

View file

@ -0,0 +1,3 @@
import { CopilotSetting } from './CopilotSetting';
export { CopilotSetting };

View file

@ -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'}
/>
)}
<CodeHinter.Portal
@ -331,6 +333,7 @@ export function CodeHinter({
darkMode={darkMode}
selectors={{ className: 'preview-block-portal' }}
dragResizePortal={true}
callgpt={callgpt}
>
<CodeMirror
value={typeof initialValue === 'string' ? initialValue : ''}
@ -382,7 +385,9 @@ export function CodeHinter({
);
}
const PopupIcon = ({ callback, icon, tip }) => {
const PopupIcon = ({ callback, icon, tip, transformation = false }) => {
const size = transformation ? 20 : 12;
return (
<div className="d-flex justify-content-end" style={{ position: 'relative' }}>
<OverlayTrigger
@ -394,8 +399,8 @@ const PopupIcon = ({ callback, icon, tip }) => {
<img
className="svg-icon m-2 popup-btn"
src={`assets/images/icons/${icon}.svg`}
width="12"
height="12"
width={size}
height={size}
onClick={(e) => {
e.stopPropagation();
callback();

View file

@ -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;
}

View file

@ -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]
</Popover>
);
return (
<div className="field transformation-editor">
<div className="align-items-center gap-2" style={{ display: 'flex', position: 'relative', height: '20px' }}>
<div className="mb-0">
<CustomToggleSwitch
isChecked={enableTransformation}
toggleSwitchFunction={toggleEnableTransformation}
action="enableTransformation"
const popoverForRecommendation = (
<Popover id="transformation-popover-container">
<div className="transformation-popover card text-center">
<img src="/assets/images/icons/copilot.svg" alt="AI copilot" height={64} width={64} />
<div className="d-flex flex-column card-body">
<h4 className="mb-2">ToolJet x OpenAI</h4>
<p className="mb-2">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong> helps you write your queries
faster. It uses OpenAI&apos;s GPT-3.5 to suggest queries based on your data.
</p>
<Button
onClick={() => window.open('https://docs.tooljet.com/docs/tooljet-copilot', '_blank')}
darkMode={darkMode}
dataCy={'transformation'}
/>
size="sm"
classNames="default-secondary-button"
styles={{ width: '100%', fontSize: '12px', fontWeight: 700, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={'Read more'} />
</Button>
</div>
<span className="mx-1 font-weight-400 tranformation-label" data-cy={'label-query-transformation'}>
{t('editor.queryManager.transformation.transformations', 'Transformations')}
</span>
<OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
</div>
</Popover>
);
const EducativeLebel = () => {
const title = () => {
return (
<>
Powered by <strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong>
</>
);
};
return (
<div className="d-flex">
<Button.UnstyledButton styles={{ height: '28px' }} darkMode={darkMode} classNames="mx-1">
<Button.Content title={title} iconSrc={'assets/images/icons/flash.svg'} direction="left" />
</Button.UnstyledButton>
<OverlayTrigger trigger="click" placement="left" overlay={popoverForRecommendation} rootClose>
<svg
width="16.7"
height="16.7"
@ -170,32 +209,99 @@ return [row for row in data if row['amount'] > 1000]
</svg>
</OverlayTrigger>
</div>
);
};
return (
<div className="field transformation-editor">
<div className="align-items-center gap-2" style={{ display: 'flex', position: 'relative', height: '20px' }}>
<div className="d-flex flex-fill">
<div className="mb-0">
<CustomToggleSwitch
isChecked={enableTransformation}
toggleSwitchFunction={toggleEnableTransformation}
action="enableTransformation"
darkMode={darkMode}
dataCy={'transformation'}
/>
</div>
<span className="mx-1 font-weight-400 tranformation-label" data-cy={'label-query-transformation'}>
{t('editor.queryManager.transformation.transformations', 'Transformations')}
</span>
<OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
<svg
width="16.7"
height="16.7"
viewBox="0 0 20 21"
fill="#3E63DD"
xmlns="http://www.w3.org/2000/svg"
style={{ cursor: 'pointer' }}
data-cy={`transformation-info-icon`}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.5C5.58172 2.5 2 6.08172 2 10.5C2 14.9183 5.58172 18.5 10 18.5C14.4183 18.5 18 14.9183 18 10.5C18 6.08172 14.4183 2.5 10 2.5ZM0 10.5C0 4.97715 4.47715 0.5 10 0.5C15.5228 0.5 20 4.97715 20 10.5C20 16.0228 15.5228 20.5 10 20.5C4.47715 20.5 0 16.0228 0 10.5ZM9 6.5C9 5.94772 9.44771 5.5 10 5.5H10.01C10.5623 5.5 11.01 5.94772 11.01 6.5C11.01 7.05228 10.5623 7.5 10.01 7.5H10C9.44771 7.5 9 7.05228 9 6.5ZM8 10.5C8 9.94771 8.44772 9.5 9 9.5H10C10.5523 9.5 11 9.94771 11 10.5V13.5C11.5523 13.5 12 13.9477 12 14.5C12 15.0523 11.5523 15.5 11 15.5H10C9.44771 15.5 9 15.0523 9 14.5V11.5C8.44772 11.5 8 11.0523 8 10.5Z"
fill="#3E63DD"
/>
</svg>
</OverlayTrigger>
</div>
<EducativeLebel />
</div>
<br></br>
{enableTransformation && (
<div
className="rounded-3"
style={{ marginLeft: '3rem', marginBottom: '20px', background: `${darkMode ? '#272822' : '#F8F9FA'}` }}
>
<div className="py-3 px-3 d-flex">
<div className="d-flex align-items-center border transformation-language-select-wrapper">
<span className="px-2">Language</span>
<div className="py-3 px-3 d-flex justify-content-between">
<div className="d-flex">
<div className="d-flex align-items-center border transformation-language-select-wrapper">
<span className="px-2">Language</span>
</div>
<Select
options={[
{ name: 'JavaScript', value: 'javascript' },
{ name: 'Python', value: 'python' },
]}
value={lang}
search={true}
onChange={(value) => {
setLang(value);
changeOption('transformationLanguage', value);
changeOption('transformation', state[value]);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={computeSelectStyles(darkMode, 140)}
useCustomStyles={true}
/>
</div>
<Select
options={[
{ name: 'JavaScript', value: 'javascript' },
{ name: 'Python', value: 'python' },
]}
value={lang}
search={true}
onChange={(value) => {
setLang(value);
changeOption('transformationLanguage', value);
changeOption('transformation', state[value]);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={computeSelectStyles(darkMode, 140)}
useCustomStyles={true}
/>
<div
data-tooltip-id="tooltip-for-active-copilot"
data-tooltip-content="Activate Copilot in the workspace settings"
>
<Button
onClick={handleCallToGPT}
darkMode={darkMode}
size="sm"
classNames={`${fetchingRecommendation ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`}
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
disabled={!isCopilotEnabled}
>
<Button.Content title={'Generate code ⌘+L'} />
</Button>
</div>
{!isCopilotEnabled && (
<ReactTooltip
id="tooltip-for-active-copilot"
className="tooltip"
style={{ backgroundColor: '#e6eefe', color: '#222' }}
/>
)}
</div>
<div className="border-top mx-3"></div>
<CodeHinter
@ -205,11 +311,12 @@ return [row for row in data if row['amount'] > 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}
/>
</div>
)}

View file

@ -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,
});
});

View file

@ -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' && <ManageGroupPermissions darkMode={props.darkMode} />}
{selectedTab === 'manageSSO' && <ManageSSO />}
{selectedTab === 'manageEnvVars' && <ManageOrgVars darkMode={props.darkMode} />}
{selectedTab === 'manageCopilot' && <CopilotSetting />}
</div>
</div>
</div>

View file

@ -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}
</Portal.Modal>
@ -55,7 +57,18 @@ const Container = ({ children, ...restProps }) => {
return <ReactPortal {...restProps}>{children}</ReactPortal>;
};
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 = () => (
<div className="modal-content" style={{ ...portalStyles, ...styles }}>
<div
@ -63,22 +76,39 @@ const Modal = ({ children, handleClose, portalStyles, styles, componentName, dar
style={{ ...portalStyles }}
>
<div className="w-100">
<code className="mx-2 text-info">{componentName ?? 'Editor'}</code>
<span
style={{
textTransform: 'none',
}}
className="badge tj-badge"
>
{componentName ?? 'Editor'}
</span>
</div>
<button
type="button"
className="btn mx-2 btn-light"
{includeGPT && (
<div className="mx-2">
<Button
onClick={handleCallGpt}
darkMode={darkMode}
size="sm"
classNames={`${loading ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`}
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={'Generate code ⌘+L'} />
</Button>
</div>
)}
<Button
title={'close'}
onClick={handleClose}
style={{ backgroundColor: darkMode && '#42546a' }}
darkMode={darkMode}
size="sm"
styles={{ width: '50px', padding: '2px' }}
>
<img
style={{ transform: 'rotate(-90deg)', filter: darkMode && 'brightness(0) invert(1)' }}
src="assets/images/icons/portal-close.svg"
width="12"
height="12"
/>
</button>
<Button.Content iconSrc={'assets/images/icons/portal-close.svg'} direction="left" />
</Button>
</div>
<div
className={`modal-body ${darkMode ? 'dark-mode-border' : ''}`}

View file

@ -12,6 +12,7 @@ const usePortal = ({ children, ...restProps }) => {
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}
>
<div className={`editor-container ${optionalProps.cls ?? ''}`} key={key}>
{React.cloneElement(children, { ...styleProps })}

View file

@ -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);
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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()
) : (
<span data-cy={`${String(title).toLowerCase().replace(/\s+/g, '-')}-option-button`} className="mx-1">
<span data-cy={`${String(btnTitle).toLowerCase().replace(/\s+/g, '-')}-option-button`} className="mx-1">
{title}
</span>
);
@ -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 (
<div
type="button"
style={{ ...styles, ...(disabled ? defaultDisabledStyles : {}) }}
className={`unstyled-button ${classNames} ${disabled && 'disabled'}`}
style={{ ...styles, ...(disabled ? defaultDisabledStyles : {}), ...cursorNotPointer }}
className={`unstyled-button ${classNames} ${disabled && 'disabled'} ${darkMode && 'dark'}`}
onClick={onClick}
>
{children}

View file

@ -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",

View file

@ -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') {

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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 {}

View file

@ -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',
};
}
}