Merge remote-tracking branch 'origin/cz/mergeAll' into fix/edited_component

This commit is contained in:
Lucas Oliveira 2024-06-07 20:06:50 -03:00
commit 98dcadc797
95 changed files with 1389 additions and 716 deletions

View file

@ -44,9 +44,9 @@ import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
import OutputModal from "../outputModal";
import TooltipRenderComponent from "../tooltipRenderComponent";
import { TEXT_FIELD_TYPES } from "./constants";
import OutputModal from "../outputModal";
export default function ParameterComponent({
left,
@ -274,7 +274,9 @@ export default function ParameterComponent({
: "Please build the component first"
}
>
<button
<Button
variant="none"
size="none"
disabled={!displayOutputPreview || unknownOutput}
onClick={() => setOpenOutputModal(true)}
data-testid={`output-inspection-${title.toLowerCase()}`}
@ -288,7 +290,7 @@ export default function ParameterComponent({
)}
name={"ScanEye"}
/>
</button>
</Button>
</ShadTooltip>
)}
</div>

View file

@ -1,10 +1,12 @@
import TableAutoCellRender from "../tableComponent/components/tableAutoCellRender";
export default function ArrayReader({ array }: { array: any[] }): JSX.Element {
//TODO check array type
return (
<div>
<ul>
{array.map((item, index) => (
<li key={index}>{item}</li>
<li key={index}>{<TableAutoCellRender value={item} />}</li>
))}
</ul>
</div>

View file

@ -115,6 +115,7 @@ function CsvOutputComponent({
style={{ height: "100%", width: "100%" }}
>
<TableComponent
key={"csv-output"}
rowData={rowData}
columnDefs={colDefs}
defaultColDef={defaultColDef}

View file

@ -13,7 +13,9 @@ function RecordsOutputComponent({
rows: any;
columnMode?: "intersection" | "union";
}) {
console.log("rows", rows);
const columns = extractColumnsFromRows(rows, columnMode);
console.log("columns", columns);
const columnDefs = columns.map((col, idx) => ({
...col,
@ -23,6 +25,7 @@ function RecordsOutputComponent({
return (
<TableComponent
key={"recordsOutputComponent"}
overlayNoRowsTemplate="No data available"
suppressRowClickSelection={true}
pagination={pagination}

View file

@ -0,0 +1,95 @@
import { cn } from "../../../../utils/utils";
import ShadTooltip from "../../../shadTooltipComponent";
import { Button } from "../../../ui/button";
import IconComponent from "../../../genericIconComponent";
export default function TableOptions({
resetGrid,
duplicateRow,
deleteRow,
hasSelection,
stateChange,
}: {
resetGrid: () => void;
duplicateRow?: () => void;
deleteRow?: () => void;
hasSelection: boolean;
stateChange: boolean;
}): JSX.Element {
return (
<div className={cn("absolute bottom-3 left-6")}>
<div className="flex items-center gap-2">
<div>
<ShadTooltip content="Reset Columns">
<Button
variant="none"
size="none"
onClick={() => {
resetGrid();
}}
disabled={!stateChange}
>
<IconComponent
name="RotateCcw"
className={cn("h-5 w-5 text-primary transition-all")}
/>
</Button>
</ShadTooltip>
</div>
{duplicateRow && (
<div>
<ShadTooltip
content={
!hasSelection ? (
<span>Select items to duplicate</span>
) : (
<span>Duplicate selected items</span>
)
}
>
<Button
variant="none"
size="none"
onClick={duplicateRow}
disabled={!hasSelection}
>
<IconComponent
name="Copy"
className={cn("h-5 w-5 text-primary transition-all")}
/>
</Button>
</ShadTooltip>
</div>
)}
{deleteRow && (
<div>
<ShadTooltip
content={
!hasSelection ? (
<span>Select items to delete</span>
) : (
<span>Delete selected items</span>
)
}
>
<Button
variant="none"
size="none"
onClick={deleteRow}
disabled={!hasSelection}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-primary transition-all",
!hasSelection ? "" : "hover:text-destructive",
)}
/>
</Button>
</ShadTooltip>
</div>
)}{" "}
</div>
</div>
);
}

View file

@ -9,20 +9,17 @@ import { Badge } from "../../../ui/badge";
export default function TableAutoCellRender({
value,
}: CustomCellRendererProps) {
}: CustomCellRendererProps | { value: any }) {
function getCellType() {
switch (typeof value) {
case "object":
if (value === null) {
return String(value);
} else if (Array.isArray(value)) {
return <ArrayReader array={value} />;
} else if (value.definitions) {
// use a custom render defined by the sender
return <ObjectRender object={value} />;
} else {
return <ObjectRender object={value} />;
}
break;
case "string":
if (isTimeStampString(value)) {
return <DateReader date={value} />;

View file

@ -1,7 +1,7 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
import { ElementRef, forwardRef, useRef } from "react";
import { ElementRef, forwardRef, useRef, useState } from "react";
import {
DEFAULT_TABLE_ALERT_MSG,
DEFAULT_TABLE_ALERT_TITLE,
@ -11,7 +11,8 @@ import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
import { cn, toTitleCase } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import ResetColumns from "./components/ResetColumns";
import TableOptions from "./components/TableOptions";
import { useParams } from "react-router-dom";
import resetGrid from "./utils/reset-grid-columns";
interface TableComponentProps extends AgGridReactProps {
@ -20,8 +21,8 @@ interface TableComponentProps extends AgGridReactProps {
alertTitle?: string;
alertDescription?: string;
editable?: boolean | string[];
onDelete?: (selectedRows: any) => void;
onDuplicate?: (selectedRows: any) => void;
onDelete?: () => void;
onDuplicate?: () => void;
}
const TableComponent = forwardRef<
@ -69,9 +70,12 @@ const TableComponent = forwardRef<
});
const gridRef = useRef(null);
// @ts-ignore
const realRef = ref?.current ? ref : gridRef;
const realRef: React.MutableRefObject<AgGridReact> = ref?.current
? ref
: gridRef;
const dark = useDarkStore((state) => state.dark);
const initialColumnDefs = useRef(colDef);
const [columnStateChange, setColumnStateChange] = useState(false);
const makeLastColumnNonResizable = (columnDefs) => {
columnDefs.forEach((colDef, index) => {
@ -87,6 +91,9 @@ const TableComponent = forwardRef<
params.api.setGridOption("columnDefs", updatedColumnDefs);
initialColumnDefs.current = params.api.getColumnDefs();
if (props.onGridReady) props.onGridReady(params);
setTimeout(() => {
setColumnStateChange(false);
}, 50);
};
const onColumnMoved = (params) => {
@ -121,7 +128,7 @@ const TableComponent = forwardRef<
>
<AgGridReact
{...props}
className={cn(props.className, "custom-scroll")}
className={cn(props.className, "cusm-scroll")}
defaultColDef={{
minWidth: 100,
autoHeight: true,
@ -131,8 +138,26 @@ const TableComponent = forwardRef<
pagination={true}
onGridReady={onGridReady}
onColumnMoved={onColumnMoved}
onStateUpdated={(e) => {
console.log(e);
if (e.sources.some((source) => source.includes("column"))) {
setColumnStateChange(true);
}
}}
/>
<TableOptions
stateChange={columnStateChange}
hasSelection={realRef.current?.api.getSelectedRows().length > 0}
duplicateRow={props.onDuplicate ? props.onDuplicate : undefined}
deleteRow={props.onDelete ? props.onDelete : undefined}
resetGrid={() => {
console.log("teste");
resetGrid(realRef, initialColumnDefs);
setTimeout(() => {
setColumnStateChange(false);
}, 100);
}}
/>
<ResetColumns resetGrid={() => resetGrid(realRef, initialColumnDefs)} />
</div>
);
},

View file

@ -982,9 +982,7 @@ export async function postBuildVertex(
files?: string[],
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
const data = input_value
? { inputs: { input_value: input_value } }
: undefined;
const data = { inputs: { input_value: input_value ?? "" } };
if (data && files) {
data["files"] = files;
}

View file

@ -44,43 +44,23 @@ export default function SessionView({ rows }: { rows: Array<any> }) {
}
return (
<div className="flex h-full w-full flex-col justify-between gap-6">
<>
<div className="flex w-full items-center justify-end gap-4 space-y-0.5">
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={handleRemoveMessages}
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
</div>
</div>
</>
<TableComponent
readOnlyEdit
onCellEditRequest={(event) => {
handleUpdateMessage(event);
}}
editable={["Sender Name", "Message"]}
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(event.api.getSelectedRows().map((row) => row.index));
}}
rowSelection="multiple"
suppressRowClickSelection={true}
pagination={true}
columnDefs={columns}
rowData={rows}
/>
</div>
<TableComponent
key={"sessionView"}
onDelete={handleRemoveMessages}
readOnlyEdit
onCellEditRequest={(event) => {
handleUpdateMessage(event);
}}
editable={["Sender Name", "Message"]}
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(event.api.getSelectedRows().map((row) => row.index));
}}
rowSelection="multiple"
suppressRowClickSelection={true}
pagination={true}
columnDefs={columns}
rowData={rows}
/>
);
}

View file

@ -0,0 +1,52 @@
import IconComponent from "../../../../../../../components/genericIconComponent";
import { Case } from "../../../../../../../shared/components/caseComponent";
import { classNames } from "../../../../../../../utils/utils";
const ButtonSendWrapper = ({
send,
lockChat,
noInput,
saveLoading,
chatValue,
}) => {
return (
<button
className={classNames(
"form-modal-send-button",
noInput
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-chat-send text-background",
)}
disabled={lockChat || saveLoading}
onClick={(): void => send()}
>
<Case condition={lockChat || saveLoading}>
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
</Case>
<Case condition={noInput}>
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
</Case>
<Case condition={!(lockChat || saveLoading) && !noInput}>
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
</Case>
</button>
);
};
export default ButtonSendWrapper;

View file

@ -0,0 +1,81 @@
import { Textarea } from "../../../../../../../components/ui/textarea";
import { classNames } from "../../../../../../../utils/utils";
const TextAreaWrapper = ({
checkSendingOk,
send,
lockChat,
noInput,
saveLoading,
chatValue,
setChatValue,
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
inputRef,
setInputFocus,
files,
isDragging,
}) => {
const getPlaceholderText = (
isDragging: boolean,
noInput: boolean,
): string => {
if (isDragging) {
return "Drop here";
} else if (noInput) {
return CHAT_INPUT_PLACEHOLDER;
} else {
return CHAT_INPUT_PLACEHOLDER_SEND;
}
};
const lockClass =
lockChat || saveLoading
? "form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: "form-modal-lock-false bg-background";
const fileClass =
files.length > 0
? "rounded-b-lg ring-0 focus:ring-0 focus:border-2 rounded-t-none border-t-0 border-border focus:border-t-0 focus:border-ring"
: "rounded-md border-t border-border focus:ring-0 focus:border-2 focus:border-ring";
const additionalClassNames = "form-modal-lockchat pl-14";
return (
<Textarea
onFocus={(e) => {
setInputFocus(true);
e.target.style.borderTopWidth = "0";
}}
onBlur={() => setInputFocus(false)}
onKeyDown={(event) => {
if (checkSendingOk(event)) {
send();
}
}}
rows={1}
ref={inputRef}
disabled={lockChat || noInput || saveLoading}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(lockClass, fileClass, additionalClassNames)}
placeholder={getPlaceholderText(isDragging, noInput)}
/>
);
};
export default TextAreaWrapper;

View file

@ -0,0 +1,32 @@
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
const UploadFileButton = ({
fileInputRef,
handleFileChange,
handleButtonClick,
lockChat,
}) => {
return (
<div>
<input
disabled={lockChat}
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<Button
disabled={lockChat}
className={`font-bold text-white transition-all ${lockChat ? "cursor-not-allowed" : "hover:text-muted-foreground"}`}
onClick={handleButtonClick}
variant="none"
size="none"
>
<ForwardedIconComponent name="PaperclipIcon" />
</Button>
</div>
);
};
export default UploadFileButton;

View file

@ -0,0 +1,7 @@
export const getClassNamesFilePreview = (inputFocus) => {
return `flex w-full items-center gap-4 rounded-t-lg bg-background px-14 py-5 overflow-auto custom-scroll ${
inputFocus
? "border border-b-0 border-ring border-2"
: "border border-b-0 border-border"
}`;
};

View file

@ -0,0 +1,14 @@
import { useEffect } from "react";
const useAutoResizeTextArea = (value, inputRef) => {
useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight! !== 0) {
inputRef.current.style!.height = "inherit"; // Reset the height
inputRef.current.style!.height = `${inputRef.current.scrollHeight!}px`; // Set it to the scrollHeight
}
}, [value]);
return inputRef;
};
export default useAutoResizeTextArea;

View file

@ -0,0 +1,55 @@
import ShortUniqueId from "short-unique-id";
import useFileUpload from "./use-file-upload";
const useDragAndDrop = (setIsDragging, setFiles, currentFlowId) => {
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((type) => type === "Files")) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((type) => type === "Files")) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const onDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files, setFiles, currentFlowId);
e.dataTransfer.clearData();
}
setIsDragging(false);
};
return {
dragOver,
dragEnter,
dragLeave,
onDrop,
};
};
const handleFiles = (files, setFiles, currentFlowId) => {
if (files) {
const uid = new ShortUniqueId({ length: 3 });
const id = uid();
const type = files[0].type.split("/")[0];
const blob = files[0];
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
};
export default useDragAndDrop;

View file

@ -0,0 +1,27 @@
import { uploadFile } from "../../../../../../controllers/API";
const useFileUpload = (blob, currentFlowId, setFiles, id) => {
uploadFile(blob, currentFlowId)
.then((res) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].path = res.data.file_path;
return newFiles;
});
})
.catch(() => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].error = true;
return newFiles;
});
});
return null;
};
export default useFileUpload;

