Introducing Versatile IO Components: JSON Viewer, DictionaryIO, and InputListIO (#1822)

In our ongoing commitment to enhancing user experience and streamlining
data interaction, we are thrilled to introduce a trio of powerful IO
components: JSON Viewer, DictionaryIO, and InputListIO. These components
are meticulously crafted to empower developers with seamless data
handling capabilities, enabling efficient processing and visualization
of JSON data structures, dictionaries, and input lists.
This commit is contained in:
Cristhian Zanforlin Lousa 2024-05-02 13:47:43 -03:00 committed by GitHub
commit bbe72887eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 495 additions and 15 deletions

View file

@ -0,0 +1,17 @@
from langflow.base.io.text import TextComponent
from langflow.field_typing.constants import Data, NestedDict
class JsonInput(TextComponent):
display_name = "JSON Input"
description = "JSON Input."
def build_config(self):
return {
"input_value": {
"display_name": "JSON",
"field_type": "NestedDict"
}
}
def build(self, input_value: NestedDict) -> NestedDict:
return input_value

View file

@ -0,0 +1,19 @@
from langflow.base.io.text import TextComponent
from langflow.field_typing.constants import Data
class KeyPairInput(TextComponent):
display_name = "Dictionary Input"
description = "Dictionary Input."
def build_config(self):
return {
"input_value": {
"display_name": "Dictionaries",
"field_type": "dict",
"list": True
}
}
def build(self, input_value: dict) -> dict:
return input_value

View file

@ -0,0 +1,13 @@
# from langflow.field_typing import Data
from langflow.schema import Record
from langflow.interface.custom.custom_component import CustomComponent
class StringListInput(CustomComponent):
display_name = "String List Input"
def build_config(self):
return {"input_value": {"display_name": "String List Input", "field_type": "str", "list": True}}
def build(self, input_value: list) -> Record:
return Record(data=input_value)

View file

@ -0,0 +1,17 @@
from typing import Optional
from langflow.base.io.text import TextComponent
from langflow.field_typing import Text, Data
class CSVOutput(TextComponent):
display_name = "CSV Output"
description = "Used view csv files"
field_config = {
"input_value": {"display_name": "csv","info":"A csv blob","input_types":["Data"]},
"separator": {"display_name": "separator","info":"The separator used in the csv file","input_types":["Text"], "field_type":"Text","default_value":";","options":[";", ",", "|"]},
}
def build(self, input_value: Data, separator) -> Data:
return {"data": input_value, "separator": separator}

View file

@ -0,0 +1,15 @@
from typing import Optional
from langflow.base.io.text import TextComponent
from langflow.field_typing import Text
class ImageOutput(TextComponent):
display_name = "Image Output"
description = "Used view image files"
field_config = {
"input_value": {"display_name": "image","info":"A image url","input_types":["Text"]},
}
def build(self, input_value: Text) -> Text:
return input_value

View file

@ -0,0 +1,17 @@
from langflow.base.io.text import TextComponent
from langflow.field_typing.constants import Data, NestedDict
class JsonOutput(TextComponent):
display_name = "JSON Output"
description = "JSON Output."
def build_config(self):
return {
"input_value": {
"display_name": "JSON",
"field_type": "NestedDict"
}
}
def build(self, input_value: NestedDict) -> NestedDict:
return input_value

View file

@ -0,0 +1,19 @@
from langflow.base.io.text import TextComponent
from langflow.field_typing.constants import Data
class KeyPairOutput(TextComponent):
display_name = "Dictionary Output"
description = "Dictionary Output."
def build_config(self):
return {
"input_value": {
"display_name": "Dictionaries",
"field_type": "dict",
"list": True
}
}
def build(self, input_value: dict) -> dict:
return input_value

View file

@ -0,0 +1,16 @@
from typing import Optional
from langflow.base.io.text import TextComponent
from langflow.field_typing import Text
class PDFOutput(TextComponent):
display_name = "PDF Output"
description = "Used view pdf files"
field_config = {
"input_value": {"display_name": "pdf","info":"A pdf url","input_types":["Text"]},
}
def build(self, input_value: Text) -> Text:
return input_value

View file

@ -0,0 +1,13 @@
# from langflow.field_typing import Data
from langflow.schema import Record
from langflow.interface.custom.custom_component import CustomComponent
class StringListOutput(CustomComponent):
display_name = "String List Output"
def build_config(self):
return {"input_value": {"display_name": "String List Output", "field_type": "str", "list": True}}
def build(self, input_value: list) -> Record:
return Record(data=input_value)

View file

@ -96,6 +96,18 @@ body {
}
.custom-hover:hover {
background-color: rgba(99, 102, 241, 0.1); /* Medium indigo color with 20% opacity */
background-color: rgba(
99,
102,
241,
0.1
); /* Medium indigo color with 20% opacity */
}
.json-view-playground .json-view {
background-color: #fff !important;
}
.json-view-flow .json-view {
background-color: #bbb !important;
}

View file

@ -618,7 +618,7 @@ export default function ParameterComponent({
<FloatComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
rangeSpec={data.node?.template[name].rangeSpec}
rangeSpec={data.node?.template[name]?.rangeSpec}
onChange={handleOnNewValue}
/>
</div>

View file

@ -267,7 +267,7 @@ export default function CodeTabsComponent({
<div className="mx-auto">
{node.data.node.template[
templateField
].list ? (
]?.list ? (
<InputListComponent
componentName={
templateField
@ -745,7 +745,7 @@ export default function CodeTabsComponent({
isList={
node.data.node!.template[
templateField
].list ?? false
]?.list ?? false
}
/>
</div>

View file

@ -12,6 +12,7 @@ export default function InputListComponent({
disabled,
editNode = false,
componentName,
playgroundDisabled,
}: InputListComponentType): JSX.Element {
useEffect(() => {
if (disabled && value.length > 0 && value[0] !== "") {
@ -24,7 +25,7 @@ export default function InputListComponent({
value = [value];
}
if (!value.length) value = [""];
if (!value?.length) value = [""];
return (
<div
@ -37,7 +38,7 @@ export default function InputListComponent({
return (
<div key={idx} className="flex w-full gap-3">
<Input
disabled={disabled}
disabled={disabled || playgroundDisabled}
type="text"
value={singleValue}
className={editNode ? "input-edit-node" : ""}
@ -64,6 +65,7 @@ export default function InputListComponent({
editNode ? "-edit" : ""
}_${componentName}-` + idx
}
disabled={disabled || playgroundDisabled}
>
<IconComponent
name="Plus"
@ -82,10 +84,15 @@ export default function InputListComponent({
newInputList.splice(idx, 1);
onChange(newInputList);
}}
disabled={disabled || playgroundDisabled}
>
<IconComponent
name="X"
className="h-4 w-4 hover:text-status-red"
className={`h-4 w-4 ${
disabled || playgroundDisabled
? ""
: "hover:text-accent-foreground"
}`}
/>
</button>
)}

View file

@ -20,7 +20,13 @@ export default function KeypairListComponent({
}
}, [disabled]);
const ref = useRef(value.length === 0 ? [{ "": "" }] : value);
const checkValueType = (value) => {
return Array.isArray(value) ? value : [value];
};
const ref = useRef<any>([]);
ref.current =
!value || value?.length === 0 ? [{ "": "" }] : checkValueType(value);
useEffect(() => {
if (JSON.stringify(value) !== JSON.stringify(ref.current)) {

View file

@ -711,13 +711,22 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
export const priorityFields = new Set(["code", "template"]);
export const INPUT_TYPES = new Set(["ChatInput", "TextInput", "KeyPairInput"]);
export const INPUT_TYPES = new Set([
"ChatInput",
"TextInput",
"KeyPairInput",
"JsonInput",
"StringListInput",
]);
export const OUTPUT_TYPES = new Set([
"ChatOutput",
"TextOutput",
"PDFOutput",
"ImageOutput",
"CSVOutput",
"JsonOutput",
"KeyPairOutput",
"StringListOutput",
]);
export const CHAT_FIRST_INITIAL_TEXT =

View file

@ -203,7 +203,7 @@ const EditNodeModal = forwardRef(
!myData.node.template[templateParam].options ? (
<div className="mx-auto">
{myData.node.template[templateParam]
.list ? (
?.list ? (
<InputListComponent
componentName={templateParam}
editNode={true}
@ -345,7 +345,7 @@ const EditNodeModal = forwardRef(
}}
isList={
data.node?.template[templateParam]
.list ?? false
?.list ?? false
}
/>
</div>
@ -420,6 +420,10 @@ const EditNodeModal = forwardRef(
.type === "int" ? (
<div className="mx-auto">
<IntComponent
rangeSpec={
data.node?.template[templateParam]
?.rangeSpec
}
id={
"edit-int-input-" +
myData.node.template[templateParam].name

View file

@ -0,0 +1,45 @@
import { useEffect, useRef } from "react";
import JsonView from "react18-json-view";
import { useDarkStore } from "../../../../../../stores/darkStore";
import { DictComponentType } from "../../../../../../types/components";
export default function IoJsonInput({
value = [],
onChange,
left,
output,
}: DictComponentType): JSX.Element {
useEffect(() => {
if (value) onChange(value);
}, [value]);
const isDark = useDarkStore((state) => state.dark);
const ref = useRef<any>(null);
ref.current = value;
const getClassNames = () => {
if (!isDark && !left) return "json-view-playground-white";
if (!isDark && left) return "json-view-playground-white-left";
if (isDark && left) return "json-view-playground-dark-left";
if (isDark && !left) return "json-view-playground-dark";
};
return (
<div className="w-full">
<JsonView
className={getClassNames()}
theme="vscode"
dark={isDark}
editable={!output}
enableClipboard
onEdit={(edit) => {
ref.current = edit["src"];
}}
onChange={(edit) => {
ref.current = edit["src"];
}}
src={ref.current}
/>
</div>
);
}

View file

@ -0,0 +1,107 @@
import _ from "lodash";
import { useRef } from "react";
import IconComponent from "../../../../../../components/genericIconComponent";
import { Input } from "../../../../../../components/ui/input";
import { classNames } from "../../../../../../utils/utils";
export type IOKeyPairInputProps = {
value: any;
onChange: (value: any) => void;
duplicateKey: boolean;
isList: boolean;
isInputField?: boolean;
};
const IOKeyPairInput = ({
value,
onChange,
duplicateKey,
isList = true,
isInputField,
}: IOKeyPairInputProps) => {
const checkValueType = (value) => {
return Array.isArray(value) ? value : [value];
};
const ref = useRef<any>([]);
ref.current =
!value || value?.length === 0 ? [{ "": "" }] : checkValueType(value);
const handleChangeKey = (event, idx) => {
const oldKey = Object.keys(ref.current[idx])[0];
const updatedObj = { [event.target.value]: ref.current[idx][oldKey] };
ref.current[idx] = updatedObj;
onChange(ref.current);
};
const handleChangeValue = (newValue, idx) => {
const key = Object.keys(ref.current[idx])[0];
ref.current[idx][key] = newValue;
onChange(ref.current);
};
return (
<>
<div className={classNames("flex h-full flex-col gap-3")}>
{ref.current?.map((obj, index) => {
return Object.keys(obj).map((key, idx) => {
return (
<div key={idx} className="flex w-full gap-2">
<Input
type="text"
value={key.trim()}
className={classNames(duplicateKey ? "input-invalid" : "")}
placeholder="Type key..."
onChange={(event) => handleChangeKey(event, index)}
disabled={!isInputField}
/>
<Input
type="text"
value={obj[key]}
placeholder="Type a value..."
onChange={(event) =>
handleChangeValue(event.target.value, index)
}
disabled={!isInputField}
/>
{isList && isInputField && index === ref.current.length - 1 ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(ref.current);
newInputList.push({ "": "" });
onChange(newInputList);
}}
>
<IconComponent
name="Plus"
className={"h-4 w-4 hover:text-accent-foreground"}
/>
</button>
) : isList && isInputField ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(ref.current);
newInputList.splice(index, 1);
onChange(newInputList);
}}
>
<IconComponent
name="X"
className="h-4 w-4 hover:text-status-red"
/>
</button>
) : (
""
)}
</div>
);
});
})}
</div>
</>
);
};
export default IOKeyPairInput;

View file

@ -1,6 +1,8 @@
import { cloneDeep } from "lodash";
import { useState } from "react";
import ImageViewer from "../../../../components/ImageViewer";
import CsvOutputComponent from "../../../../components/csvOutputComponent";
import InputListComponent from "../../../../components/inputListComponent";
import PdfViewer from "../../../../components/pdfViewer";
import {
Select,
@ -15,7 +17,13 @@ import { PDFViewConstant } from "../../../../constants/constants";
import { InputOutput } from "../../../../constants/enums";
import useFlowStore from "../../../../stores/flowStore";
import { IOFieldViewProps } from "../../../../types/components";
import {
convertValuesToNumbers,
hasDuplicateKeys,
} from "../../../../utils/reactflowUtils";
import IOFileInput from "./components/FileInput";
import IoJsonInput from "./components/JSONInput";
import IOKeyPairInput from "./components/keyPairInput";
export default function IOFieldView({
type,
@ -39,6 +47,7 @@ export default function IOFieldView({
}
}
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
function handleOutputType() {
if (!node) return <>"No node found!"</>;
@ -78,6 +87,57 @@ export default function IOFieldView({
/>
);
case "KeyPairInput":
return (
<IOKeyPairInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
const valueToNumbers = convertValuesToNumbers(e);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
}}
duplicateKey={errorDuplicateKey}
isList={node.data.node!.template["input_value"]?.list ?? false}
isInputField
/>
);
case "JsonInput":
return (
<IoJsonInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
left={left}
/>
);
case "StringListInput":
return (
<>
<InputListComponent
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
disabled={false}
/>
</>
);
default:
return (
<Textarea
@ -169,6 +229,58 @@ export default function IOFieldView({
/>
);
case "JsonOutput":
return (
<IoJsonInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
left={left}
output
/>
);
case "KeyPairOutput":
return (
<IOKeyPairInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
const valueToNumbers = convertValuesToNumbers(e);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
}}
duplicateKey={errorDuplicateKey}
isList={node.data.node!.template["input_value"]?.list ?? false}
/>
);
case "StringListOutput":
return (
<>
<InputListComponent
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
playgroundDisabled
disabled={false}
/>
</>
);
default:
return (
<Textarea

View file

@ -11,6 +11,7 @@ import "react18-json-view/src/style.css";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { CODE_DICT_DIALOG_SUBTITLE } from "../../constants/constants";
import { useDarkStore } from "../../stores/darkStore";
import BaseModal from "../baseModal";
export default function DictAreaModal({
@ -19,7 +20,7 @@ export default function DictAreaModal({
value,
}): JSX.Element {
const [open, setOpen] = useState(false);
const isDark = useDarkStore((state) => state.dark);
const ref = useRef(value);
useEffect(() => {
@ -41,7 +42,8 @@ export default function DictAreaModal({
<div className="flex h-full w-full flex-col transition-all ">
<JsonView
theme="vscode"
dark={true}
dark={isDark}
className={!isDark ? "json-view-white" : "json-view-dark"}
editable
enableClipboard
onEdit={(edit) => {

View file

@ -68,3 +68,31 @@ select:-webkit-autofill:focus {
background-color: #bbb;
border-radius: 999px;
}
.json-view-playground-white-left {
background-color: #fff !important;
height: fit-content !important;
}
.json-view-playground-dark {
background-color: #141924 !important;
height: fit-content !important;
}
.json-view-playground-white {
background-color: #f8fafc !important;
height: fit-content !important;
}
.json-view-playground-dark-left {
background-color: #0c101a !important;
height: fit-content !important;
}
.json-view-white {
background-color: #f8fafc !important;
}
.json-view-dark {
background-color: #141924 !important;
}

View file

@ -69,6 +69,7 @@ export type InputListComponentType = {
disabled: boolean;
editNode?: boolean;
componentName?: string;
playgroundDisabled?: boolean;
};
export type InputGlobalComponentType = {
@ -80,7 +81,6 @@ export type InputGlobalComponentType = {
editNode?: boolean;
};
export type KeyPairListComponentType = {
value: any;
onChange: (value: Object[]) => void;
@ -94,9 +94,11 @@ export type KeyPairListComponentType = {
export type DictComponentType = {
value: any;
onChange: (value) => void;
disabled: boolean;
disabled?: boolean;
editNode?: boolean;
id?: string;
left?: boolean;
output?: boolean;
};
export type TextAreaComponentType = {