feat: adds new JSON viewer (#5407)

* feat: Add JsonEditor component for JSON editing

This commit adds a new component called JsonEditor, which allows users to edit JSON data. The component uses the JSONEditor library and provides options for customizing the editor's appearance and behavior. It also includes functionality for updating the edited data and triggering a callback when the data changes. The component is initialized with an initial data object and can be updated with new data through props. The component is designed to be reusable and can be easily integrated into other parts of the application.

* feat: Add JsonEditor component for JSON editing and replace JsonView

The commit adds a new component called JsonEditor for editing JSON data. It replaces the previous JsonView component used for displaying JSON data. This change improves the functionality and user experience of the application.

* [autofix.ci] apply automated fixes

* Added json editor to package

* Change jsonEditor to use new vanilla jsonEditor

* Added color variables

* Removed unused buttons on json editor

* Removed unused dark store

* Fixed state management on dictAreaModal

* Change default DictComponent value to dict

* removed unused checks

* Changed to forward ref of json editor

* removed nav bar

* Fixed value not being received

* Added check if value is null and handleOnNewValue on this case

* Removed unused button on json editor

* Adds auto focus and change mode to text

* ♻️ (jsonEditor/index.tsx): remove unnecessary comments and improve code readability by removing redundant comments and empty dependency array in useEffect.

* Fixed dict component test

* Refactor json-input component to use VanillaJsonEditor and JsonEditor component

* Remove react-json-view-lite and react18-json-view dependencies

* [autofix.ci] apply automated fixes

* Refactor jsonEditor component to add readOnly prop

* Refactor json-output-view component to add read-only prop

* Refactor switchOutputView component to add JSON output view

* Refactor CSS styles for JSON editor buttons

* Update package-lock.json to add new dependencies for @mapbox/node-pre-gyp and remove jsdom and its related modules

* [autofix.ci] apply automated fixes

* Refactor textModal component to remove unused imports and dependencies

* add filter property to jsonEditor

* [autofix.ci] apply automated fixes

* Refactor jsonEditor component to handle transform queries and display error messages

* [autofix.ci] apply automated fixes

* Refactor jsonEditor component to add filter property and handle transform queries

* Refactor jsonEditor component to add filter property and handle transform queries

* Refactor Output class to add options property

* Refactor json-output-view.tsx to update default height value

* Refactor jsonEditor component to normalize transform query path

* Refactor jsonEditor component to add JSONQuery support for transform queries

* Add jsonquerylang library as a dependency

* Refactor utils.py to add support for applying JSON filters

* Refactor base.py to add apply_options method for JSON filtering

* Fix jsonOutputView to set filter option to undefined when empty

* Enhance apply_json_filter to support Data objects and improve query handling

* Improve json filtering in Output class to return original result if filtered result is None

* Refactor Component class to simplify map_outputs method and enhance output handling

* move jsonquerylang to langflow-base

* Refactor Component and Output classes for improved output handling and type consistency; enhance apply_json_filter to ensure proper data processing and add comprehensive unit tests.

* Add filter_data method to Data class and update apply_options in Output class to utilize it; enhance apply_json_filter return type to Data.

* Import apply_json_filter in Data class to enable data filtering functionality

* Add validation for JSON results in JsonEditor; ensure only objects and arrays are accepted and handle serialization errors

* 📝 (App.css): Add styles for jse-menu separator and cm-gutters to improve UI layout
♻️ (index.tsx): Refactor imports and code formatting for better readability and maintainability
🔧 (index.tsx): Update BaseModal component styles to improve UI layout and responsiveness

* 📝 (custom.css): add extra line at the end of the file for consistency
📝 (App.css): add styling for .jse-menu .jse-button.jse-group-button class
📝 (classes.css): add extra line at the end of the file for consistency

* 🐛 (data.py): fix custom_serializer to handle bytes objects by decoding them to utf-8
♻️ (base.py): refactor options field in Output class to accept BaseModel, dict, or None for better flexibility and compatibility

* Refactor apply_json_filter to directly use jsonquery on result, removing unnecessary string conversion

*  (base.py): Introduce OutputOptions model for output filtering and update Output class to use it

* 🐛 (classes.css): Hide search box background and adjust container positioning for better layout

*  (jsonEditor): Enhance filtering functionality with improved state management and user feedback

* Fixed package lock

* [autofix.ci] apply automated fixes

* Updated package lock

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
anovazzi1 2025-03-17 11:25:50 -03:00 committed by GitHub
commit eec1fb23d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 3171 additions and 1155 deletions

View file

@ -276,3 +276,4 @@ body {
) {
margin-top: 0.25rem !important;
}

View file

@ -155,8 +155,7 @@ class Component(CustomComponent):
self._reset_all_output_values()
if self.inputs is not None:
self.map_inputs(self.inputs)
if self.outputs is not None:
self.map_outputs(self.outputs)
self.map_outputs()
# Final setup
self._set_output_types(list(self._outputs_map.values()))
@ -405,7 +404,7 @@ class Component(CustomComponent):
msg = f"Output {name} not found in {self.__class__.__name__}"
raise ValueError(msg)
def map_outputs(self, outputs: list[Output]) -> None:
def map_outputs(self) -> None:
"""Maps the given list of outputs to the component.
Args:
@ -417,6 +416,19 @@ class Component(CustomComponent):
Returns:
None
"""
# override outputs (generated from the class code) with vertex outputs
# if they exist (generated from the frontend)
outputs = []
if self._vertex and self._vertex.outputs:
for output in self._vertex.outputs:
try:
output_ = Output(**output)
outputs.append(output_)
except ValidationError as e:
msg = f"Invalid output: {e}"
raise ValueError(msg) from e
else:
outputs = self.outputs
for output in outputs:
if output.name is None:
msg = "Output name cannot be None."
@ -980,8 +992,9 @@ class Component(CustomComponent):
and self._vertex.graph.flow_id is not None
):
result.set_flow_id(self._vertex.graph.flow_id)
result = output.apply_options(result)
output.value = result
return result
def _build_artifact(self, result):