View file

@ -0,0 +1,13 @@
import { useEffect } from "react";
const useFocusOnUnlock = (lockChat, inputRef) => {
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();
}
}, [lockChat, inputRef]);
return inputRef;
};
export default useFocusOnUnlock;

View file

@ -0,0 +1,50 @@
import ShortUniqueId from "short-unique-id";
import useFileUpload from "./use-file-upload";
import useAlertStore from "../../../../../../stores/alertStore";
const fsErrorText =
"Please ensure your file has one of the following extensions:";
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
export const useHandleFileChange = (setFiles, currentFlowId) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const fileInput = event.target;
const file = fileInput.files?.[0];
if (file) {
const allowedExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
const fileExtension = file.name.split(".").pop()?.toLowerCase();
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
setErrorData({
title: "Error uploading file",
list: [fsErrorText, snErrorTxt],
});
return;
}
const uid = new ShortUniqueId({ length: 10 }); // Increase the length to ensure uniqueness
const id = uid();
const type = file.type.split("/")[0];
const blob = file;
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
// Clear the file input value to ensure the change event is triggered even for the same file
fileInput.value = "";
};
return {
handleFileChange,
};
};
export default useHandleFileChange;

View file

@ -0,0 +1,61 @@
import { useEffect } from "react";
import ShortUniqueId from "short-unique-id";
import useFileUpload from "./use-file-upload";
import useAlertStore from "../../../../../../stores/alertStore";
const fsErrorText =
"Please ensure your file has one of the following extensions:";
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
const useUpload = (uploadFile, currentFlowId, setFiles, lockChat) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
const handlePaste = (event: ClipboardEvent): void => {
if (lockChat) {
return;
}
const items = event.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
const type = items[0].type.split("/")[0];
const uid = new ShortUniqueId({ length: 3 });
const blob = items[i].getAsFile();
if (blob) {
const allowedExtensions = [
"png",
"jpg",
"jpeg",
"gif",
"bmp",
"webp",
];
const fileExtension = blob.name.split(".").pop()?.toLowerCase();
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
setErrorData({
title: "Error uploading file",
list: [fsErrorText, snErrorTxt],
});
return;
}
const id = uid();
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
}
}
};
document.addEventListener("paste", handlePaste);
return () => {
document.removeEventListener("paste", handlePaste);
};
}, [uploadFile, currentFlowId, lockChat]);
return null;
};
export default useUpload;

