Compare commits
60 Commits
main
...
deploy/dev
| Author | SHA1 | Date |
|---|---|---|
|
|
f03afaf052 | 6 months ago |
|
|
30393043b1 | 6 months ago |
|
|
ae49a2bdd9 | 6 months ago |
|
|
c271c65c85 | 6 months ago |
|
|
a1de4fa428 | 6 months ago |
|
|
952bce4196 | 6 months ago |
|
|
a2172117a5 | 6 months ago |
|
|
24fdf10d1d | 6 months ago |
|
|
75303d763d | 6 months ago |
|
|
ed27b67422 | 6 months ago |
|
|
4f4473e3d0 | 6 months ago |
|
|
0ee992733c | 6 months ago |
|
|
c3940f1eed | 6 months ago |
|
|
796dc65813 | 6 months ago |
|
|
1523e1db74 | 6 months ago |
|
|
a3074aa0a5 | 6 months ago |
|
|
35e40b5769 | 6 months ago |
|
|
807a6890ac | 6 months ago |
|
|
d6af356248 | 6 months ago |
|
|
7ead579335 | 6 months ago |
|
|
613f39c96a | 7 months ago |
|
|
b6d55173cb | 7 months ago |
|
|
c4f0f3e5e0 | 7 months ago |
|
|
ae50d3e22f | 7 months ago |
|
|
4a417ff52b | 7 months ago |
|
|
c4b899ed60 | 7 months ago |
|
|
91b0672001 | 7 months ago |
|
|
0066cfe4ed | 7 months ago |
|
|
a44a200245 | 7 months ago |
|
|
5ad4465c9c | 7 months ago |
|
|
c78130990d | 7 months ago |
|
|
07288bda8b | 7 months ago |
|
|
ab45f3ce26 | 7 months ago |
|
|
71d08d0580 | 7 months ago |
|
|
534ece0ad0 | 7 months ago |
|
|
f438af1aa1 | 7 months ago |
|
|
4e27d2c35f | 7 months ago |
|
|
525e64467d | 7 months ago |
|
|
98c70ff86d | 7 months ago |
|
|
219c96aee1 | 7 months ago |
|
|
df95b6eba0 | 7 months ago |
|
|
f60792bcc7 | 7 months ago |
|
|
ba8129cf73 | 7 months ago |
|
|
635e92d762 | 7 months ago |
|
|
e2512b0af9 | 7 months ago |
|
|
a5653f253a | 7 months ago |
|
|
ed737beb4a | 7 months ago |
|
|
a85908386e | 7 months ago |
|
|
677716346a | 7 months ago |
|
|
f14678a0d8 | 7 months ago |
|
|
945a424fa8 | 7 months ago |
|
|
424a563055 | 7 months ago |
|
|
d82abbef23 | 7 months ago |
|
|
7907235124 | 7 months ago |
|
|
5d232ac1bc | 7 months ago |
|
|
4c76a5f57b | 7 months ago |
|
|
3160e5e562 | 7 months ago |
|
|
da560e5950 | 7 months ago |
|
|
6075ca5f59 | 7 months ago |
|
|
7d80cb6d95 | 7 months ago |
@ -0,0 +1,11 @@
|
||||
from tests.integration_tests.utils.parent_class import ParentClass
|
||||
|
||||
|
||||
class ChildClass(ParentClass):
|
||||
"""Test child class for module import helper tests"""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
def get_name(self):
|
||||
return f"Child: {self.name}"
|
||||
@ -0,0 +1,11 @@
|
||||
from tests.integration_tests.utils.parent_class import ParentClass
|
||||
|
||||
|
||||
class LazyLoadChildClass(ParentClass):
|
||||
"""Test lazy load child class for module import helper tests"""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test to verify boolean classes can be imported correctly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the api directory to the Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
|
||||
|
||||
try:
|
||||
# Test that we can import the boolean classes
|
||||
from core.variables.segments import BooleanSegment, ArrayBooleanSegment
|
||||
from core.variables.variables import BooleanVariable, ArrayBooleanVariable
|
||||
from core.variables.types import SegmentType
|
||||
|
||||
print("✅ Successfully imported BooleanSegment")
|
||||
print("✅ Successfully imported ArrayBooleanSegment")
|
||||
print("✅ Successfully imported BooleanVariable")
|
||||
print("✅ Successfully imported ArrayBooleanVariable")
|
||||
print("✅ Successfully imported SegmentType")
|
||||
|
||||
# Test that the segment types exist
|
||||
print(f"✅ SegmentType.BOOLEAN = {SegmentType.BOOLEAN}")
|
||||
print(f"✅ SegmentType.ARRAY_BOOLEAN = {SegmentType.ARRAY_BOOLEAN}")
|
||||
|
||||
# Test creating boolean segments directly
|
||||
bool_seg = BooleanSegment(value=True)
|
||||
print(f"✅ Created BooleanSegment: {bool_seg}")
|
||||
print(f" Value type: {bool_seg.value_type}")
|
||||
print(f" Value: {bool_seg.value}")
|
||||
|
||||
array_bool_seg = ArrayBooleanSegment(value=[True, False, True])
|
||||
print(f"✅ Created ArrayBooleanSegment: {array_bool_seg}")
|
||||
print(f" Value type: {array_bool_seg.value_type}")
|
||||
print(f" Value: {array_bool_seg.value}")
|
||||
|
||||
print("\n🎉 All boolean class imports and basic functionality work correctly!")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Import error: {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script to verify boolean condition support in IfElseNode
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the api directory to the Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
|
||||
|
||||
from core.workflow.utils.condition.processor import (
|
||||
ConditionProcessor,
|
||||
_evaluate_condition,
|
||||
)
|
||||
|
||||
|
||||
def test_boolean_conditions():
|
||||
"""Test boolean condition evaluation"""
|
||||
print("Testing boolean condition support...")
|
||||
|
||||
# Test boolean "is" operator
|
||||
result = _evaluate_condition(value=True, operator="is", expected="true")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'is' with True value passed")
|
||||
|
||||
result = _evaluate_condition(value=False, operator="is", expected="false")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'is' with False value passed")
|
||||
|
||||
# Test boolean "is not" operator
|
||||
result = _evaluate_condition(value=True, operator="is not", expected="false")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'is not' with True value passed")
|
||||
|
||||
result = _evaluate_condition(value=False, operator="is not", expected="true")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'is not' with False value passed")
|
||||
|
||||
# Test boolean "=" operator
|
||||
result = _evaluate_condition(value=True, operator="=", expected="1")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean '=' with True=1 passed")
|
||||
|
||||
result = _evaluate_condition(value=False, operator="=", expected="0")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean '=' with False=0 passed")
|
||||
|
||||
# Test boolean "≠" operator
|
||||
result = _evaluate_condition(value=True, operator="≠", expected="0")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean '≠' with True≠0 passed")
|
||||
|
||||
result = _evaluate_condition(value=False, operator="≠", expected="1")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean '≠' with False≠1 passed")
|
||||
|
||||
# Test boolean "in" operator
|
||||
result = _evaluate_condition(value=True, operator="in", expected=["true", "false"])
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'in' with True in array passed")
|
||||
|
||||
result = _evaluate_condition(value=False, operator="in", expected=["true", "false"])
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'in' with False in array passed")
|
||||
|
||||
# Test boolean "not in" operator
|
||||
result = _evaluate_condition(value=True, operator="not in", expected=["false", "0"])
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'not in' with True not in [false, 0] passed")
|
||||
|
||||
# Test boolean "null" and "not null" operators
|
||||
result = _evaluate_condition(value=True, operator="not null", expected=None)
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'not null' with True passed")
|
||||
|
||||
result = _evaluate_condition(value=False, operator="not null", expected=None)
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Boolean 'not null' with False passed")
|
||||
|
||||
print("\n🎉 All boolean condition tests passed!")
|
||||
|
||||
|
||||
def test_backward_compatibility():
|
||||
"""Test that existing string and number conditions still work"""
|
||||
print("\nTesting backward compatibility...")
|
||||
|
||||
# Test string conditions
|
||||
result = _evaluate_condition(value="hello", operator="is", expected="hello")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ String 'is' condition still works")
|
||||
|
||||
result = _evaluate_condition(value="hello", operator="contains", expected="ell")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ String 'contains' condition still works")
|
||||
|
||||
# Test number conditions
|
||||
result = _evaluate_condition(value=42, operator="=", expected="42")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Number '=' condition still works")
|
||||
|
||||
result = _evaluate_condition(value=42, operator=">", expected="40")
|
||||
assert result == True, f"Expected True, got {result}"
|
||||
print("✓ Number '>' condition still works")
|
||||
|
||||
print("✓ Backward compatibility maintained!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_boolean_conditions()
|
||||
test_backward_compatibility()
|
||||
print(
|
||||
"\n✅ All tests passed! Boolean support has been successfully added to IfElseNode."
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed: {e}")
|
||||
sys.exit(1)
|
||||
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Test script to verify the boolean array comparison fix in condition processor.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the api directory to the Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
|
||||
|
||||
from core.workflow.utils.condition.processor import (
|
||||
_assert_contains,
|
||||
_assert_not_contains,
|
||||
)
|
||||
|
||||
|
||||
def test_boolean_array_contains():
|
||||
"""Test that boolean arrays work correctly with string comparisons."""
|
||||
|
||||
# Test case 1: Boolean array [True, False, True] contains "true"
|
||||
bool_array = [True, False, True]
|
||||
|
||||
# Should return True because "true" converts to True and True is in the array
|
||||
result1 = _assert_contains(value=bool_array, expected="true")
|
||||
print(f"Test 1 - [True, False, True] contains 'true': {result1}")
|
||||
assert result1 == True, "Expected True but got False"
|
||||
|
||||
# Should return True because "false" converts to False and False is in the array
|
||||
result2 = _assert_contains(value=bool_array, expected="false")
|
||||
print(f"Test 2 - [True, False, True] contains 'false': {result2}")
|
||||
assert result2 == True, "Expected True but got False"
|
||||
|
||||
# Test case 2: Boolean array [True, True] does not contain "false"
|
||||
bool_array2 = [True, True]
|
||||
result3 = _assert_contains(value=bool_array2, expected="false")
|
||||
print(f"Test 3 - [True, True] contains 'false': {result3}")
|
||||
assert result3 == False, "Expected False but got True"
|
||||
|
||||
# Test case 3: Test not_contains
|
||||
result4 = _assert_not_contains(value=bool_array2, expected="false")
|
||||
print(f"Test 4 - [True, True] not contains 'false': {result4}")
|
||||
assert result4 == True, "Expected True but got False"
|
||||
|
||||
result5 = _assert_not_contains(value=bool_array, expected="true")
|
||||
print(f"Test 5 - [True, False, True] not contains 'true': {result5}")
|
||||
assert result5 == False, "Expected False but got True"
|
||||
|
||||
# Test case 4: Test with different string representations
|
||||
result6 = _assert_contains(
|
||||
value=bool_array, expected="1"
|
||||
) # "1" should convert to True
|
||||
print(f"Test 6 - [True, False, True] contains '1': {result6}")
|
||||
assert result6 == True, "Expected True but got False"
|
||||
|
||||
result7 = _assert_contains(
|
||||
value=bool_array, expected="0"
|
||||
) # "0" should convert to False
|
||||
print(f"Test 7 - [True, False, True] contains '0': {result7}")
|
||||
assert result7 == True, "Expected True but got False"
|
||||
|
||||
print("\n✅ All boolean array comparison tests passed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_boolean_array_contains()
|
||||
@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script to verify boolean type inference in variable factory.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the api directory to the Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
|
||||
|
||||
try:
|
||||
from factories.variable_factory import build_segment, segment_to_variable
|
||||
from core.variables.segments import BooleanSegment, ArrayBooleanSegment
|
||||
from core.variables.variables import BooleanVariable, ArrayBooleanVariable
|
||||
from core.variables.types import SegmentType
|
||||
|
||||
def test_boolean_inference():
|
||||
print("Testing boolean type inference...")
|
||||
|
||||
# Test single boolean values
|
||||
true_segment = build_segment(True)
|
||||
false_segment = build_segment(False)
|
||||
|
||||
print(f"True value: {true_segment}")
|
||||
print(f"Type: {type(true_segment)}")
|
||||
print(f"Value type: {true_segment.value_type}")
|
||||
print(f"Is BooleanSegment: {isinstance(true_segment, BooleanSegment)}")
|
||||
|
||||
print(f"\nFalse value: {false_segment}")
|
||||
print(f"Type: {type(false_segment)}")
|
||||
print(f"Value type: {false_segment.value_type}")
|
||||
print(f"Is BooleanSegment: {isinstance(false_segment, BooleanSegment)}")
|
||||
|
||||
# Test array of booleans
|
||||
bool_array_segment = build_segment([True, False, True])
|
||||
print(f"\nBoolean array: {bool_array_segment}")
|
||||
print(f"Type: {type(bool_array_segment)}")
|
||||
print(f"Value type: {bool_array_segment.value_type}")
|
||||
print(
|
||||
f"Is ArrayBooleanSegment: {isinstance(bool_array_segment, ArrayBooleanSegment)}"
|
||||
)
|
||||
|
||||
# Test empty boolean array
|
||||
empty_bool_array = build_segment([])
|
||||
print(f"\nEmpty array: {empty_bool_array}")
|
||||
print(f"Type: {type(empty_bool_array)}")
|
||||
print(f"Value type: {empty_bool_array.value_type}")
|
||||
|
||||
# Test segment to variable conversion
|
||||
bool_var = segment_to_variable(
|
||||
segment=true_segment, selector=["test", "bool_var"], name="test_boolean"
|
||||
)
|
||||
print(f"\nBoolean variable: {bool_var}")
|
||||
print(f"Type: {type(bool_var)}")
|
||||
print(f"Is BooleanVariable: {isinstance(bool_var, BooleanVariable)}")
|
||||
|
||||
array_bool_var = segment_to_variable(
|
||||
segment=bool_array_segment,
|
||||
selector=["test", "array_bool_var"],
|
||||
name="test_array_boolean",
|
||||
)
|
||||
print(f"\nArray boolean variable: {array_bool_var}")
|
||||
print(f"Type: {type(array_bool_var)}")
|
||||
print(
|
||||
f"Is ArrayBooleanVariable: {isinstance(array_bool_var, ArrayBooleanVariable)}"
|
||||
)
|
||||
|
||||
# Test that bool comes before int (critical ordering)
|
||||
print(f"\nTesting bool vs int precedence:")
|
||||
print(f"True is instance of bool: {isinstance(True, bool)}")
|
||||
print(f"True is instance of int: {isinstance(True, int)}")
|
||||
print(f"False is instance of bool: {isinstance(False, bool)}")
|
||||
print(f"False is instance of int: {isinstance(False, int)}")
|
||||
|
||||
# Verify that boolean values are correctly inferred as boolean, not int
|
||||
assert true_segment.value_type == SegmentType.BOOLEAN, (
|
||||
"True should be inferred as BOOLEAN"
|
||||
)
|
||||
assert false_segment.value_type == SegmentType.BOOLEAN, (
|
||||
"False should be inferred as BOOLEAN"
|
||||
)
|
||||
assert bool_array_segment.value_type == SegmentType.ARRAY_BOOLEAN, (
|
||||
"Boolean array should be inferred as ARRAY_BOOLEAN"
|
||||
)
|
||||
|
||||
print("\n✅ All boolean inference tests passed!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_boolean_inference()
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Import error: {e}")
|
||||
print("Make sure you're running this from the correct directory")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify boolean support in VariableAssigner node
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the api directory to the Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))
|
||||
|
||||
from core.variables import SegmentType
|
||||
from core.workflow.nodes.variable_assigner.v2.helpers import (
|
||||
is_operation_supported,
|
||||
is_constant_input_supported,
|
||||
is_input_value_valid,
|
||||
)
|
||||
from core.workflow.nodes.variable_assigner.v2.enums import Operation
|
||||
from core.workflow.nodes.variable_assigner.v2.constants import EMPTY_VALUE_MAPPING
|
||||
|
||||
|
||||
def test_boolean_operation_support():
|
||||
"""Test that boolean types support the correct operations"""
|
||||
print("Testing boolean operation support...")
|
||||
|
||||
# Boolean should support SET, OVER_WRITE, and CLEAR
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SET
|
||||
)
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE
|
||||
)
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.CLEAR
|
||||
)
|
||||
|
||||
# Boolean should NOT support arithmetic operations
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.ADD
|
||||
)
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SUBTRACT
|
||||
)
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.MULTIPLY
|
||||
)
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.DIVIDE
|
||||
)
|
||||
|
||||
# Boolean should NOT support array operations
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.APPEND
|
||||
)
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.EXTEND
|
||||
)
|
||||
|
||||
print("✓ Boolean operation support tests passed")
|
||||
|
||||
|
||||
def test_array_boolean_operation_support():
|
||||
"""Test that array boolean types support the correct operations"""
|
||||
print("Testing array boolean operation support...")
|
||||
|
||||
# Array boolean should support APPEND, EXTEND, SET, OVER_WRITE, CLEAR
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND
|
||||
)
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.EXTEND
|
||||
)
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.OVER_WRITE
|
||||
)
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.CLEAR
|
||||
)
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.REMOVE_FIRST
|
||||
)
|
||||
assert is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.REMOVE_LAST
|
||||
)
|
||||
|
||||
# Array boolean should NOT support arithmetic operations
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.ADD
|
||||
)
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.SUBTRACT
|
||||
)
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.MULTIPLY
|
||||
)
|
||||
assert not is_operation_supported(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.DIVIDE
|
||||
)
|
||||
|
||||
print("✓ Array boolean operation support tests passed")
|
||||
|
||||
|
||||
def test_boolean_constant_input_support():
|
||||
"""Test that boolean types support constant input for correct operations"""
|
||||
print("Testing boolean constant input support...")
|
||||
|
||||
# Boolean should support constant input for SET and OVER_WRITE
|
||||
assert is_constant_input_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SET
|
||||
)
|
||||
assert is_constant_input_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE
|
||||
)
|
||||
|
||||
# Boolean should NOT support constant input for arithmetic operations
|
||||
assert not is_constant_input_supported(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.ADD
|
||||
)
|
||||
|
||||
print("✓ Boolean constant input support tests passed")
|
||||
|
||||
|
||||
def test_boolean_input_validation():
|
||||
"""Test that boolean input validation works correctly"""
|
||||
print("Testing boolean input validation...")
|
||||
|
||||
# Boolean values should be valid for boolean type
|
||||
assert is_input_value_valid(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=True
|
||||
)
|
||||
assert is_input_value_valid(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=False
|
||||
)
|
||||
assert is_input_value_valid(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE, value=True
|
||||
)
|
||||
|
||||
# Non-boolean values should be invalid for boolean type
|
||||
assert not is_input_value_valid(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value="true"
|
||||
)
|
||||
assert not is_input_value_valid(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=1
|
||||
)
|
||||
assert not is_input_value_valid(
|
||||
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=0
|
||||
)
|
||||
|
||||
print("✓ Boolean input validation tests passed")
|
||||
|
||||
|
||||
def test_array_boolean_input_validation():
|
||||
"""Test that array boolean input validation works correctly"""
|
||||
print("Testing array boolean input validation...")
|
||||
|
||||
# Boolean values should be valid for array boolean append
|
||||
assert is_input_value_valid(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND, value=True
|
||||
)
|
||||
assert is_input_value_valid(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND, value=False
|
||||
)
|
||||
|
||||
# Boolean arrays should be valid for extend/overwrite
|
||||
assert is_input_value_valid(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN,
|
||||
operation=Operation.EXTEND,
|
||||
value=[True, False, True],
|
||||
)
|
||||
assert is_input_value_valid(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN,
|
||||
operation=Operation.OVER_WRITE,
|
||||
value=[False, False],
|
||||
)
|
||||
|
||||
# Non-boolean values should be invalid
|
||||
assert not is_input_value_valid(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN,
|
||||
operation=Operation.APPEND,
|
||||
value="true",
|
||||
)
|
||||
assert not is_input_value_valid(
|
||||
variable_type=SegmentType.ARRAY_BOOLEAN,
|
||||
operation=Operation.EXTEND,
|
||||
value=[True, "false"],
|
||||
)
|
||||
|
||||
print("✓ Array boolean input validation tests passed")
|
||||
|
||||
|
||||
def test_empty_value_mapping():
|
||||
"""Test that empty value mapping includes boolean types"""
|
||||
print("Testing empty value mapping...")
|
||||
|
||||
# Check that boolean types have correct empty values
|
||||
assert SegmentType.BOOLEAN in EMPTY_VALUE_MAPPING
|
||||
assert EMPTY_VALUE_MAPPING[SegmentType.BOOLEAN] is False
|
||||
|
||||
assert SegmentType.ARRAY_BOOLEAN in EMPTY_VALUE_MAPPING
|
||||
assert EMPTY_VALUE_MAPPING[SegmentType.ARRAY_BOOLEAN] == []
|
||||
|
||||
print("✓ Empty value mapping tests passed")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("Running VariableAssigner boolean support tests...\n")
|
||||
|
||||
try:
|
||||
test_boolean_operation_support()
|
||||
test_array_boolean_operation_support()
|
||||
test_boolean_constant_input_support()
|
||||
test_boolean_input_validation()
|
||||
test_array_boolean_input_validation()
|
||||
test_empty_value_mapping()
|
||||
|
||||
print(
|
||||
"\n🎉 All tests passed! Boolean support has been successfully added to VariableAssigner."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Test failed: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const i18nPrefix = 'appDebug.generate'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const IdeaOutput: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFoldIdeaOutput, {
|
||||
toggle: toggleFoldIdeaOutput,
|
||||
}] = useBoolean(true)
|
||||
|
||||
return (
|
||||
<div className='mt-4 text-[0px]'>
|
||||
<div
|
||||
className='mb-1.5 flex cursor-pointer items-center text-sm font-medium leading-5 text-text-primary'
|
||||
onClick={toggleFoldIdeaOutput}
|
||||
>
|
||||
<div className='system-sm-semibold-uppercase mr-1 text-text-secondary'>{t(`${i18nPrefix}.idealOutput`)}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>({t(`${i18nPrefix}.optional`)})</div>
|
||||
<ArrowDownRoundFill className={cn('size text-text-quaternary', isFoldIdeaOutput && 'relative top-[1px] rotate-[-90deg]')} />
|
||||
</div>
|
||||
{!isFoldIdeaOutput && (
|
||||
<Textarea
|
||||
className="h-[80px]"
|
||||
placeholder={t(`${i18nPrefix}.idealOutputPlaceholder`)}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(IdeaOutput)
|
||||
@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { GeneratorType } from './types'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import InstructionEditor from './instruction-editor'
|
||||
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
value: string
|
||||
editorKey: string
|
||||
onChange: (text: string) => void
|
||||
generatorType: GeneratorType
|
||||
isShowCurrentBlock: boolean
|
||||
}
|
||||
|
||||
const InstructionEditorInWorkflow: FC<Props> = ({
|
||||
nodeId,
|
||||
value,
|
||||
editorKey,
|
||||
onChange,
|
||||
generatorType,
|
||||
isShowCurrentBlock,
|
||||
}) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const filterVar = useCallback((payload: Var, selector: ValueSelector) => {
|
||||
const { nodesWithInspectVars } = workflowStore.getState()
|
||||
const nodeId = selector?.[0]
|
||||
return !!nodesWithInspectVars.find(node => node.nodeId === nodeId) && payload.type !== VarType.file && payload.type !== VarType.arrayFile
|
||||
}, [workflowStore])
|
||||
const {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
} = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
})
|
||||
const getVarType = useWorkflowVariableType()
|
||||
|
||||
return (
|
||||
<InstructionEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
editorKey={editorKey}
|
||||
generatorType={generatorType}
|
||||
availableVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
getVarType={getVarType}
|
||||
isShowCurrentBlock={isShowCurrentBlock}
|
||||
isShowLastRunBlock
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(InstructionEditorInWorkflow)
|
||||
@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import type { GeneratorType } from './types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
type Props = {
|
||||
editorKey: string
|
||||
value: string
|
||||
onChange: (text: string) => void
|
||||
generatorType: GeneratorType
|
||||
availableVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
getVarType?: (params: {
|
||||
nodeId: string,
|
||||
valueSelector: ValueSelector,
|
||||
}) => Type
|
||||
isShowCurrentBlock: boolean
|
||||
isShowLastRunBlock: boolean
|
||||
}
|
||||
|
||||
const i18nPrefix = 'appDebug.generate'
|
||||
|
||||
const InstructionEditor: FC<Props> = ({
|
||||
editorKey,
|
||||
generatorType,
|
||||
value,
|
||||
onChange,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
getVarType = () => Type.string,
|
||||
isShowCurrentBlock,
|
||||
isShowLastRunBlock,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const isBasicMode = !!getVarType
|
||||
// const [controlPromptEditorRerenderKey] =
|
||||
const isCode = generatorType === 'code'
|
||||
const placeholder = (
|
||||
<div className='system-sm-regular text-text-placeholder'>
|
||||
<div className='leading-6'>{t(`${i18nPrefix}.instructionPlaceHolderTitle`)}</div>
|
||||
<div className='mt-2'>
|
||||
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine1`)}</div>
|
||||
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine2`)}</div>
|
||||
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine3`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const handleInsertVariable = () => {
|
||||
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId: editorKey } as any)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<PromptEditor
|
||||
wrapperClassName='border !border-components-input-bg-normal bg-components-input-bg-normal hover:!border-components-input-bg-hover rounded-[10px] px-4 pt-3'
|
||||
key={editorKey}
|
||||
instanceId={editorKey}
|
||||
placeholder={placeholder}
|
||||
placeholderClassName='px-4 pt-3'
|
||||
className={cn('min-h-[240px] pb-8')}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: availableVars,
|
||||
getVarType,
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
position: node.position,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
currentBlock={{
|
||||
show: isShowCurrentBlock,
|
||||
generatorType,
|
||||
}}
|
||||
errorMessageBlock={{
|
||||
show: isCode,
|
||||
}}
|
||||
lastRunBlock={{
|
||||
show: isShowLastRunBlock,
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable
|
||||
isSupportFileVar={false}
|
||||
/>
|
||||
<div className='system-xs-regular absolute bottom-0 left-3 flex h-8 items-center space-x-0.5 text-components-input-text-placeholder'>
|
||||
<span>{t('appDebug.generate.press')}</span>
|
||||
<span className='system-kbd flex h-4 w-3.5 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray text-text-placeholder'>/</span>
|
||||
<span>{t('appDebug.generate.to')}</span>
|
||||
<span onClick={handleInsertVariable} className='!ml-1 cursor-pointer hover:border-b hover:border-dotted hover:border-text-tertiary hover:text-text-tertiary'>{t('appDebug.generate.insertContext')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InstructionEditor)
|
||||
@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import PromptRes from './prompt-res'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
nodeId: string
|
||||
}
|
||||
|
||||
const PromptResInWorkflow: FC<Props> = ({
|
||||
value,
|
||||
nodeId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
} = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: _payload => true,
|
||||
})
|
||||
return (
|
||||
<PromptRes
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: availableVars || [],
|
||||
getVarType: () => Type.string,
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
position: node.position,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
>
|
||||
</PromptRes>
|
||||
)
|
||||
}
|
||||
export default React.memo(PromptResInWorkflow)
|
||||
@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import type { WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
workflowVariableBlock: WorkflowVariableBlockType
|
||||
}
|
||||
|
||||
const keyIdPrefix = 'prompt-res-editor'
|
||||
const PromptRes: FC<Props> = ({
|
||||
value,
|
||||
workflowVariableBlock,
|
||||
}) => {
|
||||
const [editorKey, setEditorKey] = React.useState<string>('keyIdPrefix-0')
|
||||
useEffect(() => {
|
||||
setEditorKey(`${keyIdPrefix}-${Date.now()}`)
|
||||
}, [value])
|
||||
return (
|
||||
<PromptEditor
|
||||
key={editorKey}
|
||||
value={value}
|
||||
editable={false}
|
||||
className='h-full bg-transparent pt-0'
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(PromptRes)
|
||||
@ -0,0 +1,51 @@
|
||||
import { RiCloseLine, RiInformation2Fill } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
type Props = {
|
||||
message: string
|
||||
className?: string
|
||||
}
|
||||
const PromptToast = ({
|
||||
message,
|
||||
className,
|
||||
}: Props) => {
|
||||
const [isHide, {
|
||||
setTrue: hide,
|
||||
}] = useBoolean(false)
|
||||
// const message = `
|
||||
// # h1
|
||||
// **strong text** ~~strikethrough~~
|
||||
|
||||
// * list1
|
||||
// * list2
|
||||
|
||||
// xxxx
|
||||
|
||||
// ## h2
|
||||
// \`\`\`python
|
||||
// print('Hello, World!')
|
||||
// \`\`\`
|
||||
// `
|
||||
if (isHide)
|
||||
return
|
||||
return (
|
||||
<div className={cn('relative flex items-center p-2 ', className)}>
|
||||
{/* Background Effect */}
|
||||
<div className="pointer-events-none absolute inset-0 rounded-lg bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)] opacity-40 shadow-md"></div>
|
||||
<div className='relative flex h-full w-full justify-between'>
|
||||
<div className="flex h-full w-0 grow gap-1">
|
||||
<RiInformation2Fill className="mt-[3px] size-4 shrink-0 text-text-accent" />
|
||||
<Markdown className="w-0 grow text-sm" content={message} />
|
||||
</div>
|
||||
|
||||
<div className='relative top-[-1px] shrink-0 cursor-pointer p-0.5' onClick={hide}>
|
||||
<RiCloseLine className='size-5 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptToast
|
||||
@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import Link from 'next/link'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ResPlaceholder: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
|
||||
<Generator className='size-8 text-text-quaternary' />
|
||||
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
|
||||
<div>{t('appDebug.generate.newNoDataLine1')}</div>
|
||||
<Link className='text-text-accent' href='//todo' target='_blank'>{t('appDebug.generate.newNoDataLine2')}</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ResPlaceholder)
|
||||
@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GeneratorType } from './types'
|
||||
import PromptToast from './prompt-toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import VersionSelector from './version-selector'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { RiClipboardLine } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor'
|
||||
import PromptRes from './prompt-res'
|
||||
import PromptResInWorkflow from './prompt-res-in-workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
isBasicMode?: boolean
|
||||
nodeId?: string
|
||||
current: GenRes
|
||||
currentVersionIndex: number
|
||||
setCurrentVersionIndex: (index: number) => void
|
||||
versions: GenRes[]
|
||||
onApply: () => void
|
||||
generatorType: GeneratorType
|
||||
}
|
||||
|
||||
const Result: FC<Props> = ({
|
||||
isBasicMode,
|
||||
nodeId,
|
||||
current,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
versions,
|
||||
onApply,
|
||||
generatorType,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isGeneratorPrompt = generatorType === GeneratorType.prompt
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='mb-3 flex shrink-0 items-center justify-between'>
|
||||
<div>
|
||||
<div className='shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
|
||||
<VersionSelector
|
||||
versionLen={versions.length}
|
||||
value={currentVersionIndex}
|
||||
onChange={setCurrentVersionIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button className='px-2' onClick={() => {
|
||||
copy(current.modified)
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
<RiClipboardLine className='h-4 w-4 text-text-secondary' />
|
||||
</Button>
|
||||
<Button variant='primary' onClick={onApply}>
|
||||
{t('appDebug.generate.apply')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
current?.message && (
|
||||
<PromptToast message={current.message} className='mt-4 shrink-0' />
|
||||
)
|
||||
}
|
||||
<div className={cn('mt-3 grow', isGeneratorPrompt && 'overflow-y-auto')}>
|
||||
{isGeneratorPrompt ? (
|
||||
isBasicMode ? (
|
||||
<PromptRes
|
||||
value={current?.modified}
|
||||
workflowVariableBlock={{
|
||||
show: false,
|
||||
}}
|
||||
/>
|
||||
) : (<PromptResInWorkflow
|
||||
value={current?.modified || ''}
|
||||
nodeId={nodeId!}
|
||||
/>)
|
||||
) : (
|
||||
<CodeEditor
|
||||
editorWrapperClassName='h-full'
|
||||
className='bg-transparent pt-0'
|
||||
value={current?.modified}
|
||||
readOnly
|
||||
hideTopMenu
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Result)
|
||||
@ -0,0 +1,4 @@
|
||||
export enum GeneratorType {
|
||||
prompt = 'prompt',
|
||||
code = 'code',
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { useSessionStorageState } from 'ahooks'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type Params = {
|
||||
storageKey: string
|
||||
}
|
||||
const keyPrefix = 'gen-data-'
|
||||
const useGenData = ({ storageKey }: Params) => {
|
||||
const [versions, setVersions] = useSessionStorageState<GenRes[]>(`${keyPrefix}${storageKey}-versions`, {
|
||||
defaultValue: [],
|
||||
})
|
||||
|
||||
const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState<number>(`${keyPrefix}${storageKey}-version-index`, {
|
||||
defaultValue: 0,
|
||||
})
|
||||
|
||||
const current = versions?.[currentVersionIndex || 0]
|
||||
|
||||
const addVersion = useCallback((version: GenRes) => {
|
||||
setCurrentVersionIndex(() => versions?.length || 0)
|
||||
setVersions((prev) => {
|
||||
return [...prev!, version]
|
||||
})
|
||||
}, [setVersions, setCurrentVersionIndex, versions?.length])
|
||||
|
||||
return {
|
||||
versions,
|
||||
addVersion,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
export default useGenData
|
||||
@ -0,0 +1,101 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||
|
||||
type Option = {
|
||||
label: string
|
||||
value: number
|
||||
}
|
||||
|
||||
type VersionSelectorProps = {
|
||||
versionLen: number;
|
||||
value: number;
|
||||
onChange: (index: number) => void;
|
||||
}
|
||||
|
||||
const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, onChange }) => {
|
||||
const [isOpen, {
|
||||
setFalse: handleOpenFalse,
|
||||
toggle: handleOpenToggle,
|
||||
set: handleOpenSet,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const moreThanOneVersion = versionLen > 1
|
||||
const handleOpen = useCallback((value: boolean) => {
|
||||
if (moreThanOneVersion)
|
||||
handleOpenSet(value)
|
||||
}, [moreThanOneVersion, handleOpenToggle])
|
||||
const handleToggle = useCallback(() => {
|
||||
if (moreThanOneVersion)
|
||||
handleOpenToggle()
|
||||
}, [moreThanOneVersion, handleOpenToggle])
|
||||
|
||||
const versions = Array.from({ length: versionLen }, (_, index) => ({
|
||||
label: `Version ${index + 1}${index === versionLen - 1 ? ' · Latest' : ''}`,
|
||||
value: index,
|
||||
}))
|
||||
|
||||
const isLatest = value === versionLen - 1
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement={'bottom-start'}
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -8,
|
||||
}}
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={handleToggle}
|
||||
asChild
|
||||
>
|
||||
|
||||
<div className={cn('system-xs-medium flex items-center text-text-secondary', moreThanOneVersion && 'cursor-pointer')}>
|
||||
<div>Version {value + 1}{isLatest && ' · Latest'}</div>
|
||||
{moreThanOneVersion && <RiArrowDownSLine className='size-3 ' />}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger >
|
||||
<PortalToFollowElemContent className={cn(
|
||||
'z-[99]',
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'w-[208px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
|
||||
)}
|
||||
>
|
||||
<div className='system-xs-medium-uppercase flex h-[22px] items-center px-3 pl-3 text-text-tertiary'>
|
||||
Versions
|
||||
</div>
|
||||
{
|
||||
versions.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
|
||||
)}
|
||||
title={option.label}
|
||||
onClick={() => {
|
||||
onChange(option.value)
|
||||
handleOpenFalse()
|
||||
}}
|
||||
>
|
||||
<div className='mr-1 grow truncate px-1 pl-1'>
|
||||
{option.label}
|
||||
</div>
|
||||
{
|
||||
value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem >
|
||||
)
|
||||
}
|
||||
|
||||
export default VersionSelector
|
||||
@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z" fill="black"/>
|
||||
<path d="M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z" fill="black"/>
|
||||
<path d="M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z" fill="black"/>
|
||||
<path d="M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z" fill="black"/>
|
||||
<path d="M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z" fill="black"/>
|
||||
<path d="M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z" fill="black"/>
|
||||
<path d="M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,53 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "CodeAssistant"
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './CodeAssistant.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'CodeAssistant'
|
||||
|
||||
export default Icon
|
||||
@ -0,0 +1,55 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "MagicEdit"
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './MagicEdit.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'MagicEdit'
|
||||
|
||||
export default Icon
|
||||
@ -0,0 +1,44 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { CurrentBlockNode, DELETE_CURRENT_BLOCK_COMMAND } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CodeAssistant, MagicEdit } from '../../../icons/src/vender/line/general'
|
||||
|
||||
type CurrentBlockComponentProps = {
|
||||
nodeKey: string
|
||||
generatorType: GeneratorType
|
||||
}
|
||||
|
||||
const CurrentBlockComponent: FC<CurrentBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
generatorType,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CURRENT_BLOCK_COMMAND)
|
||||
|
||||
const Icon = generatorType === GeneratorType.prompt ? MagicEdit : CodeAssistant
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-violet-violet-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Icon className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>{generatorType === GeneratorType.prompt ? 'current_prompt' : 'current_code'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CurrentBlockComponent
|
||||
@ -0,0 +1,62 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { CURRENT_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { CurrentBlockType } from '../../types'
|
||||
import {
|
||||
$createCurrentBlockNode,
|
||||
CurrentBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(CURRENT_PLACEHOLDER_TEXT)
|
||||
|
||||
const CurrentBlockReplacementBlock = ({
|
||||
generatorType,
|
||||
onInsert,
|
||||
}: CurrentBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('CurrentBlockNodePlugin: CurrentBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createCurrentBlockNode = useCallback((): CurrentBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createCurrentBlockNode(generatorType))
|
||||
}, [onInsert, generatorType])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + CURRENT_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(CurrentBlockReplacementBlock)
|
||||
@ -0,0 +1,66 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { CurrentBlockType } from '../../types'
|
||||
import {
|
||||
$createCurrentBlockNode,
|
||||
CurrentBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_CURRENT_BLOCK_COMMAND = createCommand('INSERT_CURRENT_BLOCK_COMMAND')
|
||||
export const DELETE_CURRENT_BLOCK_COMMAND = createCommand('DELETE_CURRENT_BLOCK_COMMAND')
|
||||
|
||||
const CurrentBlock = memo(({
|
||||
generatorType,
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: CurrentBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('CURRENTBlockPlugin: CURRENTBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_CURRENT_BLOCK_COMMAND,
|
||||
() => {
|
||||
const currentBlockNode = $createCurrentBlockNode(generatorType)
|
||||
|
||||
$insertNodes([currentBlockNode])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_CURRENT_BLOCK_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, generatorType, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
CurrentBlock.displayName = 'CurrentBlock'
|
||||
|
||||
export { CurrentBlock }
|
||||
export { CurrentBlockNode } from './node'
|
||||
export { default as CurrentBlockReplacementBlock } from './current-block-replacement-block'
|
||||
@ -0,0 +1,78 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import CurrentBlockComponent from './component'
|
||||
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode & { generatorType: GeneratorType; }
|
||||
|
||||
export class CurrentBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
__generatorType: GeneratorType
|
||||
static getType(): string {
|
||||
return 'current-block'
|
||||
}
|
||||
|
||||
static clone(node: CurrentBlockNode): CurrentBlockNode {
|
||||
return new CurrentBlockNode(node.__generatorType, node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(generatorType: GeneratorType, key?: NodeKey) {
|
||||
super(key)
|
||||
|
||||
this.__generatorType = generatorType
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<CurrentBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
generatorType={this.getGeneratorType()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
getGeneratorType(): GeneratorType {
|
||||
const self = this.getLatest()
|
||||
return self.__generatorType
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): CurrentBlockNode {
|
||||
const node = $createCurrentBlockNode(serializedNode.generatorType)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'current-block',
|
||||
version: 1,
|
||||
generatorType: this.getGeneratorType(),
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#current#}}'
|
||||
}
|
||||
}
|
||||
export function $createCurrentBlockNode(type: GeneratorType): CurrentBlockNode {
|
||||
return new CurrentBlockNode(type)
|
||||
}
|
||||
|
||||
export function $isCurrentBlockNode(
|
||||
node: CurrentBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof CurrentBlockNode
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Variable02 } from '../../../icons/src/vender/solid/development'
|
||||
|
||||
type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
const ErrorMessageBlockComponent: FC<Props> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_ERROR_MESSAGE_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-orange-dark-orange-dark-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>error_message</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorMessageBlockComponent
|
||||
@ -0,0 +1,61 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { ERROR_MESSAGE_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { ErrorMessageBlockType } from '../../types'
|
||||
import {
|
||||
$createErrorMessageBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(ERROR_MESSAGE_PLACEHOLDER_TEXT)
|
||||
|
||||
const ErrorMessageBlockReplacementBlock = ({
|
||||
onInsert,
|
||||
}: ErrorMessageBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('ErrorMessageBlockNodePlugin: ErrorMessageBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createErrorMessageBlockNode = useCallback((): ErrorMessageBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createErrorMessageBlockNode())
|
||||
}, [onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + ERROR_MESSAGE_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(ErrorMessageBlockReplacementBlock)
|
||||
@ -0,0 +1,65 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { ErrorMessageBlockType } from '../../types'
|
||||
import {
|
||||
$createErrorMessageBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_ERROR_MESSAGE_BLOCK_COMMAND = createCommand('INSERT_ERROR_MESSAGE_BLOCK_COMMAND')
|
||||
export const DELETE_ERROR_MESSAGE_COMMAND = createCommand('DELETE_ERROR_MESSAGE_COMMAND')
|
||||
|
||||
const ErrorMessageBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: ErrorMessageBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('ERROR_MESSAGEBlockPlugin: ERROR_MESSAGEBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
|
||||
() => {
|
||||
const Node = $createErrorMessageBlockNode()
|
||||
|
||||
$insertNodes([Node])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_ERROR_MESSAGE_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
ErrorMessageBlock.displayName = 'ErrorMessageBlock'
|
||||
|
||||
export { ErrorMessageBlock }
|
||||
export { ErrorMessageBlockNode } from './node'
|
||||
export { default as ErrorMessageBlockReplacementBlock } from './error-message-block-replacement-block'
|
||||
@ -0,0 +1,67 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import ErrorMessageBlockComponent from './component'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode
|
||||
|
||||
export class ErrorMessageBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
static getType(): string {
|
||||
return 'error-message-block'
|
||||
}
|
||||
|
||||
static clone(node: ErrorMessageBlockNode): ErrorMessageBlockNode {
|
||||
return new ErrorMessageBlockNode(node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key)
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<ErrorMessageBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(): ErrorMessageBlockNode {
|
||||
const node = $createErrorMessageBlockNode()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'error-message-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#error_message#}}'
|
||||
}
|
||||
}
|
||||
export function $createErrorMessageBlockNode(): ErrorMessageBlockNode {
|
||||
return new ErrorMessageBlockNode()
|
||||
}
|
||||
|
||||
export function $isErrorMessageBlockNode(
|
||||
node: ErrorMessageBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof ErrorMessageBlockNode
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { DELETE_LAST_RUN_COMMAND, LastRunBlockNode } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Variable02 } from '../../../icons/src/vender/solid/development'
|
||||
|
||||
type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
const LastRunBlockComponent: FC<Props> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-text-accent hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>last_run</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LastRunBlockComponent
|
||||
@ -0,0 +1,65 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { LastRunBlockType } from '../../types'
|
||||
import {
|
||||
$createLastRunBlockNode,
|
||||
LastRunBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_LAST_RUN_BLOCK_COMMAND = createCommand('INSERT_LAST_RUN_BLOCK_COMMAND')
|
||||
export const DELETE_LAST_RUN_COMMAND = createCommand('DELETE_LAST_RUN_COMMAND')
|
||||
|
||||
const LastRunBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: LastRunBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('Last_RunBlockPlugin: Last_RunBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_LAST_RUN_BLOCK_COMMAND,
|
||||
() => {
|
||||
const Node = $createLastRunBlockNode()
|
||||
|
||||
$insertNodes([Node])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_LAST_RUN_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
LastRunBlock.displayName = 'LastRunBlock'
|
||||
|
||||
export { LastRunBlock }
|
||||
export { LastRunBlockNode } from './node'
|
||||
export { default as LastRunReplacementBlock } from './last-run-block-replacement-block'
|
||||
@ -0,0 +1,61 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { LAST_RUN_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { LastRunBlockType } from '../../types'
|
||||
import {
|
||||
$createLastRunBlockNode,
|
||||
LastRunBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(LAST_RUN_PLACEHOLDER_TEXT)
|
||||
|
||||
const LastRunReplacementBlock = ({
|
||||
onInsert,
|
||||
}: LastRunBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('LastRunMessageBlockNodePlugin: LastRunMessageBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createLastRunBlockNode = useCallback((): LastRunBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createLastRunBlockNode())
|
||||
}, [onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + LAST_RUN_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(LastRunReplacementBlock)
|
||||
@ -0,0 +1,67 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import LastRunBlockComponent from './component'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode
|
||||
|
||||
export class LastRunBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
static getType(): string {
|
||||
return 'last-run-block'
|
||||
}
|
||||
|
||||
static clone(node: LastRunBlockNode): LastRunBlockNode {
|
||||
return new LastRunBlockNode(node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key)
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<LastRunBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(): LastRunBlockNode {
|
||||
const node = $createLastRunBlockNode()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'last-run-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#last_run#}}'
|
||||
}
|
||||
}
|
||||
export function $createLastRunBlockNode(): LastRunBlockNode {
|
||||
return new LastRunBlockNode()
|
||||
}
|
||||
|
||||
export function $isLastRunBlockNode(
|
||||
node: LastRunBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof LastRunBlockNode
|
||||
}
|
||||
Loading…
Reference in New Issue