update and refactor actions and interactions
This commit is contained in:
parent
3111f56571
commit
fc403142c2
@ -1,215 +0,0 @@
|
|||||||
// import React, { useEffect, useState, useRef } from 'react';
|
|
||||||
// import { Button } from 'primereact/button';
|
|
||||||
// import { Toast } from 'primereact/toast';
|
|
||||||
// import { api } from '../services/axios';
|
|
||||||
// import { Dialog } from 'primereact/dialog';
|
|
||||||
// import { useNavigate } from 'react-router-dom';
|
|
||||||
// import { useSettingsStore } from '../state/stores/useSettingsStore';
|
|
||||||
|
|
||||||
// type UserInfo = {
|
|
||||||
// role: string;
|
|
||||||
// tenantId: string | number;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const ActivateConnectionModal = () => {
|
|
||||||
// const [loading, setLoading] = useState(false);
|
|
||||||
// const [showDialog, setShowDialog] = useState(false);
|
|
||||||
// const toast = useRef<Toast>(null);
|
|
||||||
// const [fetched, setFetched] = useState(false);
|
|
||||||
// const [systemName, setSystemName] = useState('');
|
|
||||||
// const tenantId = localStorage.getItem('tenantId');
|
|
||||||
// const systemId = localStorage.getItem('systemId');
|
|
||||||
|
|
||||||
// const navigate = useNavigate();
|
|
||||||
|
|
||||||
// const [userInfo, setUserInfo] = useState<UserInfo>({
|
|
||||||
// role: '',
|
|
||||||
// tenantId: -1,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const [messageShown, setMessageShown] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// const { setConnectionId } = useSettingsStore();
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// // TODO: similar logic used in other components, refactor into user context/state/hook
|
|
||||||
// const storedUser = localStorage.getItem('user');
|
|
||||||
// const storedSystemName = localStorage.getItem('systemName');
|
|
||||||
|
|
||||||
// if (storedUser) {
|
|
||||||
// const parsedUser = JSON.parse(storedUser);
|
|
||||||
|
|
||||||
// let role = parsedUser.roles?.includes('admin')
|
|
||||||
// ? 'admin'
|
|
||||||
// : parsedUser.roles?.includes('user')
|
|
||||||
// ? 'user'
|
|
||||||
// : '';
|
|
||||||
|
|
||||||
// let tenantId =
|
|
||||||
// role === 'admin' ? localStorage.getItem('tenantId') || -1 : -1;
|
|
||||||
|
|
||||||
// let userInfo: UserInfo = {
|
|
||||||
// role: role,
|
|
||||||
// tenantId: tenantId,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// setUserInfo(userInfo);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (storedSystemName) {
|
|
||||||
// setSystemName(storedSystemName);
|
|
||||||
// }
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (userInfo.role && userInfo.tenantId !== undefined && !fetched) {
|
|
||||||
// fetchConnection();
|
|
||||||
// }
|
|
||||||
// }, [userInfo]);
|
|
||||||
|
|
||||||
// const fetchConnection = async () => {
|
|
||||||
// setLoading(true);
|
|
||||||
// try {
|
|
||||||
// let response: any;
|
|
||||||
|
|
||||||
// if (userInfo.role === 'user') {
|
|
||||||
// response = await api('get', `/connections?systemName=${systemName}`);
|
|
||||||
// } else if (userInfo.role === 'admin' && userInfo.tenantId) {
|
|
||||||
// response = await api(
|
|
||||||
// 'get',
|
|
||||||
// `/connections?systemName=${systemName}&tenantId=${userInfo.tenantId}`,
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// throw new Error('Invalid user role or missing tenantId');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const connections = response.data.data;
|
|
||||||
|
|
||||||
// // Localstorage saves undefined as string
|
|
||||||
// if (tenantId === 'undefined' || systemId === 'undefined') {
|
|
||||||
// navigate('/systems');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!connections || connections.length === 0) {
|
|
||||||
// setShowDialog(true);
|
|
||||||
// } else if (userInfo.role === 'user') {
|
|
||||||
// setUserInfo((prevUserInfo) => ({
|
|
||||||
// ...prevUserInfo,
|
|
||||||
// tenantId: connections[0].tenant_fleks_id,
|
|
||||||
// }));
|
|
||||||
// }
|
|
||||||
// const connectionId = connections[0].id;
|
|
||||||
// localStorage.setItem('connectionId', connectionId);
|
|
||||||
// setConnectionId(connectionId);
|
|
||||||
// navigate(`/dashboard/${systemName}`);
|
|
||||||
// setLoading(false);
|
|
||||||
// setFetched(true);
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Error:', error);
|
|
||||||
// setLoading(false);
|
|
||||||
// if (!messageShown) {
|
|
||||||
// setMessageShown(true);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleActivateClick = async () => {
|
|
||||||
// setLoading(true);
|
|
||||||
// const systemId = localStorage.getItem('systemId') || null;
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// let postResponse: any;
|
|
||||||
// let requestBody: any = {
|
|
||||||
// system_id: Number(systemId),
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (!systemId) {
|
|
||||||
// throw new Error('Missing systemId');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (userInfo.role === 'admin' && userInfo.tenantId) {
|
|
||||||
// requestBody.tenant_id = userInfo.tenantId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// postResponse = await api('post', '/connections', requestBody);
|
|
||||||
|
|
||||||
// if (postResponse.status === 201) {
|
|
||||||
// const connectionId = postResponse.data.id;
|
|
||||||
// localStorage.setItem('connectionId', connectionId);
|
|
||||||
// setConnectionId(connectionId);
|
|
||||||
|
|
||||||
// navigate(`/dashboard/${systemName}`);
|
|
||||||
|
|
||||||
// setLoading(false);
|
|
||||||
// setShowDialog(false);
|
|
||||||
// toast.current.show({
|
|
||||||
// severity: 'success',
|
|
||||||
// summary: 'Success',
|
|
||||||
// detail: 'Connection established successfully.',
|
|
||||||
// life: 3000,
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// setLoading(false);
|
|
||||||
// toast.current.show({
|
|
||||||
// severity: 'error',
|
|
||||||
// summary: 'Error',
|
|
||||||
// detail: 'Failed to establish connection.',
|
|
||||||
// life: 3000,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// if (error.message === 'Missing systemId') {
|
|
||||||
// toast.current.show({
|
|
||||||
// severity: 'error',
|
|
||||||
// summary: 'Error',
|
|
||||||
// detail: 'Missing system information. Please verify.',
|
|
||||||
// life: 3000,
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// console.error('Error:', error);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleCancelClick = () => {
|
|
||||||
// setShowDialog(false);
|
|
||||||
// navigate('/systems');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className="flex items-center justify-center h-screen overflow-y-hidden bg-gradient-to-br from-purple-500 to-indigo-500">
|
|
||||||
// <div className="fixed inset-0 flex items-center justify-center overflow-y-hidden">
|
|
||||||
// <div className="transition-transform" style={{ transform: 'none' }}>
|
|
||||||
// <Dialog
|
|
||||||
// visible={showDialog}
|
|
||||||
// onHide={() => setShowDialog(false)}
|
|
||||||
// header="Activate Connection"
|
|
||||||
// footer={
|
|
||||||
// <div>
|
|
||||||
// <Button
|
|
||||||
// label="Activate"
|
|
||||||
// className="p-button-primary"
|
|
||||||
// onClick={handleActivateClick}
|
|
||||||
// disabled={loading}
|
|
||||||
// />
|
|
||||||
// <Button
|
|
||||||
// label="Cancel"
|
|
||||||
// className="p-button-secondary"
|
|
||||||
// onClick={handleCancelClick}
|
|
||||||
// disabled={loading}
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// }
|
|
||||||
// >
|
|
||||||
// {loading ? (
|
|
||||||
// <p>Performing the post request, please wait...</p>
|
|
||||||
// ) : (
|
|
||||||
// <p>Are you sure you want to activate the connection?</p>
|
|
||||||
// )}
|
|
||||||
// </Dialog>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
// export default ActivateConnectionModal;
|
|
@ -1,372 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { DataTable } from 'primereact/datatable';
|
|
||||||
import { Column } from 'primereact/column';
|
|
||||||
import { Card } from 'primereact/card';
|
|
||||||
|
|
||||||
interface CanvasData {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CanvasDataTableProps {
|
|
||||||
data: CanvasData[];
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CanvasDataTable: React.FC<CanvasDataTableProps> = ({
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
errorimport { Column } from 'primereact/column';
|
|
||||||
import { FilterMatchMode, FilterOperator } from 'primereact/api';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { api } from '../../services/api.ts';
|
|
||||||
|
|
||||||
import TableGroupHeader from '../../shared/components/_V1/TableGroupHeader.tsx';
|
|
||||||
import TableGroup from '../../shared/components/_V1/TableGroup.tsx';
|
|
||||||
|
|
||||||
import { Toast } from 'primereact/toast';
|
|
||||||
import ConfirmModal from '../../shared/components/modals/ConfirmModal.tsx';
|
|
||||||
import CreateVoucherModal from './CreateVoucherModal.tsx';
|
|
||||||
// import EditVoucherModal from './AddVoucherCodeModal.tsx';
|
|
||||||
import LoadingPage from '../../shared/components/_V1/LoadingPage.tsx';
|
|
||||||
import DetailModal from './DetailModal.tsx';
|
|
||||||
import AddVoucherCodeModal, { AddVoucherCodeModalProps } from './AddVoucherCodeModal.tsx';
|
|
||||||
import { Voucher, useVoucherStore } from '../../state/stores/Mews/useMewsVoucherStore.ts';
|
|
||||||
import { TSizeOptionValue } from '../../types/DataTable.types.ts';
|
|
||||||
import Button from '../../shared/components/_V2/Button.tsx';
|
|
||||||
|
|
||||||
export default function VoucherTableGroup() {
|
|
||||||
const { createVoucher, fetchVouchers, updateVoucher, vouchers, addVoucherCode } =
|
|
||||||
useVoucherStore();
|
|
||||||
|
|
||||||
const [filters, setFilters] = useState(null);
|
|
||||||
const [globalFilterValue, setGlobalFilterValue] = useState('');
|
|
||||||
|
|
||||||
const [showCreateVoucherModal, setShowCreateVoucherModal] = useState(false);
|
|
||||||
const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false);
|
|
||||||
|
|
||||||
const [showAddCodeModal, setShowAddCodeModal] = useState(false);
|
|
||||||
const [currentVoucherForCode, setCurrentVoucherForCode] = useState(null);
|
|
||||||
|
|
||||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
|
||||||
const [detailContent, setDetailContent] = useState([]);
|
|
||||||
const [detailTitle, setDetailTitle] = useState('');
|
|
||||||
|
|
||||||
const [selectedVouchers, setSelectedVouchers] = useState<Voucher[]>(null);
|
|
||||||
|
|
||||||
const toast = useRef<Toast>(null);
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
// Change table row size
|
|
||||||
const sizeOptions: { label: string; value: TSizeOptionValue }[] = [
|
|
||||||
{ label: 'Small', value: 'small' },
|
|
||||||
{ label: 'Normal', value: 'normal' },
|
|
||||||
{ label: 'Large', value: 'large' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const [size, setSize] = useState<TSizeOptionValue>(sizeOptions[0].value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initFilters();
|
|
||||||
// fetchVouchers;
|
|
||||||
handleRefresh();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const showToastMessage = (options) => {
|
|
||||||
toast.current?.show(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initFilters = () => {
|
|
||||||
setFilters({
|
|
||||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
|
||||||
representative: { value: null, matchMode: FilterMatchMode.IN },
|
|
||||||
createdAt: {
|
|
||||||
operator: FilterOperator.AND,
|
|
||||||
constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setGlobalFilterValue('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFilter = () => {
|
|
||||||
initFilters();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGlobalFilterChange = (e: { target: { value: any } }) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
// @ts-ignore
|
|
||||||
let _filters = { ...filters };
|
|
||||||
_filters['global'].value = value;
|
|
||||||
setFilters(_filters);
|
|
||||||
setGlobalFilterValue(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: refresh data
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
await fetchVouchers();
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (value: string | number | Date) => {
|
|
||||||
const date = new Date(value);
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateBodyTemplate = (rowData: { createdAt: string | number | Date }) => {
|
|
||||||
return formatDate(rowData.createdAt);
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionButtons = () => {
|
|
||||||
return (
|
|
||||||
<div className={'flex gap-3'}>
|
|
||||||
<Button text={true} onClick={() => setShowCreateVoucherModal(true)}>
|
|
||||||
Add Voucher
|
|
||||||
</Button>
|
|
||||||
{/* <Button
|
|
||||||
onClick={() => setShowConfirmDeleteModal(true)}
|
|
||||||
severity={'danger'}
|
|
||||||
disabled={selectedVouchers == null || selectedVouchers.length === 0}
|
|
||||||
>
|
|
||||||
Delete selected
|
|
||||||
</Button> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const editButton = (rowData: Voucher) => {
|
|
||||||
return <Button>action</Button>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDetailModal = (content, title) => {
|
|
||||||
setDetailContent(content);
|
|
||||||
setDetailTitle(title);
|
|
||||||
setDetailModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDetailModal = () => {
|
|
||||||
setDetailModalVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateVoucher = async (newVoucher: Voucher) => {
|
|
||||||
await createVoucher(newVoucher);
|
|
||||||
handleRefresh();
|
|
||||||
setShowCreateVoucherModal(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteSelectedVouchers = () => {
|
|
||||||
const URL = '/neppeURL';
|
|
||||||
const body = selectedVouchers.map((voucher) => {
|
|
||||||
return {
|
|
||||||
id: voucher.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
api('delete', URL, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
const voucherRatesBodyTemplate = (rowData) => {
|
|
||||||
if (rowData?.voucherCodes.length < 1) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const details = rowData.rates.map((rate) => `Name: ${rate.name}, Type: ${rate.type}`);
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
label='View Rates'
|
|
||||||
onClick={() => openDetailModal(details, 'Voucher Rates')}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const voucherCodesBodyTemplate = (rowData) => {
|
|
||||||
const details = rowData.voucherCodes.join(', ');
|
|
||||||
return (
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<Button
|
|
||||||
label='View Codes'
|
|
||||||
onClick={() => openDetailModal([details], 'Voucher Codes')}
|
|
||||||
disabled={isLoading || rowData.voucherCodes.length === 0}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label='Add Code'
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentVoucherForCode(rowData);
|
|
||||||
setShowAddCodeModal(true);
|
|
||||||
}}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddVoucherCode: AddVoucherCodeModalProps['addVoucherCode'] = async (
|
|
||||||
voucherId,
|
|
||||||
code
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await addVoucherCode(voucherId, code);
|
|
||||||
showToastMessage({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Code Added',
|
|
||||||
detail: 'Voucher code has been successfully added.',
|
|
||||||
});
|
|
||||||
setShowAddCodeModal(false);
|
|
||||||
handleRefresh();
|
|
||||||
} catch (error) {
|
|
||||||
showToastMessage({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: 'Failed to add voucher code.',
|
|
||||||
});
|
|
||||||
console.error('Failed to add voucher code:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeader = () => {
|
|
||||||
return (
|
|
||||||
<TableGroupHeader
|
|
||||||
size={size}
|
|
||||||
setSize={setSize}
|
|
||||||
sizeOptions={sizeOptions}
|
|
||||||
globalFilterValue={globalFilterValue}
|
|
||||||
onGlobalFilterChange={(e: any) => onGlobalFilterChange(e)}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
clearFilter={clearFilter}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const voucherCodeSortFunction = (e) => {
|
|
||||||
const result = e.data.sort((data1, data2) => {
|
|
||||||
const codes1 = data1.voucherCodes.length;
|
|
||||||
const codes2 = data2.voucherCodes.length;
|
|
||||||
|
|
||||||
return e.order * (codes1 - codes2);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full h-full flex flex-col'>
|
|
||||||
<CreateVoucherModal
|
|
||||||
visible={showCreateVoucherModal}
|
|
||||||
onHide={() => setShowCreateVoucherModal(false)}
|
|
||||||
// @ts-ignore
|
|
||||||
onConfirm={handleCreateVoucher}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AddVoucherCodeModal
|
|
||||||
visible={showAddCodeModal}
|
|
||||||
onHide={() => setShowAddCodeModal(false)}
|
|
||||||
addVoucherCode={handleAddVoucherCode} // Pass the function directly
|
|
||||||
voucherId={currentVoucherForCode?.id || ''}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
visible={showConfirmDeleteModal}
|
|
||||||
header={`Delete ${selectedVouchers?.length} voucher(s)`}
|
|
||||||
message={`Are you sure?`}
|
|
||||||
onHide={() => setShowConfirmDeleteModal(false)}
|
|
||||||
onConfirm={handleDeleteSelectedVouchers}
|
|
||||||
confirmLabel={'Confirm'}
|
|
||||||
cancelLabel={'Cancel'}
|
|
||||||
onCancel={() => setShowConfirmDeleteModal(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailModal
|
|
||||||
visible={detailModalVisible}
|
|
||||||
onHide={closeDetailModal}
|
|
||||||
title={detailTitle}
|
|
||||||
details={detailContent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<LoadingPage />
|
|
||||||
) : (
|
|
||||||
<TableGroup
|
|
||||||
body={vouchers}
|
|
||||||
size={size}
|
|
||||||
header={renderHeader}
|
|
||||||
filters={filters}
|
|
||||||
rows={10}
|
|
||||||
paginator
|
|
||||||
// stripedRows={true}
|
|
||||||
showGridlines={true}
|
|
||||||
removableSort={true}
|
|
||||||
dragSelection={false}
|
|
||||||
selectionMode={'multiple'}
|
|
||||||
selection={selectedVouchers}
|
|
||||||
onSelectionChange={(e: any) => setSelectedVouchers(e.value)}
|
|
||||||
emptyMessage='No vouchers found.'
|
|
||||||
>
|
|
||||||
<Column selectionMode='multiple' headerStyle={{ width: '3rem' }}></Column>
|
|
||||||
|
|
||||||
<Column field='id' header={actionButtons} sortable filter style={{ width: '40%' }} />
|
|
||||||
<Column field='name' header='Name' sortable filter style={{ width: '20%' }} />
|
|
||||||
<Column field='type' header='Type' sortable filter style={{ width: '20%' }} />
|
|
||||||
<Column
|
|
||||||
field='voucherCodes'
|
|
||||||
header='Voucher Codes'
|
|
||||||
body={voucherCodesBodyTemplate}
|
|
||||||
sortable
|
|
||||||
filter
|
|
||||||
sortFunction={voucherCodeSortFunction}
|
|
||||||
className='h-full align-top bg-red'
|
|
||||||
style={{ width: '20%', textAlign: 'center', lineHeight: '20px' }}
|
|
||||||
/>
|
|
||||||
<Column
|
|
||||||
field='rates'
|
|
||||||
header='Voucher Rates'
|
|
||||||
body={voucherRatesBodyTemplate}
|
|
||||||
sortable
|
|
||||||
filter
|
|
||||||
style={{ width: '20%' }}
|
|
||||||
/>
|
|
||||||
{/* <Column
|
|
||||||
field='actions'
|
|
||||||
header='Actions'
|
|
||||||
body={editButton}
|
|
||||||
style={{ width: '15%', textAlign: 'center' }}
|
|
||||||
/> */}
|
|
||||||
</TableGroup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}) => {
|
|
||||||
// Dynamically generate columns based on the first data item
|
|
||||||
const columns = data.length > 0
|
|
||||||
? Object.keys(data[0]).map(key => (
|
|
||||||
<Column
|
|
||||||
key={key}
|
|
||||||
field={key}
|
|
||||||
header={key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' ')}
|
|
||||||
sortable
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="flex-grow mb-4">
|
|
||||||
<DataTable
|
|
||||||
value={data}
|
|
||||||
loading={loading}
|
|
||||||
emptyMessage={error || "No data found. Try using the prompt below."}
|
|
||||||
className="p-datatable-sm"
|
|
||||||
paginator
|
|
||||||
rows={10}
|
|
||||||
rowsPerPageOptions={[10, 25, 50]}
|
|
||||||
>
|
|
||||||
{columns}
|
|
||||||
</DataTable>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CanvasDataTable;
|
|
@ -1,120 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Dialog } from 'primereact/dialog';
|
|
||||||
import { InputText } from 'primereact/inputtext';
|
|
||||||
import { Button } from 'primereact/button';
|
|
||||||
import { ScrollPanel } from 'primereact/scrollpanel';
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
role: 'user' | 'assistant';
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatGptModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
onHide: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatGptModal: React.FC<ChatGptModalProps> = ({ visible, onHide }) => {
|
|
||||||
const [prompt, setPrompt] = useState('');
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!prompt.trim()) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
const userMessage: Message = { role: 'user', content: prompt };
|
|
||||||
setMessages(prev => [...prev, userMessage]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/canvas-api/chatgpt/completions', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
data: prompt,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.errorMessage) {
|
|
||||||
throw new Error(data.errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const assistantMessage: Message = {
|
|
||||||
role: 'assistant',
|
|
||||||
content: typeof data.responseText === 'string'
|
|
||||||
? data.responseText
|
|
||||||
: JSON.stringify(data.responseText, null, 2)
|
|
||||||
};
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, assistantMessage]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
const errorMessage: Message = {
|
|
||||||
role: 'assistant',
|
|
||||||
content: `Error: ${error instanceof Error ? error.message : 'Failed to get response'}`
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, errorMessage]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
setPrompt('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
visible={visible}
|
|
||||||
onHide={onHide}
|
|
||||||
header="ChatGPT Assistant"
|
|
||||||
style={{ width: '50vw' }}
|
|
||||||
className="chatgpt-modal"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col h-[60vh]">
|
|
||||||
<ScrollPanel style={{ width: '100%', height: 'calc(100% - 60px)' }} className="mb-4">
|
|
||||||
<div className="flex flex-col gap-4 p-4">
|
|
||||||
{messages.map((message, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`p-3 rounded-lg max-w-[80%] ${message.role === 'user'
|
|
||||||
? 'bg-blue-100 ml-auto'
|
|
||||||
: 'bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="whitespace-pre-wrap">{message.content}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollPanel>
|
|
||||||
|
|
||||||
<div className="flex gap-2 mt-auto">
|
|
||||||
<InputText
|
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
placeholder="Type your message..."
|
|
||||||
className="flex-1"
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon="pi pi-send"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
loading={loading}
|
|
||||||
disabled={!prompt.trim() || loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatGptModal;
|
|
@ -1,37 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { InputText } from 'primereact/inputtext';
|
|
||||||
import { Button } from 'primereact/button';
|
|
||||||
|
|
||||||
interface PromptInputProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
onSubmit: () => void;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PromptInput: React.FC<PromptInputProps> = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onSubmit,
|
|
||||||
placeholder = "Type your prompt here"
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 p-4 border-t">
|
|
||||||
<InputText
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
className="flex-grow"
|
|
||||||
onKeyPress={(e) => e.key === 'Enter' && onSubmit()}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Send"
|
|
||||||
icon="pi pi-send"
|
|
||||||
onClick={onSubmit}
|
|
||||||
disabled={!value.trim()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PromptInput;
|
|
@ -1,33 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Dropdown } from 'primereact/dropdown';
|
|
||||||
|
|
||||||
export type SystemType = 'council' | 'fusemind' | 'canvas';
|
|
||||||
|
|
||||||
interface SystemSelectorProps {
|
|
||||||
value: SystemType | null;
|
|
||||||
onChange: (value: SystemType | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemOptions = [
|
|
||||||
{ label: 'CouncilAI', value: 'council' },
|
|
||||||
{ label: 'FuseMind (AI)', value: 'fusemind' },
|
|
||||||
{ label: 'Canvas API', value: 'canvas' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const SystemSelector: React.FC<SystemSelectorProps> = ({ value, onChange }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 p-4 border-b w-80">
|
|
||||||
<span className="font-semibold mb-1">Apps & Flows</span>
|
|
||||||
<Dropdown
|
|
||||||
value={value}
|
|
||||||
options={systemOptions}
|
|
||||||
onChange={(e) => onChange(e.value)}
|
|
||||||
placeholder="Select an integration"
|
|
||||||
className="w-full"
|
|
||||||
showClear
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemSelector;
|
|
@ -1,83 +0,0 @@
|
|||||||
import { SystemType } from './SystemSelector';
|
|
||||||
|
|
||||||
interface CommandResult {
|
|
||||||
system: SystemType;
|
|
||||||
endpoint: string;
|
|
||||||
params?: Record<string, any>;
|
|
||||||
courseId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseCommand = (prompt: string, system: SystemType): CommandResult | null => {
|
|
||||||
const lowerPrompt = prompt.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (system === 'canvas') {
|
|
||||||
// Extract course ID if present (e.g., "get students in course 123")
|
|
||||||
const courseIdMatch = lowerPrompt.match(/course\s+(\d+)/);
|
|
||||||
const courseId = courseIdMatch ? parseInt(courseIdMatch[1], 10) : 1; // Default to 1 if not specified
|
|
||||||
|
|
||||||
if (lowerPrompt.includes('get all students')) {
|
|
||||||
return {
|
|
||||||
system: 'canvas',
|
|
||||||
endpoint: '/users',
|
|
||||||
params: {
|
|
||||||
enrollment_type: ['student'],
|
|
||||||
per_page: 100
|
|
||||||
},
|
|
||||||
courseId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowerPrompt.includes('get courses')) {
|
|
||||||
return {
|
|
||||||
system: 'canvas',
|
|
||||||
endpoint: '/courses',
|
|
||||||
params: {
|
|
||||||
per_page: 100
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowerPrompt.includes('get assignments')) {
|
|
||||||
return {
|
|
||||||
system: 'canvas',
|
|
||||||
endpoint: '/assignments',
|
|
||||||
params: {
|
|
||||||
per_page: 100
|
|
||||||
},
|
|
||||||
courseId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (system === 'council') {
|
|
||||||
if (lowerPrompt.includes('get all students')) {
|
|
||||||
return {
|
|
||||||
system: 'council',
|
|
||||||
endpoint: '/students',
|
|
||||||
params: {
|
|
||||||
limit: 100
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowerPrompt.includes('get courses')) {
|
|
||||||
return {
|
|
||||||
system: 'council',
|
|
||||||
endpoint: '/courses',
|
|
||||||
params: {
|
|
||||||
limit: 100
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowerPrompt.includes('get assignments')) {
|
|
||||||
return {
|
|
||||||
system: 'council',
|
|
||||||
endpoint: '/assignments',
|
|
||||||
params: {
|
|
||||||
limit: 100
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
@ -1,90 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Button } from 'primereact/button';
|
|
||||||
import { Column } from 'primereact/column';
|
|
||||||
import { FilterMatchMode } from 'primereact/api';
|
|
||||||
import { TSizeOptionValue } from '../../types/DataTable.types';
|
|
||||||
import { api } from '../../services/api';
|
|
||||||
import ChatGptModal from './ChatGptModal';
|
|
||||||
import { DataTable } from 'primereact/datatable';
|
|
||||||
import { Card } from 'primereact/card';
|
|
||||||
|
|
||||||
const CanvasEndpoints: React.FC = () => {
|
|
||||||
const [showChatGptModal, setShowChatGptModal] = useState(false);
|
|
||||||
const [endpoints, setEndpoints] = useState<any[]>([]);
|
|
||||||
const [filters, setFilters] = useState<any>(null);
|
|
||||||
const [globalFilterValue, setGlobalFilterValue] = useState('');
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
||||||
const [newEndpoint, setNewEndpoint] = useState({ name: '', method: '', path: '', description: '' });
|
|
||||||
const sizeOptions: { label: string; value: TSizeOptionValue }[] = [
|
|
||||||
{ label: 'Small', value: 'small' },
|
|
||||||
{ label: 'Normal', value: 'normal' },
|
|
||||||
{ label: 'Large', value: 'large' },
|
|
||||||
];
|
|
||||||
const [size, setSize] = useState<TSizeOptionValue>(sizeOptions[0].value);
|
|
||||||
|
|
||||||
const fetchEndpoints = async () => {
|
|
||||||
try {
|
|
||||||
const response = await api('get', '/api/v1/canvas-api/endpoints');
|
|
||||||
setEndpoints(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch endpoints:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initFilters = () => {
|
|
||||||
setFilters({
|
|
||||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
|
||||||
});
|
|
||||||
setGlobalFilterValue('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFilter = () => {
|
|
||||||
initFilters();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGlobalFilterChange = (e: { target: { value: any } }) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
let _filters = { ...filters };
|
|
||||||
_filters['global'].value = value;
|
|
||||||
setFilters(_filters);
|
|
||||||
setGlobalFilterValue(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<Card className="mb-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold mb-2">Canvas Endpoints</h1>
|
|
||||||
<p className="text-gray-400">Manage and test your Canvas API endpoints</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
label="Create Endpoint"
|
|
||||||
icon="pi pi-plus"
|
|
||||||
onClick={() => setShowCreateModal(true)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Ask ChatGPT"
|
|
||||||
icon="pi pi-comments"
|
|
||||||
onClick={() => setShowChatGptModal(true)}
|
|
||||||
className="p-button-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<DataTable value={endpoints} paginator rows={10} showGridlines responsiveLayout="scroll" emptyMessage="No endpoints found.">
|
|
||||||
<Column field="id" header="ID" sortable filter style={{ width: '10%' }} />
|
|
||||||
<Column field="name" header="Name" sortable filter style={{ width: '20%' }} />
|
|
||||||
<Column field="method" header="Method" sortable filter style={{ width: '10%' }} />
|
|
||||||
<Column field="path" header="Path" sortable filter style={{ width: '40%' }} />
|
|
||||||
</DataTable>
|
|
||||||
<ChatGptModal
|
|
||||||
visible={showChatGptModal}
|
|
||||||
onHide={() => setShowChatGptModal(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CanvasEndpoints;
|
|
@ -1,110 +0,0 @@
|
|||||||
// import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
// import { InputText } from 'primereact/inputtext';
|
|
||||||
// import { Button } from 'primereact/button';
|
|
||||||
// import { Toast } from 'primereact/toast';
|
|
||||||
// import { Dialog } from 'primereact/dialog';
|
|
||||||
// import { Card } from 'primereact/card';
|
|
||||||
// import io, { Socket } from 'socket.io-client';
|
|
||||||
|
|
||||||
// interface ChatProps {
|
|
||||||
// serverUrl: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const Chat: React.FC<ChatProps> = ({ serverUrl }) => {
|
|
||||||
// const [messages, setMessages] = useState<string[]>([]);
|
|
||||||
// const [inputText, setInputText] = useState('');
|
|
||||||
// const [showChat, setShowChat] = useState(false);
|
|
||||||
// const socketRef = useRef<Socket | null>(null);
|
|
||||||
// const toastRef = useRef<Toast | null>(null);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// socketRef.current = io(serverUrl);
|
|
||||||
// socketRef.current.emit('join', 'global');
|
|
||||||
|
|
||||||
// socketRef.current.on('connect', () => {
|
|
||||||
// showToast('success', 'Connected to the chat server');
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketRef.current.on('disconnect', () => {
|
|
||||||
// showToast('warn', 'Disconnected from the chat server');
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socketRef.current.on('message', (message: string) => {
|
|
||||||
// setMessages((prevMessages) => [...prevMessages, message]);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// if (socketRef.current) {
|
|
||||||
// socketRef.current.disconnect();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }, [serverUrl]);
|
|
||||||
|
|
||||||
// const handleSendMessage = () => {
|
|
||||||
// if (inputText.trim() === '') {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (socketRef.current) {
|
|
||||||
// socketRef.current.emit('message', inputText);
|
|
||||||
// }
|
|
||||||
// setInputText('');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const toggleChat = () => {
|
|
||||||
// setShowChat((prevShowChat) => !prevShowChat);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const showToast = (severity: any, detail: string) => {
|
|
||||||
// if (toastRef.current) {
|
|
||||||
// toastRef.current.show({
|
|
||||||
// severity,
|
|
||||||
// summary: severity === 'success' ? 'Success' : 'Warning',
|
|
||||||
// detail,
|
|
||||||
// life: 3000,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
// if (event.key === 'Enter') {
|
|
||||||
// handleSendMessage();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <div className="chat-icon" onClick={toggleChat}>
|
|
||||||
// <h3>Click to chat</h3>
|
|
||||||
// </div>
|
|
||||||
// <Dialog
|
|
||||||
// visible={showChat}
|
|
||||||
// onHide={toggleChat}
|
|
||||||
// className="chat-dialog"
|
|
||||||
// modal
|
|
||||||
// >
|
|
||||||
// <Card title="Chat" className="chat-card">
|
|
||||||
// <div className="chat-container">
|
|
||||||
// {messages.map((message, index) => (
|
|
||||||
// <div key={index} className="message">
|
|
||||||
// <span className="message-content">{message}</span>
|
|
||||||
// </div>
|
|
||||||
// ))}
|
|
||||||
// </div>
|
|
||||||
// <div className="input-container">
|
|
||||||
// <InputText
|
|
||||||
// value={inputText}
|
|
||||||
// onChange={(e) => setInputText(e.target.value)}
|
|
||||||
// onKeyDown={handleInputKeyDown}
|
|
||||||
// placeholder="Type a message..."
|
|
||||||
// />
|
|
||||||
// <Button label="Send" onClick={handleSendMessage} />
|
|
||||||
// </div>
|
|
||||||
// </Card>
|
|
||||||
// </Dialog>
|
|
||||||
// <Toast ref={toastRef} position="bottom-left" className="bottom-0" />{' '}
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default Chat;
|
|
@ -1,85 +0,0 @@
|
|||||||
import { DataTable } from 'primereact/datatable';
|
|
||||||
import { Column } from 'primereact/column';
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { useChatStore } from '../state/stores/useChatStore';
|
|
||||||
|
|
||||||
interface ChatGPTModalProps {
|
|
||||||
content?: {
|
|
||||||
prompt: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableData {
|
|
||||||
columns: Array<{
|
|
||||||
field: string;
|
|
||||||
header: string;
|
|
||||||
}>;
|
|
||||||
data: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatGPTModal = ({ content }: ChatGPTModalProps) => {
|
|
||||||
const [tableData, setTableData] = useState<TableData>({ columns: [], data: [] });
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const { sendChatRequest } = useChatStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('useEffect triggered with content:', content);
|
|
||||||
if (content?.prompt) {
|
|
||||||
generateTable();
|
|
||||||
}
|
|
||||||
}, [content]);
|
|
||||||
|
|
||||||
const generateTable = async () => {
|
|
||||||
if (!content?.prompt) return;
|
|
||||||
console.log('Generating table for prompt:', content.prompt);
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await sendChatRequest('/chat/completions', {
|
|
||||||
data: `Create a table with data about: ${content.prompt}
|
|
||||||
Return ONLY a JSON object in this format:
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{"field": "column1", "header": "Column 1"},
|
|
||||||
{"field": "column2", "header": "Column 2"}
|
|
||||||
],
|
|
||||||
"data": [
|
|
||||||
{"column1": "value1", "column2": "value2"},
|
|
||||||
{"column1": "value3", "column2": "value4"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
1. Return ONLY the JSON object, no explanations
|
|
||||||
2. Make sure field names in data match column fields exactly
|
|
||||||
3. Choose appropriate column names based on the topic
|
|
||||||
4. Include at least 5 rows of data`,
|
|
||||||
responseFormat: 'json'
|
|
||||||
});
|
|
||||||
console.log('Got raw response:', response);
|
|
||||||
|
|
||||||
if (response?.responseText) {
|
|
||||||
console.log('Got responseText:', response.responseText);
|
|
||||||
setTableData(JSON.parse(response.responseText));
|
|
||||||
} else {
|
|
||||||
console.error('Invalid response format:', response);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating table:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-column gap-3 p-4">
|
|
||||||
<DataTable value={tableData.data} loading={loading} className="w-full">
|
|
||||||
{tableData.columns.map(col => (
|
|
||||||
<Column key={col.field} field={col.field} header={col.header} />
|
|
||||||
))}
|
|
||||||
</DataTable>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatGPTModal;
|
|
443
frontend/src/components/CanvasEndpoints.tsx → frontend/src/components/canvas-api/CanvasEndpoints.tsx
443
frontend/src/components/CanvasEndpoints.tsx → frontend/src/components/canvas-api/CanvasEndpoints.tsx
@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { Column } from 'primereact/column';
|
import { Column } from 'primereact/column';
|
||||||
import { FilterMatchMode } from 'primereact/api';
|
import { FilterMatchMode } from 'primereact/api';
|
||||||
import TableGroupHeader from '../shared/components/_V1/TableGroupHeader';
|
import TableGroupHeader from '../../shared/components/_V1/TableGroupHeader';
|
||||||
import TableGroup from '../shared/components/_V1/TableGroup';
|
import TableGroup from '../../shared/components/_V1/TableGroup';
|
||||||
import { TSizeOptionValue } from '../types/DataTable.types';
|
import { TSizeOptionValue } from './DataTable.types';
|
||||||
import { api } from '../services/api';
|
import { api } from '../../services/api';
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import { Dialog } from 'primereact/dialog';
|
import { Dialog } from 'primereact/dialog';
|
||||||
import { InputText } from 'primereact/inputtext';
|
import { InputText } from 'primereact/inputtext';
|
||||||
@ -12,13 +12,15 @@ import { Dropdown } from 'primereact/dropdown';
|
|||||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||||
import ChatGptModal from './ChatGPTModal';
|
import ChatGptModal from './ChatGPTModal';
|
||||||
import { Toast } from 'primereact/toast';
|
import { Toast } from 'primereact/toast';
|
||||||
|
import { useAuthStore } from '../../state/stores/useAuthStore';
|
||||||
|
import SettingsMenu, { SettingsMenuItem } from '../../shared/components/SettingsMenu';
|
||||||
|
|
||||||
export default function CanvasEndpoints() {
|
export default function CanvasEndpoints() {
|
||||||
const [endpoints, setEndpoints] = useState<any[]>([]);
|
const [endpoints, setEndpoints] = useState<any[]>([]);
|
||||||
const [filters, setFilters] = useState<any>(null);
|
const [filters, setFilters] = useState<any>(null);
|
||||||
const [globalFilterValue, setGlobalFilterValue] = useState('');
|
const [globalFilterValue, setGlobalFilterValue] = useState('');
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [newEndpoint, setNewEndpoint] = useState({ name: '', method: '', path: '', description: '' });
|
const [newEndpoint, setNewEndpoint] = useState({ name: '', method: '', path: '', description: '', publicPath: '' });
|
||||||
const sizeOptions: { label: string; value: TSizeOptionValue }[] = [
|
const sizeOptions: { label: string; value: TSizeOptionValue }[] = [
|
||||||
{ label: 'Small', value: 'small' },
|
{ label: 'Small', value: 'small' },
|
||||||
{ label: 'Normal', value: 'normal' },
|
{ label: 'Normal', value: 'normal' },
|
||||||
@ -42,8 +44,29 @@ export default function CanvasEndpoints() {
|
|||||||
const [chatError, setChatError] = useState<string | null>(null);
|
const [chatError, setChatError] = useState<string | null>(null);
|
||||||
const toast = useRef(null);
|
const toast = useRef(null);
|
||||||
const [editEndpointId, setEditEndpointId] = useState<number | null>(null);
|
const [editEndpointId, setEditEndpointId] = useState<number | null>(null);
|
||||||
|
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||||
|
const [apiKeyLoading, setApiKeyLoading] = useState(false);
|
||||||
|
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
|
||||||
|
const [appId, setAppId] = useState<number | null>(null);
|
||||||
|
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
||||||
|
const [editByVoiceEndpoint, setEditByVoiceEndpoint] = useState<any>(null);
|
||||||
|
const [showVoiceModal, setShowVoiceModal] = useState(false);
|
||||||
|
const [liveTranscript, setLiveTranscript] = useState('');
|
||||||
|
const [showVoiceInfoModal, setShowVoiceInfoModal] = useState(false);
|
||||||
|
const [fullTranscript, setFullTranscript] = useState('');
|
||||||
|
const [showCogMenu, setShowCogMenu] = useState(false);
|
||||||
|
const cogMenuRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
async function fetchAppId() {
|
||||||
|
try {
|
||||||
|
const response = await api('get', '/api/v1/app/by-name/Canvas');
|
||||||
|
setAppId(response.data.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch Canvas app id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchAppId();
|
||||||
initFilters();
|
initFilters();
|
||||||
fetchEndpoints();
|
fetchEndpoints();
|
||||||
}, []);
|
}, []);
|
||||||
@ -86,7 +109,7 @@ export default function CanvasEndpoints() {
|
|||||||
const sanitizedEndpoint = { ...newEndpoint, path: sanitizeCanvasPath(newEndpoint.path) };
|
const sanitizedEndpoint = { ...newEndpoint, path: sanitizeCanvasPath(newEndpoint.path) };
|
||||||
await api('post', '/api/v1/canvas-api/endpoints', sanitizedEndpoint);
|
await api('post', '/api/v1/canvas-api/endpoints', sanitizedEndpoint);
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
setNewEndpoint({ name: '', method: '', path: '', description: '' });
|
setNewEndpoint({ name: '', method: '', path: '', description: '', publicPath: '' });
|
||||||
fetchEndpoints();
|
fetchEndpoints();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create endpoint:', error);
|
console.error('Failed to create endpoint:', error);
|
||||||
@ -133,15 +156,14 @@ export default function CanvasEndpoints() {
|
|||||||
setCallModalGlobalFilterValue(value);
|
setCallModalGlobalFilterValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChatPromptSend = async () => {
|
const handleChatPromptSend = async (promptOverride?: string) => {
|
||||||
if (!chatPrompt.trim()) return;
|
const prompt = promptOverride !== undefined ? promptOverride : chatPrompt;
|
||||||
|
if (!prompt.trim()) return;
|
||||||
|
setChatMessages(prev => [...prev, { role: 'user', content: prompt }]);
|
||||||
setChatLoading(true);
|
setChatLoading(true);
|
||||||
setChatError(null);
|
setChatError(null);
|
||||||
setChatMessages(prev => [...prev, { role: 'user', content: chatPrompt }]);
|
|
||||||
try {
|
try {
|
||||||
const formattedRequest = {
|
const formattedRequest = { data: prompt };
|
||||||
data: chatPrompt,
|
|
||||||
};
|
|
||||||
const response = await api(
|
const response = await api(
|
||||||
'post',
|
'post',
|
||||||
'/api/v1/canvas-api/chatgpt/completions',
|
'/api/v1/canvas-api/chatgpt/completions',
|
||||||
@ -211,6 +233,7 @@ export default function CanvasEndpoints() {
|
|||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
path: endpoint.path,
|
path: endpoint.path,
|
||||||
description: endpoint.description || '',
|
description: endpoint.description || '',
|
||||||
|
publicPath: endpoint.publicPath || '',
|
||||||
});
|
});
|
||||||
setShowCreateModal(true);
|
setShowCreateModal(true);
|
||||||
};
|
};
|
||||||
@ -224,7 +247,7 @@ export default function CanvasEndpoints() {
|
|||||||
await api('post', '/api/v1/canvas-api/endpoints', sanitizedEndpoint);
|
await api('post', '/api/v1/canvas-api/endpoints', sanitizedEndpoint);
|
||||||
}
|
}
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
setNewEndpoint({ name: '', method: '', path: '', description: '' });
|
setNewEndpoint({ name: '', method: '', path: '', description: '', publicPath: '' });
|
||||||
setEditEndpointId(null);
|
setEditEndpointId(null);
|
||||||
fetchEndpoints();
|
fetchEndpoints();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -234,10 +257,280 @@ export default function CanvasEndpoints() {
|
|||||||
|
|
||||||
const handleModalHide = () => {
|
const handleModalHide = () => {
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
setNewEndpoint({ name: '', method: '', path: '', description: '' });
|
setNewEndpoint({ name: '', method: '', path: '', description: '', publicPath: '' });
|
||||||
setEditEndpointId(null);
|
setEditEndpointId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApiKey = async () => {
|
||||||
|
console.log('handleApiKey called');
|
||||||
|
setApiKeyLoading(true);
|
||||||
|
setApiKeyError(null);
|
||||||
|
try {
|
||||||
|
const user = useAuthStore.getState().user;
|
||||||
|
console.log('User:', user, 'AppId:', appId);
|
||||||
|
if (!user) throw new Error('User not logged in');
|
||||||
|
if (!appId) throw new Error('App ID not loaded');
|
||||||
|
const response = await api('post', '/api/v1/app/apikey/generate', { userId: user.id, appId });
|
||||||
|
setApiKey(response.data.apiKey);
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.error || error.message || 'Failed to fetch or generate API key';
|
||||||
|
setApiKeyError(errorMessage);
|
||||||
|
console.error('API Key Error:', error);
|
||||||
|
} finally {
|
||||||
|
setApiKeyLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchApiKey = async () => {
|
||||||
|
try {
|
||||||
|
const user = useAuthStore.getState().user;
|
||||||
|
if (!user || !appId) return;
|
||||||
|
const response = await api('post', '/api/v1/app/apikey/get', { userId: user.id, appId });
|
||||||
|
setApiKey(response.data.apiKey || null);
|
||||||
|
setApiKeyError(null);
|
||||||
|
} catch (error: any) {
|
||||||
|
setApiKey(null);
|
||||||
|
setApiKeyError(error.response?.data?.error || error.message || 'Failed to fetch API key');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showApiKeyModal) {
|
||||||
|
fetchApiKey();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [showApiKeyModal, appId]);
|
||||||
|
|
||||||
|
const tryDeleteFromSpeech = async (transcript) => {
|
||||||
|
const lower = transcript.toLowerCase();
|
||||||
|
if (lower.startsWith('create')) return false; // let normal create/chat logic handle it
|
||||||
|
|
||||||
|
if (lower.includes('delete')) {
|
||||||
|
// Try to match "delete endpoint 5" or "remove endpoint 5"
|
||||||
|
const idMatch = transcript.match(/delete endpoint (\d+)/i) || transcript.match(/remove endpoint (\d+)/i);
|
||||||
|
if (idMatch) {
|
||||||
|
const id = parseInt(idMatch[1], 10);
|
||||||
|
await handleDeleteEndpoint({ id });
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint with ID ${id} deleted (if it existed).` }]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Try to match "delete endpoint called X" or "remove endpoint named X"
|
||||||
|
const nameMatch = transcript.match(/delete endpoint (called|named) (.+)/i) || transcript.match(/remove endpoint (called|named) (.+)/i);
|
||||||
|
if (nameMatch) {
|
||||||
|
const name = nameMatch[2].trim();
|
||||||
|
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
|
||||||
|
if (endpoint) {
|
||||||
|
await handleDeleteEndpoint(endpoint);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint \"${name}\" deleted.` }]);
|
||||||
|
} else {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Try to match "delete endpoint [name]"
|
||||||
|
const nameAfterEndpointMatch = transcript.match(/delete endpoint ([a-zA-Z0-9 _-]+)/i);
|
||||||
|
if (nameAfterEndpointMatch) {
|
||||||
|
const name = nameAfterEndpointMatch[1].trim();
|
||||||
|
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
|
||||||
|
if (endpoint) {
|
||||||
|
await handleDeleteEndpoint(endpoint);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint \"${name}\" deleted.` }]);
|
||||||
|
} else {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If just "delete endpoint" with no id or name, show a message
|
||||||
|
if (lower.includes('delete endpoint')) {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Please specify the endpoint ID or name to delete.` }]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryShowFromSpeech = async (transcript) => {
|
||||||
|
const lower = transcript.toLowerCase();
|
||||||
|
const viewKeywords = ['show', 'view', 'open'];
|
||||||
|
// Find which keyword (if any) is present
|
||||||
|
const action = viewKeywords.find(kw => lower.includes(kw));
|
||||||
|
if (!action) return false;
|
||||||
|
|
||||||
|
// Try "<action> endpoint 5"
|
||||||
|
const idMatch = lower.match(/(?:show|view|open) endpoint (\d+)/i);
|
||||||
|
if (idMatch) {
|
||||||
|
const id = parseInt(idMatch[1], 10);
|
||||||
|
const endpoint = endpoints.find(e => e.id === id);
|
||||||
|
if (endpoint) {
|
||||||
|
handleCallEndpoint(endpoint);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint with ID ${id}.` }]);
|
||||||
|
} else {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with ID ${id}.` }]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try "<action> endpoint [name]"
|
||||||
|
const nameAfterEndpointMatch = lower.match(/(?:show|view|open) endpoint ([a-zA-Z0-9 _-]+)/i);
|
||||||
|
if (nameAfterEndpointMatch) {
|
||||||
|
const name = nameAfterEndpointMatch[1].trim();
|
||||||
|
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
|
||||||
|
if (endpoint) {
|
||||||
|
handleCallEndpoint(endpoint);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint \"${name}\".` }]);
|
||||||
|
} else {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try "<action> [name] endpoint"
|
||||||
|
const nameEndpointMatch = lower.match(/(?:show|view|open) (.+) endpoint/i);
|
||||||
|
if (nameEndpointMatch) {
|
||||||
|
const name = nameEndpointMatch[1].trim();
|
||||||
|
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
|
||||||
|
if (endpoint) {
|
||||||
|
handleCallEndpoint(endpoint);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint \"${name}\".` }]);
|
||||||
|
} else {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try "<action> [name]"
|
||||||
|
const nameMatch = lower.match(/(?:show|view|open) (.+)/i);
|
||||||
|
if (nameMatch) {
|
||||||
|
const name = nameMatch[1].trim();
|
||||||
|
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
|
||||||
|
if (endpoint) {
|
||||||
|
handleCallEndpoint(endpoint);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint \"${name}\".` }]);
|
||||||
|
} else {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const trySearchFromSpeech = (transcript) => {
|
||||||
|
const lower = transcript.toLowerCase().trim();
|
||||||
|
// Match "search [term]", "find [term]", or "filter by [term]"
|
||||||
|
const searchMatch = lower.match(/^(search|find|filter by)\s+(.+)$/i);
|
||||||
|
if (searchMatch) {
|
||||||
|
const term = searchMatch[2].trim();
|
||||||
|
setGlobalFilterValue(term);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Filtered endpoints by: "${term}"` }]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryModifyFromSpeech = (transcript) => {
|
||||||
|
const lower = transcript.toLowerCase();
|
||||||
|
const match = lower.match(/modify endpoint (\d+)/i);
|
||||||
|
if (match) {
|
||||||
|
const id = parseInt(match[1], 10);
|
||||||
|
const endpoint = endpoints.find(e => e.id === id);
|
||||||
|
if (endpoint) {
|
||||||
|
setEditByVoiceEndpoint({ ...endpoint });
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Ready to modify endpoint with ID ${id}. Say 'method is GET' or 'path is slash course slash id'.` }]);
|
||||||
|
} else {
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with ID ${id}.` }]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryUpdateEditByVoice = async (transcript) => {
|
||||||
|
if (!editByVoiceEndpoint) return false;
|
||||||
|
let updated = { ...editByVoiceEndpoint };
|
||||||
|
let didUpdate = false;
|
||||||
|
|
||||||
|
// Method update
|
||||||
|
const methodMatch = transcript.match(/method is (get|post|put|delete|patch)/i);
|
||||||
|
if (methodMatch) {
|
||||||
|
updated.method = methodMatch[1].toUpperCase();
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path update
|
||||||
|
const pathMatch = transcript.match(/path is (.+)/i);
|
||||||
|
if (pathMatch) {
|
||||||
|
let path = pathMatch[1]
|
||||||
|
.replace(/slash/gi, '/')
|
||||||
|
.replace(/colon id|id/gi, ':id')
|
||||||
|
.replace(/\s+/g, '');
|
||||||
|
updated.path = path;
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
setEditByVoiceEndpoint(updated);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Updated endpoint: method=${updated.method}, path=${updated.path}` }]);
|
||||||
|
// Auto-save
|
||||||
|
await api('put', `/api/v1/canvas-api/endpoints/${updated.id}`, updated);
|
||||||
|
fetchEndpoints();
|
||||||
|
setEditByVoiceEndpoint(null);
|
||||||
|
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint ${updated.id} saved.` }]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVoiceInput = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
if (!SpeechRecognition) {
|
||||||
|
alert('Speech recognition not supported in this browser.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowVoiceModal(true);
|
||||||
|
setLiveTranscript('');
|
||||||
|
setFullTranscript('');
|
||||||
|
const recognition = new SpeechRecognition();
|
||||||
|
recognition.lang = 'en-US';
|
||||||
|
recognition.interimResults = true;
|
||||||
|
recognition.maxAlternatives = 1;
|
||||||
|
|
||||||
|
recognition.onresult = async (event: any) => {
|
||||||
|
const transcript = event.results[0][0].transcript;
|
||||||
|
setLiveTranscript(transcript);
|
||||||
|
setFullTranscript(prev => prev + (prev && !prev.endsWith(' ') ? ' ' : '') + transcript);
|
||||||
|
setChatPrompt(transcript);
|
||||||
|
if (event.results[0].isFinal) {
|
||||||
|
setShowVoiceModal(false);
|
||||||
|
setLiveTranscript('');
|
||||||
|
setFullTranscript('');
|
||||||
|
if (transcript.toLowerCase().includes('open command list')) {
|
||||||
|
setShowVoiceInfoModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (await tryDeleteFromSpeech(transcript)) return;
|
||||||
|
if (await tryShowFromSpeech(transcript)) return;
|
||||||
|
if (trySearchFromSpeech(transcript)) return;
|
||||||
|
if (tryModifyFromSpeech(transcript)) return;
|
||||||
|
if (await tryUpdateEditByVoice(transcript)) return;
|
||||||
|
handleChatPromptSend(transcript);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recognition.onerror = (event: any) => {
|
||||||
|
setShowVoiceModal(false);
|
||||||
|
setLiveTranscript('');
|
||||||
|
setFullTranscript('');
|
||||||
|
alert('Speech recognition error: ' + event.error);
|
||||||
|
};
|
||||||
|
recognition.onend = () => {
|
||||||
|
setShowVoiceModal(false);
|
||||||
|
setLiveTranscript('');
|
||||||
|
setFullTranscript('');
|
||||||
|
};
|
||||||
|
recognition.start();
|
||||||
|
};
|
||||||
|
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<TableGroupHeader
|
<TableGroupHeader
|
||||||
size={size}
|
size={size}
|
||||||
@ -250,16 +543,36 @@ export default function CanvasEndpoints() {
|
|||||||
extraButtonsTemplate={() => (
|
extraButtonsTemplate={() => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
label="Create Endpoint"
|
|
||||||
icon="pi pi-plus"
|
icon="pi pi-plus"
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="p-button-secondary"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Ask ChatGPT"
|
|
||||||
icon="pi pi-comments"
|
icon="pi pi-comments"
|
||||||
onClick={() => setShowChatGptModal(true)}
|
onClick={() => setShowChatGptModal(true)}
|
||||||
className="p-button-primary"
|
className="p-button-primary"
|
||||||
/>
|
/>
|
||||||
|
<SettingsMenu
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: 'Manage API Key',
|
||||||
|
icon: 'pi pi-key',
|
||||||
|
command: () => setShowApiKeyModal(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Voice Command List',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
command: () => setShowVoiceInfoModal(true),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
buttonClassName="p-button-secondary"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-volume-up"
|
||||||
|
onClick={handleVoiceInput}
|
||||||
|
className="p-button-secondary"
|
||||||
|
disabled={chatLoading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -267,9 +580,9 @@ export default function CanvasEndpoints() {
|
|||||||
|
|
||||||
const renderCallButton = (rowData: any) => (
|
const renderCallButton = (rowData: any) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button label="Call" icon="pi pi-play" onClick={() => handleCallEndpoint(rowData)} />
|
<Button icon="pi pi-play" onClick={() => handleCallEndpoint(rowData)} />
|
||||||
<Button label="Edit" icon="pi pi-pencil" className="p-button-warning" onClick={() => openEditModal(rowData)} />
|
<Button icon="pi pi-pencil" className="p-button-warning" onClick={() => openEditModal(rowData)} />
|
||||||
<Button label="Delete" icon="pi pi-trash" className="p-button-danger" onClick={() => handleDeleteEndpoint(rowData)} />
|
<Button icon="pi pi-trash" className="p-button-danger" onClick={() => handleDeleteEndpoint(rowData)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -285,12 +598,22 @@ export default function CanvasEndpoints() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Compute filtered endpoints for search-everything
|
||||||
|
const filteredEndpoints = globalFilterValue
|
||||||
|
? endpoints.filter(endpoint =>
|
||||||
|
Object.values(endpoint)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(globalFilterValue.toLowerCase())
|
||||||
|
)
|
||||||
|
: endpoints;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full p-6'>
|
<div className='w-full h-full p-6'>
|
||||||
<h2 className='text-xl font-semibold mb-4'>Canvas Endpoints</h2>
|
<h2 className='text-xl font-semibold mb-4'>Canvas Endpoints</h2>
|
||||||
<Toast ref={toast} position="bottom-right" />
|
<Toast ref={toast} position="bottom-right" />
|
||||||
<TableGroup
|
<TableGroup
|
||||||
body={endpoints}
|
body={filteredEndpoints}
|
||||||
size={size}
|
size={size}
|
||||||
header={renderHeader}
|
header={renderHeader}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
@ -304,14 +627,15 @@ export default function CanvasEndpoints() {
|
|||||||
<Column field='id' header='ID' sortable filter style={{ width: '10%' }} />
|
<Column field='id' header='ID' sortable filter style={{ width: '10%' }} />
|
||||||
<Column field='name' header='Name' sortable filter style={{ width: '20%' }} />
|
<Column field='name' header='Name' sortable filter style={{ width: '20%' }} />
|
||||||
<Column field='method' header='Method' sortable filter style={{ width: '10%' }} />
|
<Column field='method' header='Method' sortable filter style={{ width: '10%' }} />
|
||||||
<Column field='path' header='Path' sortable filter style={{ width: '40%' }} />
|
<Column field='path' header='Path' sortable filter style={{ width: '25%' }} />
|
||||||
|
<Column field='publicPath' header='Public Path' sortable filter style={{ width: '15%' }} />
|
||||||
<Column header='Action' body={renderCallButton} style={{ width: '20%' }} />
|
<Column header='Action' body={renderCallButton} style={{ width: '20%' }} />
|
||||||
</TableGroup>
|
</TableGroup>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
visible={showCreateModal}
|
visible={showCreateModal}
|
||||||
onHide={handleModalHide}
|
onHide={handleModalHide}
|
||||||
header={editEndpointId ? "Edit Endpoint" : "Create New Endpoint"}
|
header={editEndpointId ? "Edit" : "Create"}
|
||||||
footer={
|
footer={
|
||||||
<div>
|
<div>
|
||||||
<Button label="Cancel" icon="pi pi-times" onClick={handleModalHide} className="p-button-text" />
|
<Button label="Cancel" icon="pi pi-times" onClick={handleModalHide} className="p-button-text" />
|
||||||
@ -343,6 +667,10 @@ export default function CanvasEndpoints() {
|
|||||||
<label htmlFor="path">Path</label>
|
<label htmlFor="path">Path</label>
|
||||||
<InputText id="path" value={newEndpoint.path} onChange={(e) => setNewEndpoint({ ...newEndpoint, path: e.target.value })} />
|
<InputText id="path" value={newEndpoint.path} onChange={(e) => setNewEndpoint({ ...newEndpoint, path: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-field">
|
||||||
|
<label htmlFor="publicPath">Public Path</label>
|
||||||
|
<InputText id="publicPath" value={newEndpoint.publicPath || ''} onChange={(e) => setNewEndpoint({ ...newEndpoint, publicPath: e.target.value })} />
|
||||||
|
</div>
|
||||||
<div className="p-field">
|
<div className="p-field">
|
||||||
<label htmlFor="description">Description</label>
|
<label htmlFor="description">Description</label>
|
||||||
<InputText id="description" value={newEndpoint.description} onChange={(e) => setNewEndpoint({ ...newEndpoint, description: e.target.value })} />
|
<InputText id="description" value={newEndpoint.description} onChange={(e) => setNewEndpoint({ ...newEndpoint, description: e.target.value })} />
|
||||||
@ -450,7 +778,7 @@ export default function CanvasEndpoints() {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon="pi pi-send"
|
icon="pi pi-send"
|
||||||
onClick={handleChatPromptSend}
|
onClick={() => handleChatPromptSend()}
|
||||||
loading={chatLoading}
|
loading={chatLoading}
|
||||||
disabled={!chatPrompt.trim() || chatLoading}
|
disabled={!chatPrompt.trim() || chatLoading}
|
||||||
/>
|
/>
|
||||||
@ -458,6 +786,75 @@ export default function CanvasEndpoints() {
|
|||||||
{chatError && <div className="text-red-500 mt-2">{chatError}</div>}
|
{chatError && <div className="text-red-500 mt-2">{chatError}</div>}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
visible={showApiKeyModal}
|
||||||
|
onHide={() => setShowApiKeyModal(false)}
|
||||||
|
header="API Key Management"
|
||||||
|
footer={
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
label={apiKey ? "Regenerate API Key" : "Create API Key"}
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
onClick={handleApiKey}
|
||||||
|
loading={apiKeyLoading}
|
||||||
|
className="p-button-danger"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Close"
|
||||||
|
icon="pi pi-times"
|
||||||
|
onClick={() => setShowApiKeyModal(false)}
|
||||||
|
className="p-button-text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{apiKey && (
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 font-bold">Current API Key:</div>
|
||||||
|
<div className="font-mono bg-gray-100 p-2 rounded">{apiKey}</div>
|
||||||
|
<div className="mt-2 text-sm text-red-500">
|
||||||
|
Regenerating will invalidate the old key.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!apiKey && (
|
||||||
|
<div>
|
||||||
|
<div>No API key exists for this app. Click "Create API Key" to generate one.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{apiKeyError && <div className="text-red-500 mt-2">{apiKeyError}</div>}
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
visible={showVoiceModal}
|
||||||
|
onHide={() => setShowVoiceModal(false)}
|
||||||
|
header="Listening..."
|
||||||
|
modal
|
||||||
|
style={{ minWidth: 400, textAlign: 'center' }}
|
||||||
|
closable={false}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 24, minHeight: 60 }}>{fullTranscript || <span className="text-gray-400">Say something…</span>}</div>
|
||||||
|
<div className="mt-4 text-sm text-gray-500">Speak your command or query.</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
visible={showVoiceInfoModal}
|
||||||
|
onHide={() => setShowVoiceInfoModal(false)}
|
||||||
|
header="Voice Command List"
|
||||||
|
modal
|
||||||
|
style={{ minWidth: 400 }}
|
||||||
|
>
|
||||||
|
<ul style={{ textAlign: 'left', lineHeight: 2 }}>
|
||||||
|
<li><b>show/view/open endpoint [name or id]</b> — Open an endpoint's table view</li>
|
||||||
|
<li><b>delete endpoint [name or id]</b> — Delete an endpoint</li>
|
||||||
|
<li><b>search [term]</b> — Filter endpoints</li>
|
||||||
|
<li><b>modify endpoint [id]</b> — Start editing an endpoint by voice</li>
|
||||||
|
<li><b>method is [GET/POST/PUT/DELETE/PATCH]</b> — Set method (in edit mode)</li>
|
||||||
|
<li><b>path is [your path]</b> — Set path (in edit mode, say 'slash' for /, 'colon id' for :id)</li>
|
||||||
|
<li><b>open command list</b> — Show this help</li>
|
||||||
|
</ul>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
314
frontend/src/components/canvas-api/ChatGPTModal.tsx
Normal file
314
frontend/src/components/canvas-api/ChatGPTModal.tsx
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
import { DataTable } from 'primereact/datatable';
|
||||||
|
import { Column } from 'primereact/column';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useChatStore } from '../../state/stores/useChatStore';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { generateTablePrompt } from '../../prompts/table-generation';
|
||||||
|
import { Toast } from 'primereact/toast';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
|
||||||
|
interface ChatGPTModalProps {
|
||||||
|
content?: {
|
||||||
|
prompt: string;
|
||||||
|
id?: string;
|
||||||
|
keywords?: string[];
|
||||||
|
context?: string;
|
||||||
|
tableSchema?: {
|
||||||
|
name: string;
|
||||||
|
fields: Array<{
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableData {
|
||||||
|
columns: Array<{
|
||||||
|
field: string;
|
||||||
|
header: string;
|
||||||
|
}>;
|
||||||
|
data: any[];
|
||||||
|
metadata?: {
|
||||||
|
id?: string;
|
||||||
|
keywords?: string[];
|
||||||
|
context?: string;
|
||||||
|
timestamp: string;
|
||||||
|
actions?: Array<{
|
||||||
|
action: string;
|
||||||
|
target: string;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatGPTModal = ({ content }: ChatGPTModalProps) => {
|
||||||
|
const [tableData, setTableData] = useState<TableData>({
|
||||||
|
columns: [],
|
||||||
|
data: [],
|
||||||
|
metadata: {
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedRow, setSelectedRow] = useState<any>(null);
|
||||||
|
const [showRowDialog, setShowRowDialog] = useState(false);
|
||||||
|
const [finalPrompt, setFinalPrompt] = useState<string>('');
|
||||||
|
const { sendChatRequest } = useChatStore();
|
||||||
|
const toast = useRef<Toast>(null);
|
||||||
|
|
||||||
|
const constructPath = (id: string | number, pathname: string): string => {
|
||||||
|
// Remove any leading/trailing slashes from pathname
|
||||||
|
const cleanPathname = pathname.replace(/^\/+|\/+$/g, '');
|
||||||
|
// Construct the path with proper slashes
|
||||||
|
return `/${id}/${cleanPathname}`.replace(/\/+/g, '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findRowById = (id: string | number) => {
|
||||||
|
const row = tableData.data.find(row => row.id?.toString() === id.toString());
|
||||||
|
if (row) {
|
||||||
|
setSelectedRow(row);
|
||||||
|
setShowRowDialog(true);
|
||||||
|
showToast('info', `Viewing details for ID: ${id}`);
|
||||||
|
} else {
|
||||||
|
showToast('warn', `No row found with ID: ${id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublicPath = (prompt: string) => {
|
||||||
|
// Check for path construction pattern
|
||||||
|
const pathMatch = prompt.match(/add\s+public\s+path\s+to\s+(\d+)\s+slash\s+([^\s]+)/i);
|
||||||
|
if (pathMatch) {
|
||||||
|
const [, id, pathname] = pathMatch;
|
||||||
|
const constructedPath = constructPath(id, pathname);
|
||||||
|
// Find the row with the given ID
|
||||||
|
const rowIndex = tableData.data.findIndex(row => row.id?.toString() === id.toString());
|
||||||
|
if (rowIndex !== -1) {
|
||||||
|
// Update the path of the existing row
|
||||||
|
setTableData(prev => {
|
||||||
|
const newData = [...prev.data];
|
||||||
|
newData[rowIndex] = {
|
||||||
|
...newData[rowIndex],
|
||||||
|
path: constructedPath
|
||||||
|
};
|
||||||
|
// Add an 'add_path' action to metadata
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
data: newData,
|
||||||
|
metadata: {
|
||||||
|
...prev.metadata,
|
||||||
|
actions: [
|
||||||
|
...(prev.metadata?.actions || []),
|
||||||
|
{
|
||||||
|
action: 'add_path',
|
||||||
|
target: constructedPath,
|
||||||
|
description: `added public path ${constructedPath} to endpoint ID ${id}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
showToast('success', `Updated path for ID ${id}: ${constructedPath}`);
|
||||||
|
findRowById(id);
|
||||||
|
} else {
|
||||||
|
showToast('warn', `No endpoint found with ID: ${id}`);
|
||||||
|
}
|
||||||
|
return { id, constructedPath };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (content?.prompt) {
|
||||||
|
setFinalPrompt(content.prompt);
|
||||||
|
// Handle public path update only, not endpoint creation
|
||||||
|
handlePublicPath(content.prompt);
|
||||||
|
generateTable();
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const showToast = (severity: 'success' | 'info' | 'warn' | 'error', message: string) => {
|
||||||
|
toast.current?.show({
|
||||||
|
severity,
|
||||||
|
summary: severity.charAt(0).toUpperCase() + severity.slice(1),
|
||||||
|
detail: message,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateTable = async () => {
|
||||||
|
if (!content?.prompt) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const promptText = generateTablePrompt({
|
||||||
|
prompt: content.prompt,
|
||||||
|
context: content.context,
|
||||||
|
keywords: content.keywords,
|
||||||
|
id: content.id,
|
||||||
|
tableSchema: content.tableSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await sendChatRequest('/chat/completions', {
|
||||||
|
data: promptText,
|
||||||
|
responseFormat: 'json'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.responseText) {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(response.responseText);
|
||||||
|
setTableData({
|
||||||
|
...parsedData,
|
||||||
|
metadata: {
|
||||||
|
...parsedData.metadata,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success message based on the primary action
|
||||||
|
const primaryAction = parsedData.metadata?.actions?.[0];
|
||||||
|
if (primaryAction) {
|
||||||
|
const actionMessage = {
|
||||||
|
create: 'Successfully created',
|
||||||
|
delete: 'Successfully deleted',
|
||||||
|
update: 'Successfully updated',
|
||||||
|
show: 'Successfully retrieved',
|
||||||
|
search: 'Search completed',
|
||||||
|
view: 'Viewing details'
|
||||||
|
}[primaryAction.action] || 'Operation completed';
|
||||||
|
|
||||||
|
showToast('success', `${actionMessage} ${primaryAction.target}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing response:', error);
|
||||||
|
showToast('error', 'Error processing the response');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast('error', 'Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating table:', error);
|
||||||
|
showToast('error', 'Error generating table');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (event: any) => {
|
||||||
|
const rowData = event.data;
|
||||||
|
setSelectedRow(rowData);
|
||||||
|
setShowRowDialog(true);
|
||||||
|
|
||||||
|
// Add view action to metadata if not present
|
||||||
|
if (tableData.metadata?.actions) {
|
||||||
|
const hasViewAction = tableData.metadata.actions.some(
|
||||||
|
action => action.action === 'view' && action.target === rowData.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasViewAction) {
|
||||||
|
setTableData(prev => ({
|
||||||
|
...prev,
|
||||||
|
metadata: {
|
||||||
|
...prev.metadata,
|
||||||
|
actions: [
|
||||||
|
...(prev.metadata?.actions || []),
|
||||||
|
{
|
||||||
|
action: 'view',
|
||||||
|
target: rowData.id || 'selected row',
|
||||||
|
description: `view details for ${rowData.id || 'selected row'}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
showToast('info', `Viewing details for ${rowData.id || 'selected row'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRowDialog = () => {
|
||||||
|
if (!selectedRow) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
visible={showRowDialog}
|
||||||
|
onHide={() => setShowRowDialog(false)}
|
||||||
|
header={`View Details - ${selectedRow.id || 'Selected Row'}`}
|
||||||
|
style={{ width: '50vw' }}
|
||||||
|
>
|
||||||
|
<div className="grid">
|
||||||
|
{tableData.columns.map(col => (
|
||||||
|
<div key={col.field} className="col-12 md:col-6 p-2">
|
||||||
|
<div className="font-bold">{col.header}</div>
|
||||||
|
<div>{selectedRow[col.field] || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toast ref={toast} position="top-right" />
|
||||||
|
<div className="flex flex-column gap-3 p-4 relative">
|
||||||
|
{finalPrompt && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Your Request
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
value={finalPrompt}
|
||||||
|
readOnly
|
||||||
|
className="w-full p-2 border rounded"
|
||||||
|
style={{ backgroundColor: '#f8f9fa' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DataTable
|
||||||
|
value={tableData.data}
|
||||||
|
loading={loading}
|
||||||
|
className="w-full"
|
||||||
|
onRowClick={handleRowClick}
|
||||||
|
selectionMode="single"
|
||||||
|
rowHover
|
||||||
|
>
|
||||||
|
{tableData.columns.map(col => (
|
||||||
|
<Column
|
||||||
|
key={col.field}
|
||||||
|
field={col.field}
|
||||||
|
header={col.header}
|
||||||
|
body={(rowData) => (
|
||||||
|
<div className="cursor-pointer hover:bg-gray-100 p-2">
|
||||||
|
{rowData[col.field] || 'N/A'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DataTable>
|
||||||
|
{tableData.metadata && (
|
||||||
|
<div className="text-sm text-gray-500 mt-2">
|
||||||
|
<p>ID: {tableData.metadata.id}</p>
|
||||||
|
<p>Keywords: {tableData.metadata.keywords?.join(', ')}</p>
|
||||||
|
<p>Context: {tableData.metadata.context}</p>
|
||||||
|
<p>Generated: {new Date(tableData.metadata.timestamp).toLocaleString()}</p>
|
||||||
|
{tableData.metadata.actions && tableData.metadata.actions.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="font-bold">Detected Actions:</p>
|
||||||
|
{tableData.metadata.actions.map((action, index) => (
|
||||||
|
<div key={index} className="ml-2">
|
||||||
|
<span className="font-semibold">{action.action}:</span> {action.target}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{renderRowDialog()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatGPTModal;
|
65
frontend/src/components/canvas-api/ParseDocsModal.tsx
Normal file
65
frontend/src/components/canvas-api/ParseDocsModal.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
|
||||||
|
interface ParseDocsModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onHide: () => void;
|
||||||
|
onParsed?: (endpoints: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParseDocsModal({ visible, onHide, onParsed }: ParseDocsModalProps) {
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [results, setResults] = useState<any>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleParse = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResults(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/parse-docs', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ url }),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch or parse docs');
|
||||||
|
const data = await response.json();
|
||||||
|
setResults(data);
|
||||||
|
if (onParsed && data.restructured) onParsed(data.restructured);
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || 'Unknown error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog visible={visible} onHide={onHide} header="Parse API Docs" style={{ width: '50vw' }}>
|
||||||
|
<div>
|
||||||
|
<InputText
|
||||||
|
value={url}
|
||||||
|
onChange={e => setUrl(e.target.value)}
|
||||||
|
placeholder="Enter documentation URL"
|
||||||
|
className="w-full mb-3"
|
||||||
|
/>
|
||||||
|
<Button label="Parse" onClick={handleParse} loading={loading} disabled={!url} />
|
||||||
|
{error && <div className="text-red-500 mt-2">{error}</div>}
|
||||||
|
</div>
|
||||||
|
{results && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h4>Raw HTML (first 500 chars):</h4>
|
||||||
|
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{results.rawHtml?.slice(0, 500)}...</pre>
|
||||||
|
<h4>AI Output:</h4>
|
||||||
|
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(results.aiOutput, null, 2)}</pre>
|
||||||
|
<h4>Restructured Output:</h4>
|
||||||
|
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(results.restructured, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ParseDocsModal;
|
0
frontend/src/state/stores/useCanvasStore.ts → frontend/src/components/canvas-api/useCanvasStore.ts
0
frontend/src/state/stores/useCanvasStore.ts → frontend/src/components/canvas-api/useCanvasStore.ts
75
frontend/src/constants/actions.ts
Normal file
75
frontend/src/constants/actions.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
export interface ActionKeyword {
|
||||||
|
action: string;
|
||||||
|
target: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ACTION_KEYWORDS: Record<string, string[]> = {
|
||||||
|
create: ['create', 'new', 'make'],
|
||||||
|
delete: ['delete', 'remove', 'drop'],
|
||||||
|
update: ['update', 'modify', 'change', 'edit'],
|
||||||
|
show: ['show', 'display', 'list', 'get', 'fetch', 'view'],
|
||||||
|
search: ['search', 'find', 'query', 'filter'],
|
||||||
|
view: ['view', 'details', 'info', 'expand']
|
||||||
|
};
|
||||||
|
|
||||||
|
export const detectActions = (prompt: string, keywords: string[]): ActionKeyword[] => {
|
||||||
|
const actions: ActionKeyword[] = [];
|
||||||
|
const promptLower = prompt.toLowerCase().trim();
|
||||||
|
|
||||||
|
// If prompt starts with 'add', treat as 'add' action
|
||||||
|
if (promptLower.startsWith('add')) {
|
||||||
|
// Try to match 'add ... to [id] slash [path]'
|
||||||
|
const match = promptLower.match(/add.*to\s+(\d+)\s+slash\s+([^\s]+)/i);
|
||||||
|
if (match) {
|
||||||
|
const [, id, pathname] = match;
|
||||||
|
actions.push({
|
||||||
|
action: 'add',
|
||||||
|
target: `id:${id} path:/${pathname.replace(/^\/+|\/+$/g, '')}`,
|
||||||
|
description: `add /${pathname.replace(/^\/+|\/+$/g, '')} to endpoint ID ${id}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: just mark as a generic add action
|
||||||
|
actions.push({
|
||||||
|
action: 'add',
|
||||||
|
target: '',
|
||||||
|
description: 'add action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check for explicit action-target pairs
|
||||||
|
const words = promptLower.split(' ');
|
||||||
|
for (let i = 0; i < words.length - 1; i++) {
|
||||||
|
const word = words[i];
|
||||||
|
const nextWord = words[i + 1];
|
||||||
|
for (const [action, triggers] of Object.entries(ACTION_KEYWORDS)) {
|
||||||
|
if (triggers.includes(word)) {
|
||||||
|
actions.push({
|
||||||
|
action,
|
||||||
|
target: nextWord,
|
||||||
|
description: `${action} ${nextWord}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check keywords for additional context
|
||||||
|
keywords.forEach(keyword => {
|
||||||
|
const keywordWords = keyword.toLowerCase().split(' ');
|
||||||
|
for (const [action, triggers] of Object.entries(ACTION_KEYWORDS)) {
|
||||||
|
if (triggers.some(trigger => keywordWords.includes(trigger))) {
|
||||||
|
const target = keywordWords.find(word => !triggers.includes(word));
|
||||||
|
if (target) {
|
||||||
|
actions.push({
|
||||||
|
action,
|
||||||
|
target,
|
||||||
|
description: `${action} ${target}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
@ -6,7 +6,7 @@ import TenantProfile from '../components/TenantProfile';
|
|||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import LoginModal from '../components/LoginModal';
|
import LoginModal from '../components/LoginModal';
|
||||||
|
|
||||||
const DualModalComponent = () => {
|
const DualModalComponent = () => {
|
||||||
const { systemId } = useParams();
|
const { systemId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isLoggedIn } = useAuthStore();
|
const { isLoggedIn } = useAuthStore();
|
||||||
|
75
frontend/src/prompts/table-generation.ts
Normal file
75
frontend/src/prompts/table-generation.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { ActionKeyword, detectActions } from '../constants/actions';
|
||||||
|
|
||||||
|
interface TableGenerationPromptParams {
|
||||||
|
prompt: string;
|
||||||
|
context?: string;
|
||||||
|
keywords?: string[];
|
||||||
|
id?: string;
|
||||||
|
tableSchema?: {
|
||||||
|
name: string;
|
||||||
|
fields: Array<{
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateTablePrompt = (params: TableGenerationPromptParams): string => {
|
||||||
|
const {
|
||||||
|
prompt,
|
||||||
|
context = 'General information',
|
||||||
|
keywords = [],
|
||||||
|
id = 'auto-generated',
|
||||||
|
tableSchema
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const detectedActions = detectActions(prompt, keywords);
|
||||||
|
|
||||||
|
return `Create a table with data about: ${prompt}
|
||||||
|
Context: ${context}
|
||||||
|
Keywords: ${keywords.join(', ') || 'None specified'}
|
||||||
|
${tableSchema ? `
|
||||||
|
Table Schema:
|
||||||
|
Name: ${tableSchema.name}
|
||||||
|
Fields:
|
||||||
|
${tableSchema.fields.map(f => `- ${f.name} (${f.type})${f.description ? `: ${f.description}` : ''}`).join('\n')}
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
Detected Actions:
|
||||||
|
${detectedActions.map(action => `- ${action.description}`).join('\n')}
|
||||||
|
|
||||||
|
Return ONLY a JSON object in this format:
|
||||||
|
{
|
||||||
|
"columns": [
|
||||||
|
{"field": "column1", "header": "Column 1"},
|
||||||
|
{"field": "column2", "header": "Column 2"}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"column1": "value1", "column2": "value2"},
|
||||||
|
{"column1": "value3", "column2": "value4"}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"id": "${id}",
|
||||||
|
"keywords": ${JSON.stringify(keywords)},
|
||||||
|
"context": "${context}",
|
||||||
|
"timestamp": "${new Date().toISOString()}",
|
||||||
|
"actions": ${JSON.stringify(detectedActions)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Return ONLY the JSON object, no explanations
|
||||||
|
2. Make sure field names in data match column fields exactly
|
||||||
|
3. Choose appropriate column names based on the topic
|
||||||
|
4. Include at least 5 rows of data
|
||||||
|
5. Ensure all data is relevant to the context and keywords
|
||||||
|
6. Format dates in ISO format if present
|
||||||
|
7. Use consistent data types within columns
|
||||||
|
8. If table schema is provided, strictly follow the field names and types
|
||||||
|
9. Ensure all required fields from the schema are present in the data
|
||||||
|
10. Format the data according to the specified types in the schema
|
||||||
|
11. For multiple actions, prioritize the first action mentioned
|
||||||
|
12. When creating endpoints, include all related actions in the endpoint description
|
||||||
|
13. For row click actions, use the 'view' action type`;
|
||||||
|
};
|
@ -1,10 +1,11 @@
|
|||||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import DualModalComponent from './layouts/DualModal';
|
import DualModalComponent from './layouts/DualModal';
|
||||||
|
|
||||||
import CouncilAI from './components/CouncilAI/CouncilAI';
|
import CouncilAI from './components/CouncilAI/CouncilAI';
|
||||||
import FuseMindHome from './components/FuseMind/FuseMindHome';
|
import FuseMindHome from './components/FuseMind/FuseMindHome';
|
||||||
import { useParams } from 'react-router-dom';
|
import CanvasEndpoints from './components/canvas-api/CanvasEndpoints';
|
||||||
import CanvasEndpoints from './components/CanvasEndpoints';
|
|
||||||
|
|
||||||
const FuseMindHomeWrapper = () => {
|
const FuseMindHomeWrapper = () => {
|
||||||
const { systemId } = useParams();
|
const { systemId } = useParams();
|
||||||
|
37
frontend/src/shared/components/SettingsMenu.tsx
Normal file
37
frontend/src/shared/components/SettingsMenu.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import { Menu } from 'primereact/menu';
|
||||||
|
|
||||||
|
export interface SettingsMenuItem {
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
command?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsMenuProps {
|
||||||
|
items: SettingsMenuItem[];
|
||||||
|
buttonClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsMenu: React.FC<SettingsMenuProps> = ({ items, buttonClassName }) => {
|
||||||
|
const menuRef = useRef<any>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-cog"
|
||||||
|
aria-label="Settings"
|
||||||
|
onClick={e => menuRef.current.toggle(e)}
|
||||||
|
className={buttonClassName || ''}
|
||||||
|
tooltip="Settings"
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
model={items}
|
||||||
|
popup
|
||||||
|
ref={menuRef}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsMenu;
|
@ -3,7 +3,7 @@ import { SelectButton } from 'primereact/selectbutton';
|
|||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
import Button from '../_V2/Button';
|
import Button from '../_V2/Button';
|
||||||
import { TSizeOptionValue } from '../../../types/DataTable.types';
|
import { TSizeOptionValue } from '../../../components/canvas-api/DataTable.types';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
size: any;
|
size: any;
|
||||||
|
@ -24,14 +24,18 @@ export const useChatStore = create<ChatStore>(() => ({
|
|||||||
|
|
||||||
const formattedRequest = {
|
const formattedRequest = {
|
||||||
data: requestData.data,
|
data: requestData.data,
|
||||||
// model: requestData.model || 'gpt-4o-mini',
|
model: requestData.model || 'gpt-4-mini',
|
||||||
model: requestData.model || 'o4-mini',
|
|
||||||
response_format: requestData.responseFormat === 'json' ? { type: 'json_object' } : undefined
|
response_format: requestData.responseFormat === 'json' ? { type: 'json_object' } : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Sending formatted request:', formattedRequest);
|
console.log('Sending formatted request:', formattedRequest);
|
||||||
const response = await api('post', `/council${endpoint}`, formattedRequest, undefined, 'json', 60, true);
|
const response = await api('post', endpoint, formattedRequest, undefined, 'json', 60, true);
|
||||||
console.log('Got API response:', response);
|
console.log('Got API response:', response);
|
||||||
|
|
||||||
|
if (!response || !response.data) {
|
||||||
|
throw new Error('Invalid response from server');
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to send chat request to ${endpoint}:`, error);
|
console.error(`Failed to send chat request to ${endpoint}:`, error);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Entity, Property, ManyToOne } from '@mikro-orm/core';
|
import { Entity, Property, ManyToOne } from '@mikro-orm/core';
|
||||||
import { BaseEntity } from '../_BaseEntity';
|
import { BaseEntity } from '../_BaseEntity';
|
||||||
import { User } from '../user/_User';
|
import { User } from '../user/_User';
|
||||||
|
import { App } from '../app/_App';
|
||||||
|
import { APIKeyRepository } from '../../repositories/APIKeyRepository';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class APIKey extends BaseEntity {
|
export class APIKey extends BaseEntity {
|
||||||
@ -9,4 +11,9 @@ export class APIKey extends BaseEntity {
|
|||||||
|
|
||||||
@ManyToOne(() => User)
|
@ManyToOne(() => User)
|
||||||
user!: User;
|
user!: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => App)
|
||||||
|
app!: App;
|
||||||
|
|
||||||
|
// Each APIKey is now linked to both a user and an app (system)
|
||||||
}
|
}
|
||||||
|
59
src/apps/_app/http/controllers/APIKeyController.ts
Normal file
59
src/apps/_app/http/controllers/APIKeyController.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
import { APIKeyService } from '../../services/APIKeyService';
|
||||||
|
import { App } from '../../entities/app/_App';
|
||||||
|
import { User } from '../../entities/user/_User';
|
||||||
|
import { APIKey } from '../../entities/apikey/_APIKey';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export class APIKeyController {
|
||||||
|
private apiKeyService: APIKeyService;
|
||||||
|
constructor(apiKeyService: APIKeyService) {
|
||||||
|
this.apiKeyService = apiKeyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrCreateApiKey(request: FastifyRequest<{ Body: { userId: number; appId: number } }>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { userId, appId } = request.body;
|
||||||
|
if (!userId || !appId) {
|
||||||
|
return reply.status(400).send({ error: 'userId and appId are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiKeyEntity = await this.apiKeyService.getAPIKeyEntityForUserAndApp(userId, appId);
|
||||||
|
if (!apiKeyEntity) {
|
||||||
|
// Generate a new API key
|
||||||
|
const key = crypto.randomBytes(32).toString('hex');
|
||||||
|
const em = request.em;
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
const user = await em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
return reply.status(404).send({ error: `User with ID ${userId} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if app exists
|
||||||
|
const app = await em.findOne(App, { id: appId });
|
||||||
|
if (!app) {
|
||||||
|
return reply.status(404).send({ error: `App with ID ${appId} not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new API key
|
||||||
|
apiKeyEntity = new APIKey();
|
||||||
|
apiKeyEntity.key = key;
|
||||||
|
apiKeyEntity.user = user;
|
||||||
|
apiKeyEntity.app = app;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await em.persistAndFlush(apiKeyEntity);
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('Database error while creating API key:', dbError);
|
||||||
|
return reply.status(500).send({ error: 'Failed to create API key in database' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.send({ apiKey: apiKeyEntity.key });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getOrCreateApiKey:', error);
|
||||||
|
return reply.status(500).send({ error: 'Internal server error while processing API key request' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,4 +5,8 @@ export class APIKeyRepository extends EntityRepository<APIKey> {
|
|||||||
async findAPIKeyByUserId(userId: number): Promise<APIKey | null> {
|
async findAPIKeyByUserId(userId: number): Promise<APIKey | null> {
|
||||||
return this.findOne({ user: userId });
|
return this.findOne({ user: userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAPIKeyByUserAndApp(userId: number, appId: number): Promise<APIKey | null> {
|
||||||
|
return this.findOne({ user: userId, app: appId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
20
src/apps/_app/routes/APIKeyRoutes.ts
Normal file
20
src/apps/_app/routes/APIKeyRoutes.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { APIKeyService } from '../services/APIKeyService';
|
||||||
|
import { APIKeyController } from '../http/controllers/APIKeyController';
|
||||||
|
|
||||||
|
const apiKeyRoutes: FastifyPluginAsync = async (app) => {
|
||||||
|
app.post('/generate', async (request: FastifyRequest<{ Body: { userId: number; appId: number } }>, reply: FastifyReply) => {
|
||||||
|
const apiKeyService = new APIKeyService(request.em);
|
||||||
|
const controller = new APIKeyController(apiKeyService);
|
||||||
|
return controller.getOrCreateApiKey(request, reply);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/get', async (request: FastifyRequest<{ Body: { userId: number; appId: number } }>, reply: FastifyReply) => {
|
||||||
|
const apiKeyService = new APIKeyService(request.em);
|
||||||
|
const apiKey = await apiKeyService.getAPIKeyForUserAndApp(request.body.userId, request.body.appId);
|
||||||
|
if (!apiKey) return reply.send({ apiKey: null });
|
||||||
|
return reply.send({ apiKey });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apiKeyRoutes;
|
13
src/apps/_app/routes/AppRoutes.ts
Normal file
13
src/apps/_app/routes/AppRoutes.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { App } from '../entities/app/_App';
|
||||||
|
|
||||||
|
const appRoutes: FastifyPluginAsync = async (app) => {
|
||||||
|
app.get('/by-name/:name', async (request: FastifyRequest<{ Params: { name: string } }>, reply: FastifyReply) => {
|
||||||
|
const { name } = request.params;
|
||||||
|
const appEntity = await request.em.findOne(App, { name });
|
||||||
|
if (!appEntity) return reply.status(404).send({ error: 'App not found' });
|
||||||
|
return reply.send({ id: appEntity.id, name: appEntity.name });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default appRoutes;
|
@ -15,4 +15,13 @@ export class APIKeyService {
|
|||||||
const apiKey = await this.apiKeyRepository.findAPIKeyByUserId(userId);
|
const apiKey = await this.apiKeyRepository.findAPIKeyByUserId(userId);
|
||||||
return apiKey ? apiKey.key : null;
|
return apiKey ? apiKey.key : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAPIKeyForUserAndApp(userId: number, appId: number): Promise<string | null> {
|
||||||
|
const apiKey = await this.apiKeyRepository.findOne({ user: userId, app: appId });
|
||||||
|
return apiKey ? apiKey.key : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAPIKeyEntityForUserAndApp(userId: number, appId: number): Promise<APIKey | null> {
|
||||||
|
return this.apiKeyRepository.findOne({ user: userId, app: appId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,4 +18,7 @@ export class CanvasApiEndpoint extends BaseEntity {
|
|||||||
|
|
||||||
@ManyToOne(() => User, { nullable: true })
|
@ManyToOne(() => User, { nullable: true })
|
||||||
user?: User;
|
user?: User;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
publicPath?: string;
|
||||||
}
|
}
|
@ -1,14 +1,29 @@
|
|||||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import { CanvasApiEndpoint } from '../../entities/CanvasApiEndpoint';
|
import { CanvasApiEndpoint } from '../../entities/CanvasApiEndpoint';
|
||||||
|
|
||||||
|
interface CreateEndpointBody {
|
||||||
|
name: string;
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
description?: string;
|
||||||
|
user?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateEndpointBody {
|
||||||
|
name?: string;
|
||||||
|
method?: string;
|
||||||
|
path?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class CanvasApiEndpointController {
|
export class CanvasApiEndpointController {
|
||||||
async getAll(request: FastifyRequest, reply: FastifyReply) {
|
async getAll(request: FastifyRequest, reply: FastifyReply) {
|
||||||
const endpoints = await request.em.find(CanvasApiEndpoint, {});
|
const endpoints = await request.em.find(CanvasApiEndpoint, {});
|
||||||
return reply.send(endpoints);
|
return reply.send(endpoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(request: FastifyRequest, reply: FastifyReply) {
|
async create(request: FastifyRequest<{ Body: CreateEndpointBody }>, reply: FastifyReply) {
|
||||||
const { name, method, path, description, user } = request.body as any;
|
const { name, method, path, description, user } = request.body;
|
||||||
if (!name || !method || !path) {
|
if (!name || !method || !path) {
|
||||||
return reply.status(400).send({ error: 'name, method, and path are required' });
|
return reply.status(400).send({ error: 'name, method, and path are required' });
|
||||||
}
|
}
|
||||||
@ -31,15 +46,15 @@ export class CanvasApiEndpointController {
|
|||||||
return reply.send({ success: true });
|
return reply.send({ success: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
|
async update(request: FastifyRequest<{ Params: { id: string }; Body: UpdateEndpointBody }>, reply: FastifyReply) {
|
||||||
const id = Number(request.params.id);
|
const id = Number(request.params.id);
|
||||||
if (!id) return reply.status(400).send({ error: 'Missing id' });
|
if (!id) return reply.status(400).send({ error: 'Missing id' });
|
||||||
const endpoint = await request.em.findOne(CanvasApiEndpoint, { id });
|
const endpoint = await request.em.findOne(CanvasApiEndpoint, { id });
|
||||||
if (!endpoint) return reply.status(404).send({ error: 'Not found' });
|
if (!endpoint) return reply.status(404).send({ error: 'Not found' });
|
||||||
const { name, method, path, description } = request.body as any;
|
const { name, method, path, description } = request.body;
|
||||||
if (name) endpoint.name = name;
|
if (name) endpoint.name = name;
|
||||||
if (method) endpoint.method = method;
|
if (method) endpoint.method = method;
|
||||||
if (path) endpoint.path = (path as string).replace(/^\/api\/v1/, '');
|
if (path) endpoint.path = path.replace(/^\/api\/v1/, '');
|
||||||
if (description !== undefined) endpoint.description = description;
|
if (description !== undefined) endpoint.description = description;
|
||||||
await request.em.persistAndFlush(endpoint);
|
await request.em.persistAndFlush(endpoint);
|
||||||
return reply.send(endpoint);
|
return reply.send(endpoint);
|
||||||
|
@ -11,6 +11,21 @@ interface CanvasProxyRequestInput {
|
|||||||
params?: any;
|
params?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateEndpointBody {
|
||||||
|
name: string;
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
description?: string;
|
||||||
|
user?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateEndpointBody {
|
||||||
|
name?: string;
|
||||||
|
method?: string;
|
||||||
|
path?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
||||||
const controller = new CanvasApiEndpointController();
|
const controller = new CanvasApiEndpointController();
|
||||||
|
|
||||||
@ -18,26 +33,26 @@ const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return controller.getAll(request, reply);
|
return controller.getAll(request, reply);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/endpoints', async (request: FastifyRequest, reply: FastifyReply) => {
|
app.post('/endpoints', async (request: FastifyRequest<{ Body: CreateEndpointBody }>, reply: FastifyReply) => {
|
||||||
return controller.create(request, reply);
|
return controller.create(request, reply);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/endpoints/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
app.delete('/endpoints/:id', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
|
||||||
return controller.delete(request, reply);
|
return controller.delete(request, reply);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put('/endpoints/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
app.put('/endpoints/:id', async (request: FastifyRequest<{ Params: { id: string }; Body: UpdateEndpointBody }>, reply: FastifyReply) => {
|
||||||
return controller.update(request, reply);
|
return controller.update(request, reply);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dynamic proxy route for external Canvas REST API
|
// Dynamic proxy route for external Canvas REST API
|
||||||
app.post('/proxy-external', async (request: FastifyRequest, reply: FastifyReply) => {
|
app.post('/proxy-external', async (request: FastifyRequest<{ Body: CanvasProxyRequestInput }>, reply: FastifyReply) => {
|
||||||
try {
|
try {
|
||||||
let baseUrl = process.env.CANVAS_API_URL || 'https://talnet.instructure.com';
|
let baseUrl = process.env.CANVAS_API_URL || 'https://talnet.instructure.com';
|
||||||
// Remove trailing /api/v1 if present
|
// Remove trailing /api/v1 if present
|
||||||
baseUrl = baseUrl.replace(/\/api\/v1$/, '');
|
baseUrl = baseUrl.replace(/\/api\/v1$/, '');
|
||||||
const apiKey = process.env.CANVAS_API_KEY || '';
|
const apiKey = process.env.CANVAS_API_KEY || '';
|
||||||
const { path, method = 'GET', params } = request.body as CanvasProxyRequestInput;
|
const { path, method = 'GET', params } = request.body;
|
||||||
if (!path) return reply.status(400).send({ error: 'Missing path' });
|
if (!path) return reply.status(400).send({ error: 'Missing path' });
|
||||||
if (!ALLOWED_METHODS.includes(method as Method)) {
|
if (!ALLOWED_METHODS.includes(method as Method)) {
|
||||||
return reply.status(405).send({ error: `Method ${method} not allowed` });
|
return reply.status(405).send({ error: `Method ${method} not allowed` });
|
||||||
|
@ -509,6 +509,16 @@
|
|||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"mappedType": "integer"
|
"mappedType": "integer"
|
||||||
|
},
|
||||||
|
"public_path": {
|
||||||
|
"name": "public_path",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"length": 255,
|
||||||
|
"mappedType": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "canvas_api_endpoints",
|
"name": "canvas_api_endpoints",
|
||||||
@ -592,6 +602,15 @@
|
|||||||
"primary": false,
|
"primary": false,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "integer"
|
"mappedType": "integer"
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"name": "app_id",
|
||||||
|
"type": "int",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "apikey",
|
"name": "apikey",
|
||||||
@ -621,6 +640,18 @@
|
|||||||
],
|
],
|
||||||
"referencedTableName": "public.user",
|
"referencedTableName": "public.user",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
|
},
|
||||||
|
"apikey_app_id_foreign": {
|
||||||
|
"constraintName": "apikey_app_id_foreign",
|
||||||
|
"columnNames": [
|
||||||
|
"app_id"
|
||||||
|
],
|
||||||
|
"localTableName": "public.apikey",
|
||||||
|
"referencedColumnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"referencedTableName": "public.app",
|
||||||
|
"updateRule": "cascade"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nativeEnums": {}
|
"nativeEnums": {}
|
||||||
|
@ -11,13 +11,14 @@ export class Migration20240415134130 extends Migration {
|
|||||||
|
|
||||||
this.addSql('create table "tenant_app" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "tenant_id" int not null, "app_id" int not null, "user_id" int not null);');
|
this.addSql('create table "tenant_app" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "tenant_id" int not null, "app_id" int not null, "user_id" int not null);');
|
||||||
|
|
||||||
this.addSql('create table "apikey" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "key" varchar(255) not null, "user_id" int not null);');
|
this.addSql('create table "apikey" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "key" varchar(255) not null, "user_id" int not null, "app_id" int not null);');
|
||||||
|
|
||||||
this.addSql('alter table "tenant_app" add constraint "tenant_app_tenant_id_foreign" foreign key ("tenant_id") references "tenant" ("id") on update cascade;');
|
this.addSql('alter table "tenant_app" add constraint "tenant_app_tenant_id_foreign" foreign key ("tenant_id") references "tenant" ("id") on update cascade;');
|
||||||
this.addSql('alter table "tenant_app" add constraint "tenant_app_app_id_foreign" foreign key ("app_id") references "app" ("id") on update cascade;');
|
this.addSql('alter table "tenant_app" add constraint "tenant_app_app_id_foreign" foreign key ("app_id") references "app" ("id") on update cascade;');
|
||||||
this.addSql('alter table "tenant_app" add constraint "tenant_app_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade;');
|
this.addSql('alter table "tenant_app" add constraint "tenant_app_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade;');
|
||||||
|
|
||||||
this.addSql('alter table "apikey" add constraint "apikey_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade;');
|
this.addSql('alter table "apikey" add constraint "apikey_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade;');
|
||||||
|
this.addSql('alter table "apikey" add constraint "apikey_app_id_foreign" foreign key ("app_id") references "app" ("id") on update cascade;');
|
||||||
}
|
}
|
||||||
|
|
||||||
async down(): Promise<void> {
|
async down(): Promise<void> {
|
||||||
@ -28,6 +29,7 @@ export class Migration20240415134130 extends Migration {
|
|||||||
this.addSql('alter table "tenant_app" drop constraint "tenant_app_user_id_foreign";');
|
this.addSql('alter table "tenant_app" drop constraint "tenant_app_user_id_foreign";');
|
||||||
|
|
||||||
this.addSql('alter table "apikey" drop constraint "apikey_user_id_foreign";');
|
this.addSql('alter table "apikey" drop constraint "apikey_user_id_foreign";');
|
||||||
|
this.addSql('alter table "apikey" drop constraint "apikey_app_id_foreign";');
|
||||||
|
|
||||||
this.addSql('drop table if exists "app" cascade;');
|
this.addSql('drop table if exists "app" cascade;');
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ export class Migration20250502161232 extends Migration {
|
|||||||
this.addSql(`create table "canvas_api_endpoints" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "name" varchar(255) not null, "method" varchar(255) not null, "path" varchar(255) not null, "description" varchar(255) null, "user_id" int null);`);
|
this.addSql(`create table "canvas_api_endpoints" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "name" varchar(255) not null, "method" varchar(255) not null, "path" varchar(255) not null, "description" varchar(255) null, "user_id" int null);`);
|
||||||
|
|
||||||
this.addSql(`alter table "canvas_api_endpoints" add constraint "canvas_api_endpoints_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade on delete set null;`);
|
this.addSql(`alter table "canvas_api_endpoints" add constraint "canvas_api_endpoints_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade on delete set null;`);
|
||||||
|
|
||||||
|
this.addSql('alter table "canvas_api_endpoints" add column "public_path" varchar(255) null;');
|
||||||
}
|
}
|
||||||
|
|
||||||
override async down(): Promise<void> {
|
override async down(): Promise<void> {
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { Migration } from '@mikro-orm/migrations';
|
|
||||||
|
|
||||||
export class Migration20250505072850 extends Migration {
|
|
||||||
|
|
||||||
override async up(): Promise<void> {
|
|
||||||
this.addSql(`alter table "canvas_api_endpoints" drop column "dummy";`);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async down(): Promise<void> {
|
|
||||||
this.addSql(`alter table "canvas_api_endpoints" add column "dummy" varchar(255) null;`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,14 +1,17 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import userRoutes from './apps/_app/routes/UserRoutes';
|
import userRoutes from './apps/_app/routes/UserRoutes';
|
||||||
import canvasRoutes from './apps/canvas-api/canvasRouter';
|
import canvasRoutes from './apps/canvas-api/canvasRouter';
|
||||||
|
import apiKeyRoutes from './apps/_app/routes/APIKeyRoutes';
|
||||||
|
import appRoutes from './apps/_app/routes/AppRoutes';
|
||||||
|
|
||||||
const routesPlugin: FastifyPluginAsync = async (app) => {
|
const routesPlugin: FastifyPluginAsync = async (app) => {
|
||||||
try {
|
try {
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
// TODO: Define routes in separate list
|
// TODO: Define routes in separate list
|
||||||
await app.register(userRoutes, { prefix: '/app/users' });
|
await app.register(userRoutes, { prefix: '/app/users' });
|
||||||
|
await app.register(apiKeyRoutes, { prefix: '/app/apikey' });
|
||||||
await app.register(canvasRoutes, { prefix: '/canvas-api' });
|
await app.register(canvasRoutes, { prefix: '/canvas-api' });
|
||||||
|
await app.register(appRoutes, { prefix: '/app' });
|
||||||
|
|
||||||
app.setErrorHandler((error, request, reply) => {
|
app.setErrorHandler((error, request, reply) => {
|
||||||
if (!reply.sent) {
|
if (!reply.sent) {
|
||||||
|
Loading…
Reference in New Issue
Block a user