View file

@ -1,7 +1,4 @@
import { useEffect, useState } from "react";
import ShortUniqueId from "short-unique-id";
import IconComponent from "../../../../../components/genericIconComponent";
import { Textarea } from "../../../../../components/ui/textarea";
import { useRef, useState } from "react";
import {
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
@ -9,11 +6,18 @@ import {
import { uploadFile } from "../../../../../controllers/API";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import {
ChatInputType,
FilePreviewType,
chatInputType,
} from "../../../../../types/components";
import { classNames } from "../../../../../utils/utils";
import FilePreview from "../filePreviewChat";
import ButtonSendWrapper from "./components/buttonSendWrapper";
import TextAreaWrapper from "./components/textAreaWrapper";
import UploadFileButton from "./components/uploadFileButton";
import { getClassNamesFilePreview } from "./helpers/get-class-file-preview";
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area";
import useFocusOnUnlock from "./hooks/use-focus-unlock";
import useHandleFileChange from "./hooks/use-handle-file-change";
import useUpload from "./hooks/use-upload";
export default function ChatInput({
lockChat,
chatValue,
@ -21,199 +25,102 @@ export default function ChatInput({
setChatValue,
inputRef,
noInput,
}: chatInputType): JSX.Element {
files,
setFiles,
isDragging,
}: ChatInputType): JSX.Element {
const [repeat, setRepeat] = useState(1);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const uid = new ShortUniqueId({ length: 3 });
const [files, setFiles] = useState<FilePreviewType[]>([]);
const [inputFocus, setInputFocus] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();
}
}, [lockChat, inputRef]);
useFocusOnUnlock(lockChat, inputRef);
useAutoResizeTextArea(chatValue, inputRef);
useUpload(uploadFile, currentFlowId, setFiles, lockChat || saveLoading);
const { handleFileChange } = useHandleFileChange(setFiles, currentFlowId);
useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight !== 0) {
inputRef.current.style.height = "inherit"; // Reset the height
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
}
}, [chatValue]);
//listen to ctrl-v to paste images
useEffect(() => {
const handlePaste = (event: ClipboardEvent): void => {
const items = event.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
const blob = items[i].getAsFile();
if (blob) {
const id = uid();
setFiles([
...files,
{ file: blob, loading: true, error: false, id },
]);
uploadFile(blob, currentFlowId)
.then((res) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex(
(file) => file.id === id,
);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].path = res.data.file_path;
return newFiles;
});
})
.catch((err) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex(
(file) => file.id === id,
);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].error = true;
return newFiles;
});
});
}
}
}
}
};
document.addEventListener("paste", handlePaste);
return () => {
document.removeEventListener("paste", handlePaste);
};
}, []);
function send() {
const send = () => {
sendMessage({
repeat,
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
});
setFiles([]);
}
};
const checkSendingOk = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
return (
event.key === "Enter" &&
!lockChat &&
!saveLoading &&
!event.shiftKey &&
!event.nativeEvent.isComposing
);
};
const classNameFilePreview = getClassNamesFilePreview(inputFocus);
const handleButtonClick = () => {
fileInputRef.current!.click();
};
return (
<div className="flex w-full flex-col-reverse">
<div className="relative w-full">
<Textarea
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!lockChat &&
!saveLoading &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
send();
}
}}
rows={1}
ref={inputRef}
disabled={lockChat || noInput || saveLoading}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={
lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue
}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(
lockChat || saveLoading
? " form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: " form-modal-lock-false bg-background",
"form-modal-lockchat",
)}
placeholder={
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
}
<TextAreaWrapper
checkSendingOk={checkSendingOk}
send={send}
lockChat={lockChat}
noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue}
setChatValue={setChatValue}
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
inputRef={inputRef}
setInputFocus={setInputFocus}
files={files}
isDragging={isDragging}
/>
<div className="form-modal-send-icon-position">
<button
className={classNames(
"form-modal-send-button",
noInput
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-chat-send text-background",
)}
disabled={lockChat || saveLoading}
onClick={(): void => send()}
>
{lockChat || saveLoading ? (
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
) : noInput ? (
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
) : (
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
)}
</button>
<ButtonSendWrapper
send={send}
lockChat={lockChat}
noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue}
/>
</div>
<div
className={`absolute bottom-2 left-4 ${lockChat || saveLoading ? "cursor-not-allowed" : ""}`}
>
<UploadFileButton
lockChat={lockChat || saveLoading}
fileInputRef={fileInputRef}
handleFileChange={handleFileChange}
handleButtonClick={handleButtonClick}
/>
</div>
</div>
<div className="flex w-full gap-2 pb-2">
{files.map((file) => (
<FilePreview
error={file.error}
file={file.file}
loading={file.loading}
key={file.id}
onDelete={() => {
setFiles((prev) => prev.filter((f) => f.id !== file.id));
// TODO: delete file on backend
}}
/>
))}
</div>
{/*
<Popover>
<PopoverTrigger asChild>
<Button variant="primary" className="h-13 px-4">
<IconComponent name="Repeat" className="" aria-hidden="true" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-fit">
<div className="flex flex-col items-center justify-center gap-2">
<span className="text-sm">Repetitions: </span>
<Input
onChange={(e) => {
handleChange(parseInt(e.target.value));
{files.length > 0 && (
<div className={classNameFilePreview}>
{files.map((file) => (
<FilePreview
error={file.error}
file={file.file}
loading={file.loading}
key={file.id}
onDelete={() => {
setFiles((prev: FilePreviewType[]) =>
prev.filter((f) => f.id !== file.id),
);
// TODO: delete file on backend
}}
className="w-16"
type="number"
min={0}
/>
</div>
</PopoverContent>
</Popover> */}
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,35 @@
import { useState } from "react";
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import formatFileName from "../../../filePreviewChat/utils/format-file-name";
import FileCard from "../../../fileComponent";
export default function FileCardWrapper({
index,
name,
type,
path,
}: {
index: number;
name: string;
type: string;
path: string;
}) {
const [show, setShow] = useState<boolean>(true);
return (
<div key={index} className="flex flex-col gap-2">
<span
onClick={() => setShow(!show)}
className="flex cursor-pointer gap-2 text-sm text-muted-foreground"
>
{formatFileName(name, 50)}
<ForwardedIconComponent name={show ? "ChevronDown" : "ChevronRight"} />
</span>
<FileCard
showFile={show}
fileName={name}
fileType={type}
content={path}
/>
</div>
);
}