View file

@ -235,6 +235,19 @@ class Data(BaseModel):
def __eq__(self, /, other):
return isinstance(other, Data) and self.data == other.data
def filter_data(self, filter_str: str) -> "Data":
"""Filters the data dictionary based on the filter string.
Args:
filter_str (str): The filter string to apply to the data dictionary.
Returns:
Data: The filtered Data.
"""
from langflow.template.utils import apply_json_filter
return apply_json_filter(self.data, filter_str)
def custom_serializer(obj):
if isinstance(obj, datetime):
@ -246,6 +259,8 @@ def custom_serializer(obj):
return str(obj)
if isinstance(obj, BaseModel):
return obj.model_dump()
if isinstance(obj, bytes):
return obj.decode("utf-8", errors="replace")
# Add more custom serialization rules as needed
msg = f"Type {type(obj)} not serializable"
raise TypeError(msg)

View file

@ -20,6 +20,7 @@ from pydantic import (
from langflow.field_typing import Text
from langflow.field_typing.range_spec import RangeSpec
from langflow.helpers.custom import format_type
from langflow.schema.data import Data
from langflow.type_extraction.type_extraction import post_process_type
@ -172,6 +173,11 @@ class Input(BaseModel):
return v
class OutputOptions(BaseModel):
filter: str | None = None
"""Filter to be applied to the output data."""
class Output(BaseModel):
types: list[str] = Field(default=[])
"""List of output types for the field."""
@ -202,6 +208,9 @@ class Output(BaseModel):
allows_loop: bool = Field(default=False)
"""Specifies if the output allows looping."""
options: OutputOptions | None = Field(default=None)
"""Options for the output."""
tool_mode: bool = Field(default=True)
"""Specifies if the output should be used as a tool"""
@ -222,7 +231,6 @@ class Output(BaseModel):
result = handler(self)
if self.value == UNDEFINED:
result["value"] = UNDEFINED.value
return result
@model_validator(mode="after")
@ -234,4 +242,14 @@ class Output(BaseModel):
raise ValueError(msg)
if self.display_name is None:
self.display_name = self.name
# Convert dict options to OutputOptions model
if isinstance(self.options, dict):
self.options = OutputOptions(**self.options)
return self
def apply_options(self, result):
if not self.options:
return result
if self.options.filter and isinstance(result, Data):
return result.filter_data(self.options.filter)
return result

View file

@ -2,6 +2,8 @@ from pathlib import Path
from platformdirs import user_cache_dir
from langflow.schema.data import Data
def raw_frontend_data_is_valid(raw_frontend_data):
"""Check if the raw frontend data is valid for processing."""
@ -96,3 +98,65 @@ def update_frontend_node_with_template_values(frontend_node, raw_frontend_node):
frontend_node["description"] = raw_frontend_node.get("description", frontend_node.get("description", ""))
return frontend_node
def apply_json_filter(result, filter_) -> Data:
"""Apply a json filter to the result.
Args:
result (Data): The JSON data to filter
filter_ (str): The filter query string in jsonquery format
Returns:
Data: The filtered result
"""
if not filter_ or not filter_.strip():
return result
# if result is a Data object, get the data
if isinstance(result, Data):
result = result.data
try:
from jsonquerylang import jsonquery
# If query doesn't start with '.', add it to match jsonquery syntax
return Data(data=jsonquery(result, filter_))
except (ImportError, ValueError, TypeError):
# Fallback to basic path-based filtering
# or if there's an error processing the query
# Normalize array access notation and handle direct key access
filter_str = filter_.strip()
normalized_query = "." + filter_str if not filter_str.startswith(".") else filter_str
normalized_query = normalized_query.replace("[", ".[")
path = normalized_query.strip().split(".")
path = [p for p in path if p]
current = result
for key in path:
if current is None:
return None
# Handle array access
if key.startswith("[") and key.endswith("]"):
try:
index = int(key[1:-1])
if not isinstance(current, list) or index >= len(current):
return None
current = current[index]
except (ValueError, TypeError):
return None
# Handle object access
elif isinstance(current, dict):
if key not in current:
return None
current = current[key]
# Handle array operation
elif isinstance(current, list):
try:
current = [item[key] for item in current if isinstance(item, dict) and key in item]
except (TypeError, KeyError):
return None
else:
return None
return Data(data=current)

View file

@ -81,6 +81,7 @@ dependencies = [
"mcp>=1.1.2",
"aiosqlite>=0.20.0",
"greenlet>=3.1.1",
"jsonquerylang>=1.1.1",
"sqlalchemy[aiosqlite]>=2.0.38,<3.0.0",
]

View file

@ -0,0 +1 @@
"""Utils tests package."""

View file

@ -0,0 +1,97 @@
import pytest
from hypothesis import given
from hypothesis import strategies as st
from langflow.schema.data import Data
from langflow.template.utils import apply_json_filter
# Helper function to create nested dictionaries
def dict_strategy():
return st.recursive(
st.one_of(st.integers(), st.text(), st.floats(allow_nan=False, allow_infinity=False), st.booleans()),
lambda children: st.lists(children, max_size=5) | st.dictionaries(st.text(), children, max_size=5),
max_leaves=10,
)
# Test basic dictionary access
@given(data=st.dictionaries(st.text(), st.integers()), key=st.text())
def test_basic_dict_access(data, key):
if key in data:
result = apply_json_filter(data, key)
assert result == data[key]
else:
result = apply_json_filter(data, key)
assert result is None
# Test array access
@given(data=st.lists(st.integers(), min_size=1), index=st.integers())
def test_array_access(data, index):
filter_str = f"[{index}]"
result = apply_json_filter(data, filter_str)
if 0 <= index < len(data):
assert result == data[index]
else:
assert result is None
# Test nested object access
@given(nested_data=dict_strategy())
def test_nested_object_access(nested_data):
# Wrap in Data object to test both raw and Data object inputs
data_obj = Data(data=nested_data)
result = apply_json_filter(data_obj, "")
assert result == nested_data
# Test edge cases
@pytest.mark.parametrize(
("input_data", "filter_str", "expected"),
[
({}, "", {}), # Empty dict, empty filter
([], "", []), # Empty list, empty filter
(None, "any.path", None), # None input
({"a": 1}, None, {"a": 1}), # None filter
({"a": 1}, " ", {"a": 1}), # Whitespace filter
],
)
def test_edge_cases(input_data, filter_str, expected):
result = apply_json_filter(input_data, filter_str)
assert result == expected
# Test complex nested access
@given(data=st.dictionaries(keys=st.text(), values=st.dictionaries(keys=st.text(), values=st.lists(st.integers()))))
def test_complex_nested_access(data):
if data:
outer_key = next(iter(data))
if data[outer_key]:
inner_key = next(iter(data[outer_key]))
filter_str = f"{outer_key}.{inner_key}"
result = apply_json_filter(data, filter_str)
assert result == data[outer_key][inner_key]
# Test array operations on objects
@given(data=st.lists(st.dictionaries(keys=st.text(), values=st.integers(), min_size=1), min_size=1))
def test_array_object_operations(data):
if data and all(data):
key = next(iter(data[0]))
result = apply_json_filter(data, key)
expected = [item[key] for item in data if key in item]
assert result == expected
# Test invalid inputs
@pytest.mark.parametrize(
("input_data", "filter_str"),
[
({"a": 1}, "[invalid]"), # Invalid array index
([1, 2, 3], "nonexistent"), # Nonexistent key on array
({"a": 1}, "..[invalid]"), # Invalid syntax
],
)
def test_invalid_inputs(input_data, filter_str):
result = apply_json_filter(input_data, filter_str)
assert result is None or isinstance(result, dict | list | Data)

File diff suppressed because it is too large Load diff

View file

@ -67,13 +67,11 @@
"react-hook-form": "^7.52.0",
"react-hotkeys-hook": "^4.5.0",
"react-icons": "^5.2.1",
"react-json-view-lite": "^1.5.0",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
"react-pdf": "^9.0.0",
"react-router-dom": "^6.23.1",
"react-syntax-highlighter": "^15.5.0",
"react18-json-view": "^0.2.8",
"reactflow": "^11.11.3",
"rehype-mathjax": "^4.0.3",
"rehype-raw": "^6.1.1",
@ -84,6 +82,7 @@
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"vanilla-jsoneditor": "^2.3.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.3.2",
"web-vitals": "^4.1.1",

View file

@ -219,3 +219,33 @@ code {
box-shadow: none !important;
border-radius: 0px !important;
}
.jse-space {
border-radius: var(--radius) !important;
}
.jse-button.jse-first {
border-radius: 5px !important;
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
.jse-group-button.jse-button:not(.jse-first) {
border-radius: 5px !important;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
border-right: solid !important;
}
.jse-group-button.jse-button.jse-selected:not(.jse-first) {
border-right: none !important;
}
.jse-menu .jse-button.jse-group-button {
border: 1px solid var(--jse-menu-color, var(--jse-text-color-inverse, #fff)) !important;
}
.jse-menu .jse-separator {
margin-left: 10px !important;
}
.cm-gutters {
display: none !important;
}

View file

@ -1,3 +1,4 @@
import JsonOutputViewComponent from "@/components/core/jsonOutputComponent/json-output-view";
import { MAX_TEXT_LENGTH } from "@/constants/constants";
import { LogsLogType, OutputLogType } from "@/types/api";
import { useMemo } from "react";
@ -36,7 +37,8 @@ const SwitchOutputView: React.FC<SwitchOutputViewProps> = ({
: flowPoolNode?.data?.logs?.[outputName]) ?? {};
const resultType = results?.type;
let resultMessage = results?.message ?? {};
const RECORD_TYPES = ["data", "object", "array", "message"];
const RECORD_TYPES = ["array", "message"];
const JSON_TYPES = ["data", "object"];
if (resultMessage?.raw) {
resultMessage = resultMessage.raw;
}
@ -50,7 +52,6 @@ const SwitchOutputView: React.FC<SwitchOutputViewProps> = ({
) {
return `${resultMessage.substring(0, MAX_TEXT_LENGTH)}...`;
}
if (Array.isArray(resultMessage)) {
return resultMessage.map((item) => {
if (item?.data && typeof item?.data === "object") {
@ -105,6 +106,13 @@ const SwitchOutputView: React.FC<SwitchOutputViewProps> = ({
columnMode="union"
/>
</Case>
<Case condition={JSON_TYPES.includes(resultType)}>
<JsonOutputViewComponent
nodeId={nodeId}
outputName={outputName}
data={resultMessageMemoized}
/>
</Case>
<Case condition={resultType === "stream"}>
<div className="flex h-full w-full items-center justify-center align-middle">

View file

@ -203,9 +203,7 @@ export default function Dropdown({
className="h-4 w-4"
/>
)}
{value && filteredOptions.includes(value)
? value
: placeholderName}{" "}
{value && filteredOptions.includes(value) ? value : placeholderName}{" "}
</span>
<ForwardedIconComponent
name="ChevronsUpDown"

View file

@ -0,0 +1,410 @@
import { jsonquery } from "@jsonquerylang/jsonquery";
import { Check } from "lucide-react";
import { KeyboardEvent, useEffect, useRef, useState } from "react";
import {
Content,
createJSONEditor,
JsonEditor as VanillaJsonEditor,
} from "vanilla-jsoneditor";
import useAlertStore from "../../../stores/alertStore";
import { cn } from "../../../utils/utils";
import { Button } from "../../ui/button";
import { Input } from "../../ui/input";
interface JsonEditorProps {
data?: Content;
onChange?: (data: Content) => void;
readOnly?: boolean;
options?: any;
jsonRef?: React.MutableRefObject<VanillaJsonEditor | null>;
width?: string;
height?: string;
className?: string;
setFilter?: (filter: string) => void;
allowFilter?: boolean;
initialFilter?: string;
}
const JsonEditor = ({
data = { json: {} },
onChange,
readOnly,
jsonRef,
options = {},
width = "100%",
height = "400px",
className,
setFilter,
allowFilter = false,
initialFilter,
}: JsonEditorProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const jsonEditorRef = useRef<VanillaJsonEditor | null>(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const newRef = jsonRef ?? jsonEditorRef;
const [transformQuery, setTransformQuery] = useState(initialFilter ?? "");
const [originalData, setOriginalData] = useState(data);
const [isFiltered, setIsFiltered] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
// Apply initial filter when component mounts
useEffect(() => {
if (initialFilter && newRef.current) {
setTransformQuery(initialFilter);
handleTransform(true);
}
}, [initialFilter, newRef.current]);
const isValidResult = (result: any): boolean => {
// Only allow objects and arrays
return (
result !== null &&
(Array.isArray(result) ||
(typeof result === "object" && !Array.isArray(result)))
);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTransformQuery(e.target.value);
setIsFiltered(false);
setShowSuccess(false);
};
const applyFilter = (filtered: { json: any }, query: string) => {
onChange?.(filtered);
setFilter?.(query.trim());
setShowSuccess(true);
setTimeout(() => {
setShowSuccess(false);
}, 5000);
};
const handleTransform = (isInitial = false) => {
if (!newRef.current) return;
// If query is empty, act as reset
if (!transformQuery.trim()) {
handleReset();
return;
}
try {
// Always start with original data for transformation
const json =
"json" in originalData
? originalData.json
: JSON.parse(originalData.text!);
// Try JSONQuery first
try {
const result = jsonquery(json, transformQuery);
if (result !== undefined) {
// Validate that result is a JSON object or array
if (isValidResult(result)) {
try {
JSON.stringify(result); // Still check JSON serializability
const filteredContent = { json: result };
newRef.current.set(filteredContent);
if (isFiltered && !isInitial) {
// Apply the filter
applyFilter(filteredContent, transformQuery.trim());
} else {
// Just preview the filter
setIsFiltered(true);
}
return;
} catch (jsonError) {
setErrorData({
title: "Invalid Result",
list: [
"The filtered result contains values that cannot be serialized to JSON",
],
});
return;
}
} else {
setErrorData({
title: "Invalid Result",
list: [
"The filtered result must be a JSON object or array, not a primitive value",
],
});
return;
}
}
} catch (jsonQueryError) {
// If JSONQuery fails, continue with our path-based method
console.debug(
"JSONQuery parsing failed, falling back to path-based method:",
jsonQueryError,
);
}
// Fallback to our path-based method
const normalizedQuery = transformQuery.replace(/\[/g, ".[");
const path = normalizedQuery.trim().split(".").filter(Boolean);
let result = json;
for (const key of path) {
if (result === undefined || result === null) {
setErrorData({
title: "Invalid Path",
list: [`Path '${transformQuery}' led to undefined or null value`],
});
return;
}
if (Array.isArray(result)) {
// Handle array access with [index] notation
const indexMatch = key.match(/\[(\d+)\]/);
if (indexMatch) {
const index = parseInt(indexMatch[1]);
if (index >= result.length) {
setErrorData({
title: "Invalid Array Index",
list: [
`Index ${index} is out of bounds for array of length ${result.length}`,
],
});
return;
}
result = result[index];
continue;
}
// Apply operation to all array items
result = result
.map((item) => {
if (!(key in item)) {
setErrorData({
title: "Invalid Property",
list: [`Property '${key}' does not exist in array items`],
});
return undefined;
}
return item[key];
})
.filter((item) => item !== undefined);
} else {
if (!(key in result)) {
setErrorData({
title: "Invalid Property",
list: [`Property '${key}' does not exist in object`],
});
return;
}
result = result[key];
}
}
if (result !== undefined) {
// Validate that result is a JSON object or array
if (isValidResult(result)) {
try {
JSON.stringify(result); // Still check JSON serializability
const filteredContent = { json: result };
newRef.current.set(filteredContent);
if (isFiltered && !isInitial) {
// Apply the filter
applyFilter(filteredContent, transformQuery.trim());
} else {
// Just preview the filter
setIsFiltered(true);
}
return;
} catch (jsonError) {
setErrorData({
title: "Invalid Result",
list: [
"The filtered result contains values that cannot be serialized to JSON",
],
});
}
} else {
setErrorData({
title: "Invalid Result",
list: [
"The filtered result must be a JSON object or array, not a primitive value",
],
});
}
} else {
setErrorData({
title: "Invalid Result",
list: ["Transform resulted in undefined value"],
});
}
} catch (error) {
console.error("Error applying transform:", error);
setErrorData({
title: "Transform Error",
list: [(error as Error).message],
});
}
};
const handleReset = () => {
if (!newRef.current) return;
newRef.current.set(originalData);
onChange?.(originalData);
setTransformQuery("");
setFilter?.("");
setIsFiltered(false);
setShowSuccess(false);
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleTransform();
}
};
const getFilteredContent = (
sourceJson: any,
query: string,
): { json: any } | undefined => {
// Try JSONQuery first
try {
const result = jsonquery(sourceJson, query);
if (result !== undefined && isValidResult(result)) {
try {
JSON.stringify(result); // Check serializability
return { json: result };
} catch {
return undefined;
}
}
} catch (jsonQueryError) {
console.debug(
"JSONQuery parsing failed, falling back to path-based method:",
jsonQueryError,
);
}
// Fallback to path-based method
try {
const normalizedQuery = query.replace(/\[/g, ".[");
const path = normalizedQuery.trim().split(".").filter(Boolean);
let result = sourceJson;
for (const key of path) {
if (result === undefined || result === null) return undefined;
if (Array.isArray(result)) {
const indexMatch = key.match(/\[(\d+)\]/);
if (indexMatch) {
const index = parseInt(indexMatch[1]);
if (index >= result.length) return undefined;
result = result[index];
continue;
}
result = result
.map((item) => (key in item ? item[key] : undefined))
.filter((item) => item !== undefined);
} else {
if (!(key in result)) return undefined;
result = result[key];
}
}
if (result !== undefined && isValidResult(result)) {
try {
JSON.stringify(result);
return { json: result };
} catch {
return undefined;
}
}
} catch {
return undefined;
}
return undefined;
};
useEffect(() => {
if (!containerRef.current) return;
let initialContent = data;
if (initialFilter?.trim()) {
try {
const json = "json" in data ? data.json : JSON.parse(data.text!);
const filtered = getFilteredContent(json, initialFilter);
if (filtered) {
initialContent = filtered;
}
} catch (error) {
console.error("Error applying initial filter:", error);
}
}
const editor = createJSONEditor({
target: containerRef.current,
props: {
...options,
navigationBar: false,
mode: "text",
content: initialContent,
readOnly,
onChange: (content) => {
onChange?.(content);
},
},
});
setTimeout(() => editor.focus(), 100);
newRef.current = editor;
setOriginalData(data);
return () => {
if (newRef.current) {
newRef.current.destroy();
}
};
}, []);
return (
<div className="flex min-h-0 flex-1 flex-col">
{allowFilter && (
<div className="mb-2 flex shrink-0 gap-2">
<Input
placeholder="Enter path (e.g. users[0].name) or JSONQuery (e.g. .users | filter(.age > 25))"
value={transformQuery}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
className="font-mono text-sm"
/>
<Button
onClick={() => handleTransform()}
variant="primary"
size="sm"
className={cn(
"min-w-[60px] whitespace-nowrap",
showSuccess && "!bg-green-500 hover:!bg-green-600",
)}
>
{showSuccess ? (
<Check className="h-4 w-4" />
) : isFiltered ? (
"Apply"
) : (
"Filter"
)}
</Button>
<Button
onClick={handleReset}
variant="outline"
size="sm"
className="whitespace-nowrap"
>
Clear
</Button>
</div>
)}
<div className="relative min-h-0 flex-1">
<div ref={containerRef} className={cn("absolute inset-0", className)} />
</div>
</div>
);
};
export default JsonEditor;

View file

@ -0,0 +1,61 @@
import useFlowStore from "@/stores/flowStore";
import { APIClassType } from "@/types/api";
import React from "react";
import JsonEditor from "../jsonEditor";
interface JsonOutputViewComponentProps {
data: string | object;
width?: string;
height?: string;
nodeId: string;
outputName: string;
}
const JsonOutputViewComponent: React.FC<JsonOutputViewComponentProps> = ({
data,
nodeId,
outputName,
}) => {
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
const setNode = useFlowStore((state) => state.setNode);
const node = useFlowStore((state) => state.getNode(nodeId));
const outputs = (node?.data.node as APIClassType)?.outputs;
const output = outputs?.find((o) => o.name === outputName);
const initialFilter = output?.options?.filter;
return (
<div className="flex flex-1 flex-col">
<JsonEditor
data={{ json: jsonData }}
readOnly={true}
className="flex-1 rounded border border-border"
setFilter={(filter) => {
setNode(nodeId, (old) => {
const outputs = (old.data.node as APIClassType).outputs;
const output = outputs?.find((o) => o.name === outputName);
if (output) {
output.options = {
...output.options,
filter: filter !== "" ? filter : undefined,
};
}
return {
...old,
data: {
...old.data,
node: {
...old.data.node,
outputs: outputs,
},
},
};
});
}}
allowFilter={true}
initialFilter={initialFilter}
/>
</div>
);
};
export default JsonOutputViewComponent;

View file

@ -8,7 +8,7 @@ import { Button } from "../../../../ui/button";
import { InputProps } from "../../types";
export default function DictComponent({
value = [],
value,
handleOnNewValue,
disabled,
editNode = false,
@ -16,7 +16,7 @@ export default function DictComponent({
name = "",
}: InputProps<object | object[] | string, { name: string }>): JSX.Element {
useEffect(() => {
if (disabled) {
if (disabled || value === null) {
handleOnNewValue({ value: {} }, { skipSnapshot: true });
}
}, [disabled]);

View file

@ -1,44 +1,30 @@
import JsonEditor from "@/components/core/jsonEditor";
import { IOJSONInputComponentType } from "@/types/components";
import { useEffect, useRef } from "react";
import JsonView from "react18-json-view";
import { useDarkStore } from "../../../../../stores/darkStore";
import { JsonEditor as VanillaJsonEditor } from "vanilla-jsoneditor";
export default function IoJsonInput({
value = [],
onChange,
left,
output,
}: IOJSONInputComponentType): 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";
};
const jsonEditorRef = useRef<VanillaJsonEditor | null>(null);
useEffect(() => {
if (jsonEditorRef.current) {
jsonEditorRef.current.set({ json: value || {} });
}
}, [value]);
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 className="h-400px w-full">
<JsonEditor
data={{ json: value }}
jsonRef={jsonEditorRef}
height="400px"
/>
</div>
);

View file

@ -44,8 +44,8 @@ const Content: React.FC<ContentProps> = ({
return (
<div
className={cn(
`flex w-full flex-grow flex-col transition-all duration-300`,
overflowHidden ? "overflow-hidden" : "overflow-visible",
`flex flex-1 flex-col rounded-md transition-all duration-300`,
overflowHidden ? "overflow-hidden" : "overflow-auto",
className,
)}
>
@ -238,10 +238,12 @@ function BaseModal({
const contentClasses = cn(
minWidth,
height,
"flex flex-col duration-300 overflow-hidden",
"flex flex-col flex-1 overflow-hidden",
className,
);
const formClasses = "flex flex-col flex-1 gap-6 overflow-hidden";
//UPDATE COLORS AND STYLE CLASSSES
return (
<>
@ -251,7 +253,9 @@ function BaseModal({
<ModalContent className={contentClasses}>{modalContent}</ModalContent>
</Modal>
) : type === "full-screen" ? (
<div className="min-h-full w-full flex-1">{modalContent}</div>
<div className="min-h-full w-full flex-1 overflow-hidden">
{modalContent}
</div>
) : (
<Dialog open={open} onOpenChange={setOpen}>
{triggerChild}
@ -267,7 +271,7 @@ function BaseModal({
event.preventDefault();
onSubmit();
}}
className="flex h-full flex-col gap-6"
className={formClasses}
>
{modalContent}
</Form.Root>

View file

@ -1,16 +1,8 @@
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
// import "ace-builds/webpack-resolver";
import { cloneDeep } from "lodash";
import { useEffect, useState } from "react";
import JsonView from "react18-json-view";
import "react18-json-view/src/dark.css";
import "react18-json-view/src/style.css";
import useAlertStore from "@/stores/alertStore";
import { useEffect, useRef, useState } from "react";
import { JsonEditor as VanillaJsonEditor } from "vanilla-jsoneditor";
import IconComponent from "../../components/common/genericIconComponent";
import { useDarkStore } from "../../stores/darkStore";
import JsonEditor from "../../components/core/jsonEditor";
import BaseModal from "../baseModal";
export default function DictAreaModal({
@ -25,41 +17,53 @@ export default function DictAreaModal({
disabled?: boolean;
}): JSX.Element {
const [open, setOpen] = useState(false);
const isDark = useDarkStore((state) => state.dark);
const [componentValue, setComponentValue] = useState(value);
const setErrorData = useAlertStore((state) => state.setErrorData);
("");
const jsonEditorRef = useRef<VanillaJsonEditor | null>(null);
useEffect(() => {
setComponentValue(value);
if (jsonEditorRef.current) {
jsonEditorRef.current.set({ json: value || {} });
}
}, [value, open]);
const handleSubmit = () => {
if (onChange) {
onChange(componentValue);
setOpen(false);
try {
const componentValue = jsonEditorRef.current?.get() ?? { json: {} };
const jsonValue =
"json" in componentValue
? JSON.parse(JSON.stringify(componentValue.json))
: JSON.parse(componentValue.text!);
onChange(jsonValue);
setOpen(false);
} catch (error) {
console.error("Error getting JSON:", error);
setErrorData({
title: "Error getting dictionary",
list: ["Check your dictionary format"],
});
}
}
};
const handleJsonChange = (edit) => {
setComponentValue(edit.src);
};
const customizeCopy = (copy) => {
navigator.clipboard.writeText(JSON.stringify(copy));
};
const handleChangeType = (type: "array" | "object") => {
setComponentValue((value) => {
if (type === "array") {
if (value && Object.keys(value).length > 0) {
return [value];
}
return [];
jsonEditorRef?.current?.set(typeChanged(type));
};
const typeChanged = (type: "array" | "object") => {
if (type === "array") {
if (value && Object.keys(value).length > 0) {
return { json: [value] };
}
if (value && Array.isArray(value) && value.length > 0) {
return value[0];
}
return {};
});
return { json: [] };
}
if (value && Array.isArray(value) && value.length > 0) {
return { json: value[0] };
}
return { json: {} };
};
const IteractiveReader = () => {
@ -100,13 +104,10 @@ export default function DictAreaModal({
const renderContent = () => (
<BaseModal.Content>
<div className="flex h-full w-full flex-col transition-all">
<JsonView
theme="vscode"
editable={!!onChange}
enableClipboard
onChange={handleJsonChange}
src={cloneDeep(componentValue)}
customizeCopy={customizeCopy}
<JsonEditor
data={{ json: value }}
jsonRef={jsonEditorRef}
height="400px"
/>
</div>
</BaseModal.Content>
@ -118,6 +119,7 @@ export default function DictAreaModal({
open={open}
disable={disabled}
setOpen={setOpen}
className="overflow-visible"
onSubmit={onChange ? handleSubmit : undefined}
>
<BaseModal.Trigger className="h-full" asChild>

View file

@ -5,8 +5,6 @@ import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
// import "ace-builds/webpack-resolver";
import { useState } from "react";
import "react18-json-view/src/dark.css";
import "react18-json-view/src/style.css";
import IconComponent from "../../components/common/genericIconComponent";
import { Button } from "../../components/ui/button";
import BaseModal from "../baseModal";

View file

@ -291,6 +291,21 @@ input[type="search"]::-webkit-search-cancel-button {
min-width: 78px;
}
.jse-group-button.jse-last,
.jse-button.jse-sort,
.jse-dropdown-button:last-child,
.jse-button.jse-transform {
display: none !important;
}
.jse-search-box-background {
display: none !important;
}
.jse-search-box-container {
position: absolute !important;
right: 0 !important;
}
.linenumber {
font-style: normal !important;
}

View file

@ -44,6 +44,24 @@
--tooltip: 0 0% 0%; /* hsl(0, 0%, 0%) */
--tooltip-foreground: 0 0% 100%; /* hsl(0, 0%, 100%) */
--jse-theme-color: hsl(0, 0%, 100%);
--jse-theme-color-highlight: hsl(240, 6%, 90%);
--jse-menu-color: hsl(0, 0%, 0%);
--jse-main-border: hsl(240, 6%, 90%);
--jse-background-color: hsl(0, 0%, 100%);
--jse-text-color: hsl(0, 0%, 0%);
--jse-selection-background-color: hsl(240, 5%, 96%);
--jse-selection-background-inactive-color: hsl(240, 6%, 90%);
--jse-hover-background-color: hsl(240, 5%, 96%);
--jse-active-line-background-color: hsl(240, 5%, 96%);
--jse-search-match-background-color: hsl(240, 5%, 96%);
--jse-search-match-color: hsl(243, 75%, 59%);
--jse-text-readonly: hsl(240, 4%, 46%);
--jse-error-color: hsl(0, 72%, 51%);
--jse-warning-color: hsl(48, 96%, 89%);
--jse-info-color: hsl(221, 83%, 53%);
--jse-success-color: hsl(142, 76%, 36%);
--node-selected: 243 75% 59%;
--round-btn-shadow: #00000063;
--ice: #31a3cc;
@ -207,6 +225,98 @@
--accent-pink-foreground: 329 86% 70%; /* hsl(329, 86%, 70%) */
--tooltip: 0 0% 100%; /* hsl(0, 0%, 100%) */
--jse-theme-color: hsl(240, 6%, 10%) !important;
--jse-theme-color-highlight: hsl(240, 4%, 16%);
--jse-panel-background: hsl(240, 4%, 16%);
--jse-menu-color: hsl(240, 6%, 90%);
--jse-main-border: hsl(240, 5%, 26%);
--jse-background-color: hsl(0, 0%, 0%);
--jse-text-color: hsl(0, 0%, 100%);
--jse-selection-background-color: hsl(240, 4%, 16%);
--jse-selection-background-inactive-color: hsl(240, 5%, 26%);
--jse-hover-background-color: hsl(240, 4%, 16%);
--jse-active-line-background-color: hsl(240, 4%, 16%);
--jse-search-match-background-color: hsl(240, 4%, 16%);
--jse-search-match-color: hsl(234, 89%, 74%);
--jse-text-readonly: hsl(240, 5%, 65%);
--jse-error-color: hsl(0, 84%, 60%);
--jse-warning-color: hsl(45, 97%, 65%);
--jse-info-color: hsl(221, 83%, 53%);
--jse-success-color: hsl(142, 76%, 36%);
--jse-theme: dark;
--jse-text-color-inverse: #4d4d4d;
--jse-modal-background: #2f2f2f;
--jse-modal-overlay-background: rgba(0, 0, 0, 0.5);
--jse-modal-code-background: #2f2f2f;
--jse-tooltip-color: var(--jse-text-color);
--jse-tooltip-background: #4b4b4b;
--jse-tooltip-border: 1px solid #737373;
--jse-tooltip-action-button-color: inherit;
--jse-tooltip-action-button-background: #737373;
--jse-panel-background-border: 1px solid #464646;
--jse-panel-color: var(--jse-text-color);
--jse-panel-color-readonly: #737373;
--jse-panel-border: 1px solid #3c3c3c;
--jse-panel-button-color-highlight: #e5e5e5;
--jse-panel-button-background-highlight: #464646;
--jse-navigation-bar-background: #656565;
--jse-navigation-bar-background-highlight: #7e7e7e;
--jse-navigation-bar-dropdown-color: var(--jse-text-color);
--jse-context-menu-background: #4b4b4b;
--jse-context-menu-background-highlight: #595959;
--jse-context-menu-separator-color: #595959;
--jse-context-menu-color: var(--jse-text-color);
--jse-context-menu-pointer-background: #737373;
--jse-context-menu-pointer-background-highlight: #818181;
--jse-context-menu-pointer-color: var(--jse-context-menu-color);
--jse-key-color: #9cdcfe;
--jse-value-color: var(--jse-text-color);
--jse-value-color-number: #b5cea8;
--jse-value-color-boolean: #569cd6;
--jse-value-color-null: #569cd6;
--jse-value-color-string: #ce9178;
--jse-value-color-url: #ce9178;
--jse-delimiter-color: #949494;
--jse-edit-outline: 2px solid var(--jse-text-color);
--jse-search-match-background-color: #343434;
--jse-collapsed-items-background-color: #333333;
--jse-collapsed-items-selected-background-color: #565656;
--jse-collapsed-items-link-color: #b2b2b2;
--jse-collapsed-items-link-color-highlight: #ec8477;
--jse-search-match-color: #724c27;
--jse-search-match-outline: 1px solid #966535;
--jse-search-match-active-color: #9f6c39;
--jse-search-match-active-outline: 1px solid #bb7f43;
--jse-tag-background: #444444;
--jse-tag-color: #bdbdbd;
--jse-table-header-background: #333333;
--jse-table-header-background-highlight: #424242;
--jse-table-row-odd-background: rgba(255, 255, 255, 0.1);
--jse-input-background: #3d3d3d;
--jse-input-border: var(--jse-main-border);
--jse-button-background: #808080;
--jse-button-background-highlight: #7a7a7a;
--jse-button-color: #e0e0e0;
--jse-button-secondary-background: #494949;
--jse-button-secondary-background-highlight: #5d5d5d;
--jse-button-secondary-background-disabled: #9d9d9d;
--jse-button-secondary-color: var(--jse-text-color);
--jse-a-color: #55abff;
--jse-a-color-highlight: #4387c9;
--jse-svelte-select-background: #3d3d3d;
--jse-svelte-select-border: 1px solid #4f4f4f;
--list-background: #3d3d3d;
--item-hover-bg: #505050;
--multi-item-bg: #5b5b5b;
--input-color: #d4d4d4;
--multi-clear-bg: #8a8a8a;
--multi-item-clear-icon-color: #d4d4d4;
--multi-item-outline: 1px solid #696969;
--list-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.4);
--jse-color-picker-background: #656565;
--jse-color-picker-border-box-shadow: #8c8c8c 0 0 0 1px;
--tooltip-foreground: 0 0% 0%; /* hsl(0, 0%, 0%) */
--error-red: 0, 75%, 15%; /*hsla(0, 75%, 15%)*/
--error-red-border: 0, 70%, 35%; /*hsla(0,70%,35%)*/

View file

@ -106,6 +106,7 @@ export type OutputFieldType = {
hidden?: boolean;
proxy?: OutputFieldProxyType;
allows_loop?: boolean;
options?: { [key: string]: any };
};
export type errorsTypeAPI = {
function: { errors: Array<string> };

View file

@ -29,33 +29,22 @@ test(
await adjustScreenView(page);
await page.getByTestId("dict_nesteddict_metadata").first().click();
await page.getByText("{}").last().clear();
await page
.getByText("{")
.getByRole("textbox")
.last()
.hover()
.then(async () => {
await page.locator(".json-view--edit").first().click();
await page.locator(".json-view--input").first().fill("keytest");
await page.locator(".json-view--edit").first().click();
.fill(
'{"keytest": "proptest", "keytest1": "proptest1", "keytest2": "proptest2"}',
);
await page.locator(".json-view--edit").first().click();
await page.locator(".json-view--input").first().fill("keytest1");
await page.locator(".json-view--edit").first().click();
await page.locator(".json-view--edit").first().click();
await page.locator(".json-view--input").first().fill("keytest2");
await page.locator(".json-view--edit").first().click();
});
await page
.locator(".json-view--pair")
.first()
.hover()
.then(async () => {
await page.locator(".json-view--edit").nth(2).click();
await page.locator(".json-view--null").first().fill("proptest1");
await page.locator(".json-view--edit").nth(2).click();
});
await page.getByTitle("Switch to tree mode (current mode: text)").click();
expect(await page.getByText("keytest", { exact: true }).count()).toBe(1);
expect(await page.getByText("keytest1", { exact: true }).count()).toBe(1);
expect(await page.getByText("keytest2", { exact: true }).count()).toBe(1);
expect(await page.getByText("proptest", { exact: true }).count()).toBe(1);
expect(await page.getByText("proptest1", { exact: true }).count()).toBe(1);
expect(await page.getByText("proptest2", { exact: true }).count()).toBe(1);
await page.getByText("Save").last().click();
@ -65,22 +54,25 @@ test(
await page.getByTestId("advanced-button-modal").click();
await page.getByTestId("edit_dict_nesteddict_edit_metadata").last().click();
await page.getByTitle("Switch to tree mode (current mode: text)").click();
await page.waitForSelector(".jse-bracket", {
timeout: 3000,
});
expect(await page.getByText("keytest", { exact: true }).count()).toBe(1);
expect(await page.getByText("keytest1", { exact: true }).count()).toBe(1);
expect(await page.getByText("keytest2", { exact: true }).count()).toBe(1);
expect(await page.getByText("proptest1").count()).toBe(1);
expect(await page.getByText("proptest", { exact: true }).count()).toBe(1);
expect(await page.getByText("proptest1", { exact: true }).count()).toBe(1);
expect(await page.getByText("proptest2", { exact: true }).count()).toBe(1);
await page
.locator(".json-view--pair")
.first()
.hover()
.then(async () => {
await page.locator(".json-view--edit").nth(3).click();
await page.locator(".json-view--edit").nth(2).click();
});
.getByText("proptest", { exact: true })
.last()
.click({ button: "right" });
await page.getByText("Remove").last().click();
expect(await page.getByText("keytest", { exact: true }).count()).toBe(0);
expect(await page.getByText("proptest1").count()).toBe(0);
expect(await page.getByText("proptest", { exact: true }).count()).toBe(0);
},
);

47
uv.lock generated
View file

@ -634,7 +634,7 @@ name = "blessed"
version = "1.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinxed", marker = "sys_platform == 'win32'" },
{ name = "jinxed", marker = "platform_system == 'Windows'" },
{ name = "six" },
{ name = "wcwidth" },
]
@ -1140,7 +1140,7 @@ name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "colorama", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
@ -3436,7 +3436,7 @@ name = "ipykernel"
version = "6.29.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "appnope", marker = "sys_platform == 'darwin'" },
{ name = "appnope", marker = "platform_system == 'Darwin'" },
{ name = "comm" },
{ name = "debugpy" },
{ name = "ipython" },
@ -3545,7 +3545,7 @@ name = "jinxed"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ansicon", marker = "sys_platform == 'win32'" },
{ name = "ansicon", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/d0/59b2b80e7a52d255f9e0ad040d2e826342d05580c4b1d7d7747cfb8db731/jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf", size = 80981 }
wheels = [
@ -3724,6 +3724,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 },
]
[[package]]
name = "jsonquerylang"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/49/2bc349923d322ef682391c149b59f1e766356b3c98e340233a3cc8c5edc9/jsonquerylang-1.1.1.tar.gz", hash = "sha256:b66cc9cae12135fe9fb7f6e2d4f1f703685806223a180d85d695afa414b9d2ba", size = 13573 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/8a/d4ef935f41b13b617fb72a27a2ca7f0d950cb777fabe4e57b121ed688195/jsonquerylang-1.1.1-py3-none-any.whl", hash = "sha256:40906259692439fce4da345f9175e455a820b38c87d738b8bd9567078644f3d8", size = 12724 },
]
[[package]]
name = "jsonref"
version = "1.1.0"
@ -4610,6 +4619,7 @@ dependencies = [
{ name = "httpx", extra = ["http2"] },
{ name = "jq", marker = "sys_platform != 'win32'" },
{ name = "json-repair" },
{ name = "jsonquerylang" },
{ name = "langchain" },
{ name = "langchain-community" },
{ name = "langchain-core" },
@ -4746,6 +4756,7 @@ requires-dist = [
{ name = "httpx", extras = ["http2"], specifier = ">=0.27,<1.0.0" },
{ name = "jq", marker = "sys_platform != 'win32'", specifier = ">=1.7.0,<2.0.0" },
{ name = "json-repair", specifier = ">=0.30.3" },
{ name = "jsonquerylang", specifier = ">=1.1.1" },
{ name = "langchain", specifier = "~=0.3.10" },
{ name = "langchain-community", specifier = "~=0.3.10" },
{ name = "langchain-core", specifier = "~=0.3.15" },
@ -6737,7 +6748,7 @@ name = "portalocker"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "pywin32", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 }
wheels = [
@ -9218,19 +9229,19 @@ dependencies = [
{ name = "fsspec" },
{ name = "jinja2" },
{ name = "networkx" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "sympy" },
{ name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "typing-extensions" },
]
wheels = [
@ -9271,7 +9282,7 @@ name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "colorama", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
wheels = [