View file

@ -7,13 +7,17 @@ import remarkMath from "remark-math";
import MaleTechnology from "../../../../../assets/male-technologist.png";
import Robot from "../../../../../assets/robot.png";
import CodeTabsComponent from "../../../../../components/codeTabsComponent";
import IconComponent from "../../../../../components/genericIconComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/genericIconComponent";
import SanitizedHTMLWrapper from "../../../../../components/sanitizedHTMLWrapper";
import useAlertStore from "../../../../../stores/alertStore";
import useFlowStore from "../../../../../stores/flowStore";
import { chatMessagePropsType } from "../../../../../types/components";
import { classNames, cn } from "../../../../../utils/utils";
import FileCard from "../fileComponent";
import formatFileName from "../filePreviewChat/utils/format-file-name";
import FileCardWrapper from "./components/fileCardWrapper";
export default function ChatMessage({
chat,
@ -22,6 +26,7 @@ export default function ChatMessage({
updateChat,
setLockChat,
}: chatMessagePropsType): JSX.Element {
const [showFile, setShowFile] = useState<boolean>(true);
const convert = new Convert({ newline: true });
const [hidden, setHidden] = useState(true);
const template = chat.template;
@ -308,34 +313,35 @@ dark:prose-invert"
</span>
</>
) : (
<span
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}
>
{chatMessage}
</span>
<div className="flex flex-col">
<span
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}
>
{chatMessage}
</span>
{chat.files && (
<div className="my-2 flex flex-col gap-5">
{chat.files.map((file, index) => {
return (
<FileCardWrapper
index={index}
name={file.name}
type={file.type}
path={file.path}
/>
);
})}
</div>
)}
</div>
)}
</div>
)}
</div>
<div id={lastMessage ? "last-chat-message" : ""}></div>
{chat.files && (
<div className="my-2 w-full">
{chat.files.map((file, index) => {
return (
<div key={index} className="my-2 w-full">
<FileCard
fileName={file.name}
fileType={file.type}
content={file.path}
/>
</div>
);
})}
</div>
)}
</>
);
}

View file

@ -0,0 +1,30 @@
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
export default function DownloadButton({
isHovered,
handleDownload,
}: {
isHovered: boolean;
handleDownload: () => void;
}): JSX.Element | undefined {
if (isHovered) {
return (
<div
className={`absolute right-1 top-1 rounded-md bg-muted text-sm font-bold text-foreground `}
>
<Button
variant={"none"}
className="bg-transparent px-2 py-1 text-ring"
onClick={handleDownload}
>
<ForwardedIconComponent
name="DownloadCloud"
className="h-5 w-5 bg-transparent text-current hover:scale-110"
/>
</Button>
</div>
);
}
return undefined;
}

View file

@ -1,19 +1,21 @@
import { useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import { ForwardedIconComponent } from "../../../../../components/genericIconComponent";
import { BACKEND_URL, BASE_URL_API } from "../../../../../constants/constants";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import { fileCardPropsType } from "../../../../../types/components";
import formatFileName from "../filePreviewChat/utils/format-file-name";
import DownloadButton from "./components/downloadButton/downloadButton";
import getClasses from "./utils/get-classes";
import handleDownload from "./utils/handle-download";
const imgTypes = new Set(["png", "jpg"]);
const imgTypes = new Set(["png", "jpg", "jpeg", "gif", "webp", "image"]);
export default function FileCard({
fileName,
content,
fileType,
}: fileCardPropsType): JSX.Element {
const handleDownload = (): void => {
//TODO: update download function
};
showFile = true,
}: fileCardPropsType): JSX.Element | undefined {
const [isHovered, setIsHovered] = useState(false);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
function handleMouseEnter(): void {
@ -23,64 +25,57 @@ export default function FileCard({
setIsHovered(false);
}
if (imgTypes.has(fileType)) {
const fileWrapperClasses = getClasses(isHovered);
const imgSrc = `${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1,
)}${BASE_URL_API}files/images/${content}`;
if (showFile) {
if (imgTypes.has(fileType)) {
return (
<div
className="inline-block w-full rounded-lg transition-all"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ display: "inline-block" }}
>
<div className="relative w-[50%] rounded-lg border border-border">
<img
src={imgSrc}
alt="generated image"
className="m-0 h-auto w-auto rounded-lg border border-border p-0 transition-all"
/>
<DownloadButton
isHovered={isHovered}
handleDownload={() => handleDownload({ fileName, content })}
/>
</div>
</div>
);
}
return (
<div
className="relative h-1/4 w-1/4"
className={fileWrapperClasses}
onClick={() => handleDownload({ fileName, content })}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<img
src={`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1,
)}${BASE_URL_API}files/images/${content}`}
alt="generated image"
className="h-full w-full rounded-lg"
/>
{isHovered && (
<div className={`file-card-modal-image-div `}>
<button
className="file-card-modal-image-button "
onClick={handleDownload}
>
<IconComponent
name="DownloadCloud"
className="h-5 w-5 text-current hover:scale-110"
/>
</button>
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<ForwardedIconComponent name="File" className="h-8 w-8" />
<div className="flex flex-col">
<span className="font-bold">{formatFileName(fileName, 20)}</span>
<span>File</span>
</div>
)}
</div>
<DownloadButton
isHovered={isHovered}
handleDownload={() => handleDownload({ fileName, content })}
/>
</div>
);
}
return (
<button onClick={handleDownload} className="file-card-modal-button">
<div className="file-card-modal-div">
{" "}
{imgTypes.has(fileType) ? (
<img
src={`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1,
)}${BASE_URL_API}files/images/${content}`}
alt=""
className="h-8 w-8"
/>
) : (
<IconComponent name="File" className="h-8 w-8" />
)}
<div className="file-card-modal-footer">
{" "}
<div className="file-card-modal-name">{fileName}</div>
<div className="file-card-modal-type">{fileType}</div>
</div>
<IconComponent
name="DownloadCloud"
className="ml-auto h-6 w-6 text-current"
/>
</div>
</button>
);
return undefined;
}

View file

@ -0,0 +1,5 @@
export default function getClasses(isHovered: boolean): string {
return `relative ${false ? "h-20 w-20" : "h-20 w-80"} cursor-pointer rounded-lg border border-ring bg-muted shadow transition duration-300 hover:drop-shadow-lg ${
isHovered ? "shadow-md" : ""
}`;
}

View file

@ -0,0 +1,43 @@
import {
BACKEND_URL,
BASE_URL_API,
} from "../../../../../../constants/constants";
let isDownloading = false;
export default async function handleDownload({
fileName,
content,
}: {
fileName: string;
content: string;
}): Promise<void> {
if (isDownloading) return;
try {
isDownloading = true;
const response = await fetch(
`${BACKEND_URL.slice(0, BACKEND_URL.length - 1)}${BASE_URL_API}files/download/${content}`,
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName); // Set the filename
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); // Clean up the URL object
} catch (error) {
console.error("Failed to download file:", error);
} finally {
isDownloading = false;
}
}

View file

@ -1,6 +1,11 @@
import { useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import LoadingComponent from "../../../../../components/loadingComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/genericIconComponent";
import { Skeleton } from "../../../../../components/ui/skeleton";
import formatFileName from "./utils/format-file-name";
const supImgFiles = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "image"];
export default function FilePreview({
error,
@ -13,35 +18,93 @@ export default function FilePreview({
error: boolean;
onDelete: () => void;
}) {
const fileType = file.type.toLowerCase();
const isImage = supImgFiles.some((type) => fileType.includes(type));
const [isHovered, setIsHovered] = useState(false);
return (
<div className="relative inline-block w-56">
{loading && <LoadingComponent remSize={5} />}
{error && <div>Error...</div>}
<div
className={`relative overflow-hidden rounded-md bg-background p-4 transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<img
src={URL.createObjectURL(file)}
alt="file"
className="block h-auto w-full"
/>
{isHovered && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
<div
className="cursor-pointer rounded-full bg-white bg-opacity-80 p-2"
onClick={onDelete}
<div className="relative inline-block">
{loading ? (
isImage ? (
<div className="flex h-20 w-20 items-center justify-center rounded-md border border-ring bg-background ">
<svg
aria-hidden="true"
className={`h-10 w-10 animate-spin fill-black text-muted`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<IconComponent name="trash" className="stroke-red-500" />
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
) : (
<div
className={`relative ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-wait rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
>
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-48" />
<Skeleton className="h-3 w-10" />
</div>
</div>
</div>
)}
</div>
)
) : error ? (
<div>Error...</div>
) : (
<div
className={`relative mt-2 ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-pointer rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{isImage ? (
<img
src={URL.createObjectURL(file)}
alt="file"
className="block h-full w-full rounded-md border border-border"
/>
) : (
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<ForwardedIconComponent name="File" className="h-8 w-8" />
<div className="flex flex-col">
<span className="font-bold">{formatFileName(file.name)}</span>
<span>File</span>
</div>
</div>
)}
{isHovered && (
<div
className={`absolute ${
isImage ? "bottom-16 left-16" : "bottom-16 left-[19em]"
} flex h-5 w-5 items-center justify-center`}
>
<div
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-gray-200 p-2 transition-all"
onClick={onDelete}
>
<IconComponent name="X" className="stroke-slate-950 stroke-2" />
</div>
</div>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,14 @@
export default function formatFileName(
name: string,
numberToTruncate: number = 25,
): string {
if (name[numberToTruncate] === undefined) {
return name;
}
const fileExtension = name.split(".").pop(); // Get the file extension
const baseName = name.slice(0, name.lastIndexOf(".")); // Get the base name without the extension
if (baseName.length > 6) {
return `${baseName.slice(0, numberToTruncate)}...${fileExtension}`;
}
return name;
}

View file

@ -11,9 +11,10 @@ import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api";
import { ChatMessageType, ChatOutputType } from "../../../../types/chat";
import { chatViewProps } from "../../../../types/components";
import { FilePreviewType, chatViewProps } from "../../../../types/components";
import { classNames } from "../../../../utils/utils";
import ChatInput from "./chatInput";
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
import ChatMessage from "./chatMessage";
export default function ChatView({
@ -61,13 +62,12 @@ export default function ChatView({
const chatMessages: ChatMessageType[] = chatOutputResponses
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
//
.filter(
(output) => output.data.messages && output.data.messages.length > 0,
)
.filter((output) => output.data.message)
.map((output, index) => {
try {
const { sender, message, sender_name, stream_url, files } = output
.data.messages[0] as ChatOutputType;
.data.message as ChatOutputType;
const is_ai = sender === "Machine" || sender === null;
return {
isSend: !is_ai,
@ -154,9 +154,23 @@ export default function ChatView({
// return newChatHistory;
// });
}
const [files, setFiles] = useState<FilePreviewType[]>([]);
const [isDragging, setIsDragging] = useState(false);
const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop(
setIsDragging,
setFiles,
currentFlowId,
);
return (
<div className="eraser-column-arrangement">
<div
className="eraser-column-arrangement"
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
>
<div className="eraser-size">
<div className="eraser-position">
<Button
@ -258,6 +272,9 @@ export default function ChatView({
setChatValue(value);
}}
inputRef={ref}
files={files}
setFiles={setFiles}
isDragging={isDragging}
/>
</div>
</div>

View file

@ -3,33 +3,29 @@ import AccordionComponent from "../../components/accordionComponent";
import IconComponent from "../../components/genericIconComponent";
import ShadTooltip from "../../components/shadTooltipComponent";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import {
CHAT_FORM_DIALOG_SUBTITLE,
OUTPUTS_MODAL_TITLE,
TEXT_INPUT_MODAL_TITLE,
} from "../../constants/constants";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import { InputOutput } from "../../constants/enums";
import { getMessagesTable } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useMessagesStore } from "../../stores/messagesStore";
import { IOModalPropsType } from "../../types/components";
import { NodeDataType, NodeType } from "../../types/flow";
import { updateVerticesOrder } from "../../utils/buildUtils";
import { cn } from "../../utils/utils";
import BaseModal from "../baseModal";
import IOFieldView from "./components/IOFieldView";
import ChatView from "./components/chatView";
import { getMessagesTable } from "../../controllers/API";
import { useMessagesStore } from "../../stores/messagesStore";
import SessionView from "./components/SessionView";
import useRemoveSession from "./components/SessionView/hooks";
import useAlertStore from "../../stores/alertStore";
import { Button } from "../../components/ui/button";
import ChatView from "./components/chatView";
export default function IOModal({
children,
@ -93,12 +89,6 @@ export default function IOModal({
return updateVerticesOrder(currentFlow!.id, null);
}
// useEffect(() => {
// if (open) {
// updateVertices();
// }
// }, [open, currentFlow]);
async function sendMessage({
repeat = 1,
files,
@ -121,10 +111,14 @@ export default function IOModal({
setLockChat(false);
});
}
const { rows, columns } = await getMessagesTable("union", currentFlow!.id);
setMessages(rows);
setColumns(columns);
setLockChat(false);
if (chatInput) {
setNode(chatInput.id, (node: NodeType) => {
const newNode = { ...node };
newNode.data.node!.template["input_value"].value = chatValue;
return newNode;
});

View file

@ -23,8 +23,6 @@ const useColumnDefs = (
: templateParam.name) ?? params.data.key
);
},
tooltipField: "display_name",
tooltipComponent: TableTooltipRender,
wrapText: true,
autoHeight: true,
flex: 1,
@ -35,7 +33,6 @@ const useColumnDefs = (
headerName: "Description",
field: "info",
tooltipField: "info",
tooltipComponent: TableTooltipRender,
wrapText: true,
autoHeight: true,
flex: 2,

View file

@ -92,6 +92,7 @@ const EditNodeModal = forwardRef(
<div className="h-full">
{nodeLength > 0 && (
<TableComponent
key={"editNode"}
onGridReady={(params) => {
setGridApi(params.api);
}}

View file

@ -86,6 +86,7 @@ export default function FlowLogsModal({
</TabsList>
</Tabs>
<TableComponent
key={activeTab}
readOnlyEdit
className="h-max-full h-full w-full"
pagination={rows.length === 0 ? false : true}

View file

@ -6,13 +6,11 @@ import { cn } from "../../../../../../utils/utils";
type ApiKeyHeaderComponentProps = {
selectedRows: string[];
handleDeleteKey: () => void;
fetchApiKeys: () => void;
userId: string;
};
const ApiKeyHeaderComponent = ({
selectedRows,
handleDeleteKey,
fetchApiKeys,
userId,
}: ApiKeyHeaderComponentProps) => {
@ -30,20 +28,6 @@ const ApiKeyHeaderComponent = ({
<p className="text-sm text-muted-foreground">{API_PAGE_PARAGRAPH}</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={handleDeleteKey}
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
<SecretKeyModal data={userId} onCloseModal={fetchApiKeys}>
<Button data-testid="api-key-button-store" variant="primary">
<ForwardedIconComponent name="Plus" className="mr-2 w-4" />

View file

@ -45,13 +45,14 @@ export default function ApiKeysPage() {
<div className="flex h-full w-full flex-col justify-between gap-6">
<ApiKeyHeaderComponent
selectedRows={selectedRows}
handleDeleteKey={handleDeleteKey}
fetchApiKeys={fetchApiKeys}
userId={userId}
/>
<div className="flex h-full w-full flex-col justify-between">
<TableComponent
key={"apiKeys"}
onDelete={handleDeleteKey}
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(event.api.getSelectedRows().map((row) => row.id));

View file

@ -143,20 +143,6 @@ export default function GlobalVariablesPage() {
</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={removeVariables}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
<AddNewVariableButton>
<Button data-testid="api-key-button-store" variant="primary">
<IconComponent name="Plus" className="mr-2 w-4" />
@ -168,6 +154,7 @@ export default function GlobalVariablesPage() {
<div className="flex h-full w-full flex-col justify-between">
<TableComponent
key={"globalVariables"}
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(event.api.getSelectedRows().map((row) => row.name));
@ -177,6 +164,7 @@ export default function GlobalVariablesPage() {
pagination={true}
columnDefs={colDefs}
rowData={rowData}
onDelete={removeVariables}
/>
</div>
</div>

View file

@ -113,6 +113,7 @@ export default function ShortcutsPage() {
</div>
<div className="flex h-full w-full flex-col justify-between">
<TableComponent
key={"shortcuts"}
pagination={false}
columnDefs={colDefs}
rowData={nodesRowData}

View file

@ -8,7 +8,6 @@ type HeaderMessagesComponentProps = {
};
const HeaderMessagesComponent = ({
selectedRows,
handleRemoveMessages,
}: HeaderMessagesComponentProps) => {
return (
<>
@ -26,22 +25,6 @@ const HeaderMessagesComponent = ({
behaviors.
</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={handleRemoveMessages}
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
</div>
</div>
</>
);

View file

@ -53,6 +53,8 @@ export default function MessagesPage() {
<div className="flex h-full w-full flex-col justify-between">
<TableComponent
key={"sessionView"}
onDelete={handleRemoveMessages}
readOnlyEdit
onCellEditRequest={(event) => {
handleUpdateMessage(event);

View file

@ -16,18 +16,14 @@ import {
import { BuildStatus } from "../constants/enums";
import { getFlowPool } from "../controllers/API";
import { VertexBuildTypeAPI } from "../types/api";
import { ChatInputType, ChatOutputType } from "../types/chat";
import {
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import {
ChatOutputType,
FlowStoreType,
VertexLayerElementType,
chatInputType,
} from "../types/zustand/flow";
import { FlowStoreType, VertexLayerElementType } from "../types/zustand/flow";
import { buildVertices } from "../utils/buildUtils";
import {
checkChatInput,
@ -77,7 +73,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
},
updateFlowPool: (
nodeId: string,
data: VertexBuildTypeAPI | ChatOutputType | chatInputType,
data: VertexBuildTypeAPI | ChatOutputType | ChatInputType,
buildId?: string,
) => {
let newFlowPool = cloneDeep({ ...get().flowPool });
@ -94,9 +90,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}
//update data results
else {
newFlowPool[nodeId][index].data.messages[0] = data as
newFlowPool[nodeId][index].data.message = data as
| ChatOutputType
| chatInputType;
| ChatInputType;
}
}
get().setFlowPool(newFlowPool);
@ -492,8 +488,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
(id) => !vertexBuildData.inactivated_vertices?.includes(id),
);
const top_level_vertices = vertexBuildData.top_level_vertices.filter(
(vertex) =>
!vertexBuildData.inactivated_vertices?.includes(vertex.id),
(vertex) => !vertexBuildData.inactivated_vertices?.includes(vertex),
);
const nextVertices: VertexLayerElementType[] = zip(
next_vertices_ids,
@ -518,7 +513,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}
get().addDataToFlowPool(
{ ...vertexBuildData, buildId: runId },
{ ...vertexBuildData, run_id: runId },
vertexBuildData.id,
);

View file

@ -1,5 +1,5 @@
import { Edge, Node, Viewport } from "reactflow";
import { ChatOutputType, chatInputType } from "../chat";
import { ChatInputType, ChatOutputType } from "../chat";
import { FlowType } from "../flow";
//kind and class are just representative names to represent the actual structure of the object received by the API
export type APIDataType = { [key: string]: APIKindType };
@ -158,19 +158,20 @@ export type VertexBuildTypeAPI = {
data: VertexDataTypeAPI;
timestamp: string;
params: any;
messages: ChatOutputType[] | chatInputType[];
messages: ChatOutputType[] | ChatInputType[];
};
// data is the object received by the API
// it has results, artifacts, timedelta, duration
export type VertexDataTypeAPI = {
results: { [key: string]: string };
logs: any[];
messages: ChatOutputType[] | chatInputType[];
logs: { message: any; type: string }[];
messages: ChatOutputType[] | ChatInputType[];
inactive?: boolean;
timedelta?: number;
duration?: string;
artifacts?: any;
message: ChatOutputType | ChatInputType;
};
export type CodeErrorDataTypeAPI = {

View file

@ -22,15 +22,19 @@ export type ChatOutputType = {
files?: Array<{ path: string; type: string; name: string }>;
};
export type chatInputType = {
result: string;
export type ChatInputType = {
message: string;
sender: string;
sender_name: string;
stream_url?: string;
files?: Array<{ path: string; type: string; name: string }>;
};
export type FlowPoolObjectType = {
timestamp: string;
valid: boolean;
// list of chat outputs or list of chat inputs
messages: Array<ChatOutputType | chatInputType> | [];
data: { artifacts?: any; results: any | ChatOutputType | chatInputType };
messages: Array<ChatOutputType | ChatInputType> | [];
data: { artifacts: any; results: any | ChatOutputType | ChatInputType };
id: string;
};

View file

@ -487,7 +487,12 @@ export type headerFlowsType = {
style?: FlowStyleType;
};
export type chatInputType = {
export type ChatInputType = {
isDragging: boolean;
files: FilePreviewType[];
setFiles: (
files: FilePreviewType[] | ((prev: FilePreviewType[]) => FilePreviewType[]),
) => void;
chatValue: string;
inputRef: {
current: any;
@ -526,6 +531,7 @@ export type fileCardPropsType = {
fileName: string;
content: string;
fileType: string;
showFile?: boolean;
};
export type nodeToolbarPropsType = {

View file

@ -9,28 +9,16 @@ import {
} from "reactflow";
import { BuildStatus } from "../../../constants/enums";
import { VertexBuildTypeAPI } from "../../api";
import { ChatInputType, ChatOutputType } from "../../chat";
import { FlowState } from "../../tabs";
export type chatInputType = {
result: string;
files?: string[];
};
export type ChatOutputType = {
message: string;
sender: string;
sender_name: string;
stream_url?: string;
files?: string[];
};
export type FlowPoolObjectType = {
timestamp: string;
valid: boolean;
messages: Array<ChatOutputType | chatInputType> | [];
messages: Array<ChatOutputType | ChatInputType> | [];
data: {
artifacts: any | ChatOutputType | chatInputType;
results: any | ChatOutputType | chatInputType;
artifacts: any | ChatOutputType | ChatInputType;
results: any | ChatOutputType | ChatInputType;
};
duration?: string;
progress?: number;
@ -45,8 +33,8 @@ export type FlowPoolObjectTypeNew = {
timestamp: string;
valid: boolean;
data: {
logs?: any | ChatOutputType | chatInputType;
results: any | ChatOutputType | chatInputType;
logs?: any | ChatOutputType | ChatInputType;
results: any | ChatOutputType | ChatInputType;
};
duration?: string;
progress?: number;
@ -167,7 +155,7 @@ export type FlowStoreType = {
};
updateFlowPool: (
nodeId: string,
data: VertexBuildTypeAPI | ChatOutputType | chatInputType,
data: VertexBuildTypeAPI | ChatOutputType | ChatInputType,
buildId?: string,
) => void;
getNodePosition: (nodeId: string) => { x: number; y: number };

View file

@ -1,4 +1,4 @@
export type chatInputType = {
export type ChatInputType = {
result: string;
};
@ -12,7 +12,7 @@ export type FlowPoolObjectType = {
timestamp: string;
valid: boolean;
params: any;
data: { artifacts: any; results: any | ChatOutputType | chatInputType };
data: { artifacts: any; results: any | ChatOutputType | ChatInputType };
id: string;
};

View file

@ -93,6 +93,7 @@ import {
Package2,
Palette,
Paperclip,
PaperclipIcon,
Pencil,
PencilLine,
Pin,
@ -535,6 +536,7 @@ export const nodeIconsLucide: iconsType = {
FolderPlusIcon,
FolderIcon,
Discord: FaDiscord,
PaperclipIcon,
RotateCcw,
Settings,
Streamlit,

View file

@ -1,7 +1,6 @@
import { ColDef, ColGroupDef } from "ag-grid-community";
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import TableAutoCellRender from "../components/tableComponent/components/tableAutoCellRender";
import { APIDataType, TemplateVariableType } from "../types/api";
import {
groupedObjType,
@ -10,6 +9,7 @@ import {
} from "../types/components";
import { NodeType } from "../types/flow";
import { FlowState } from "../types/tabs";
import TableAutoCellRender from "../components/tableComponent/components/tableAutoCellRender";
export function classNames(...classes: Array<string>): string {
return classes.filter(Boolean).join(" ");

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "fs";
test("user must interact with chat with Input/Output", async ({ page }) => {
if (!process.env.CI) {
@ -147,3 +148,87 @@ test("user must be able to see output inspection", async ({ page }) => {
await page.getByText("Type", { exact: true }).isVisible();
await page.getByText("User", { exact: true }).last().isVisible();
});
test("user must be able to send an image on chat", async ({ page }) => {
if (!process.env.CI) {
dotenv.config();
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
}
await page.goto("/");
await page.waitForTimeout(1000);
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count();
}
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForTimeout(1000);
await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
if (!process.env.OPENAI_API_KEY) {
//You must set the OPENAI_API_KEY on .env file to run this test
expect(false).toBe(true);
}
await page
.getByTestId("popover-anchor-input-openai_api_key")
.fill(process.env.OPENAI_API_KEY ?? "");
await page.getByText("Chat Input", { exact: true }).click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
await page.getByTestId("toggle-edit-return_record").click();
expect(
await page.getByTestId("toggle-edit-return_record").isChecked(),
).toBeTruthy();
await page.getByText("Save Changes").click();
await page.getByText("Playground", { exact: true }).click();
const jsonContent = readFileSync(
"src/frontend/tests/end-to-end/assets/chain.png",
"utf-8",
);
// Create the DataTransfer and File
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
// Convert the buffer to a hex array
const file = new File([data], "chain.png", {
type: "application/json",
});
dt.items.add(file);
return dt;
}, jsonContent);
const element = await page.getByPlaceholder("Send a message...");
await element.dispatchEvent("drop", { dataTransfer });
await page.getByText("chain.png").isVisible();
await page.getByTestId("icon-LucideSend").click();
await page.waitForTimeout(2000);
await page.getByText("chain.png").isVisible();
await page.getByText("Close", { exact: true }).click();
await page.getByTestId("icon-ScanEye").last().click();
await page.getByText("Restart").isHidden();
});