diff --git a/backend/funix/__init__.py b/backend/funix/__init__.py
index 3426372..00ee22b 100644
--- a/backend/funix/__init__.py
+++ b/backend/funix/__init__.py
@@ -58,6 +58,7 @@
new_funix_type = hint.new_funix_type
set_app_secret = secret.set_app_secret
generate_redirect_link = widget.generate_redirect_link
+generate_redirect_button = widget.generate_redirect_button
mime_encode = mime_encoded
# ---- Util ----
diff --git a/backend/funix/__main__.py b/backend/funix/__main__.py
index 0a914a2..13bd47b 100644
--- a/backend/funix/__main__.py
+++ b/backend/funix/__main__.py
@@ -81,7 +81,7 @@ def cli_main():
This function is called when you run `python -m funix` or `funix` from the command line.
"""
- plac.call(main, version="Funix 0.6.3")
+ plac.call(main, version="Funix 0.6.4")
if __name__ == "__main__":
diff --git a/backend/funix/app/__init__.py b/backend/funix/app/__init__.py
index 5926d04..30694f7 100644
--- a/backend/funix/app/__init__.py
+++ b/backend/funix/app/__init__.py
@@ -12,11 +12,12 @@
from urllib.parse import urlparse
from uuid import uuid4
-from flask import Flask, Response, abort, request
+from flask import Flask, Response, abort, request, send_file
from flask_sock import Sock
from sqlalchemy import create_engine, text
from sqlalchemy.pool import SingletonThreadPool
+from funix.config import banned_function_name_and_path
from funix.config.switch import GlobalSwitchOption
from funix.decorator.file import get_file_info
from funix.decorator.lists import get_uuid_with_name
@@ -367,3 +368,19 @@ def enable_funix_host_checker(regex: str):
def funix_host_check():
if len(re.findall(regex_string, request.host)) == 0:
abort(403)
+
+
+def serve_file(route_or_path: str, path: str = None):
+ if route_or_path and path:
+ if route_or_path in banned_function_name_and_path:
+ raise ValueError(f"{route_or_path} is not allowed, banned names: {banned_function_name_and_path}")
+ @app.route(route_or_path)
+ def __serve_file():
+ return send_file(path)
+ else:
+ safe_route = os.path.basename(route_or_path)
+ if safe_route in banned_function_name_and_path:
+ raise ValueError(f"{safe_route} is not allowed, banned names: {banned_function_name_and_path}")
+ @app.route(safe_route)
+ def __serve_file():
+ return send_file(route_or_path)
diff --git a/backend/funix/config/__init__.py b/backend/funix/config/__init__.py
index 0da0cbd..98028a0 100644
--- a/backend/funix/config/__init__.py
+++ b/backend/funix/config/__init__.py
@@ -27,7 +27,7 @@
A list, contains the upload widget names.
"""
-banned_function_name_and_path = ["list", "file", "static", "config", "param", "call"]
+banned_function_name_and_path = ["list", "file", "static", "config", "param", "call", "update"]
"""
The banned function name and path.
diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py
index 4de862e..3497c00 100644
--- a/backend/funix/decorator/__init__.py
+++ b/backend/funix/decorator/__init__.py
@@ -51,7 +51,7 @@
set_function_secret,
)
from funix.decorator.theme import get_parsed_theme_fot_funix
-from funix.decorator.widget import parse_argument_config, widget_parse
+from funix.decorator.widget import get_uuid_by_callable, parse_argument_config, widget_parse
from funix.hint import (
AcceptableWidgetsList,
ArgumentConfigType,
@@ -64,6 +64,7 @@
InputLayout,
LabelsType,
Markdown,
+ NextTo,
OutputLayout,
PreFillType,
ReactiveType,
@@ -246,6 +247,7 @@ def funix(
order: Optional[int] = None,
just_run: bool = False,
class_line: Optional[int] = None,
+ next_to: Optional[Callable] = None,
):
"""
Decorator for functions to convert them to web apps
@@ -301,6 +303,7 @@ def funix(
order(int): the order of the function
just_run(bool): just run the function, no input panel for the function
class_line(int): the line number of the class
+ next_to(Callable): the function to be called next
Returns:
function: the decorated function
@@ -489,6 +492,13 @@ def decorator(function: Callable[P, R]) -> Callable[P, R]:
need_websocket = True
setattr(function_signature, "_return_annotation", Markdown)
+ if next_to:
+ print(f"WARNING: the {function_name} function has next to, the output type will be ignored.")
+ setattr(function_signature, "_return_annotation", NextTo)
+
+ if need_websocket and next_to:
+ raise ValueError("You cannot use next to with websocket mode.")
+
has_reactive_params = False
reactive_config: dict[str, tuple[Callable, dict[str, str]]] = {}
@@ -751,6 +761,7 @@ def decorated_function_param_getter():
param_widget_whitelist_callable,
param_widget_example_callable,
class_method_qualname if is_class_method else function.__qualname__,
+ next_to
)
decorated_function_param_getter_name = f"{function_name}_param_getter"
@@ -817,6 +828,7 @@ def wrapper(ws=None):
secret_key,
matplotlib_format,
ws,
+ next_to,
)
if result is not None:
return result
diff --git a/backend/funix/decorator/call.py b/backend/funix/decorator/call.py
index f4dfdea..2386fd8 100644
--- a/backend/funix/decorator/call.py
+++ b/backend/funix/decorator/call.py
@@ -1,7 +1,7 @@
import sys
from copy import deepcopy
from functools import wraps
-from inspect import isgeneratorfunction
+from inspect import isgeneratorfunction, signature
from json import dumps, loads
from traceback import format_exc
from typing import Any, Callable
@@ -9,6 +9,8 @@
from uuid import uuid4
from flask import request, session
+from funix.decorator.encoder import FunixJsonEncoder
+from git import exc
from requests import post
from funix.app.websocket import StdoutToWebsocket
@@ -19,6 +21,7 @@
from funix.decorator.pre_fill import get_pre_fill_metadata
from funix.decorator.response import response_item_to_class
from funix.decorator.secret import get_secret_by_id
+from funix.decorator.widget import get_path_by_callable
from funix.hint import PreFillEmpty, WrapperException
from funix.session import set_global_variable
@@ -104,6 +107,7 @@ def funix_call(
secret_key: bool,
matplotlib_format: str,
ws=None,
+ next_to: Callable | None = None,
):
for limiter in global_rate_limiters + limiters:
limit_result = limiter.rate_limit()
@@ -189,6 +193,24 @@ def wrapped_function(**wrapped_function_kwargs):
function_call_result = call_function_get_frame(
function, **wrapped_function_kwargs
)
+ if next_to:
+ try:
+ if isinstance(function_call_result[1], tuple):
+ arguments = signature(next_to).bind(*function_call_result[1])
+ arguments.apply_defaults()
+ arguments = arguments.arguments
+ elif isinstance(function_call_result[1], dict):
+ arguments = signature(next_to).bind(**function_call_result[1])
+ arguments.apply_defaults()
+ arguments = arguments.arguments
+ else:
+ print("Warning: You must return a tuple or a dict.")
+ arguments = function_call_result[1]
+ except:
+ print("Warning: Cannot bind arguments, it means the type of the arguments is not correct.")
+ arguments = function_call_result[1]
+
+ return dumps({"args": arguments, "path": get_path_by_callable(next_to) }, cls=FunixJsonEncoder)
return pre_anal_result(function_call_result[0], function_call_result[1])
except WrapperException as e:
return {
diff --git a/backend/funix/decorator/encoder.py b/backend/funix/decorator/encoder.py
new file mode 100644
index 0000000..c988ff8
--- /dev/null
+++ b/backend/funix/decorator/encoder.py
@@ -0,0 +1,21 @@
+from json import JSONEncoder
+from datetime import datetime
+
+
+class FunixJsonEncoder(JSONEncoder):
+ def default(self, o):
+ if isinstance(o, datetime):
+ return o.isoformat()
+ if hasattr(o, "__class__"):
+ clz = o.__class__
+ if hasattr(clz, "__base__"):
+ base = clz.__base__
+ # Check pydantic
+ try:
+ from pydantic import BaseModel
+
+ if issubclass(base, BaseModel):
+ return o.model_dump(mode="json")
+ except ImportError:
+ return super().default(o)
+ return super().default(o)
diff --git a/backend/funix/decorator/param.py b/backend/funix/decorator/param.py
index cb992e0..60d63c7 100644
--- a/backend/funix/decorator/param.py
+++ b/backend/funix/decorator/param.py
@@ -4,7 +4,7 @@
from json import dumps, JSONEncoder
from types import MappingProxyType
from typing import Any, Callable
-from datetime import datetime
+
from flask import Response
@@ -16,12 +16,14 @@
get_type_dict,
get_type_widget_prop,
)
+from funix.decorator.widget import get_uuid_by_callable
from funix.session import get_global_variable
from funix.decorator.layout import (
pydantic_layout_dict,
pydantic_name_dict,
pydantic_widget_dict,
)
+from funix.decorator.encoder import FunixJsonEncoder
dataframe_parse_metadata: dict[str, dict[str, list[str]]] = {}
"""
@@ -34,25 +36,6 @@
"""
-class FunixJsonEncoder(JSONEncoder):
- def default(self, o):
- if isinstance(o, datetime):
- return o.isoformat()
- if hasattr(o, "__class__"):
- clz = o.__class__
- if hasattr(clz, "__base__"):
- base = clz.__base__
- # Check pydantic
- try:
- from pydantic import BaseModel
-
- if issubclass(base, BaseModel):
- return o.model_dump(mode="json")
- except ImportError:
- return super().default(o)
- return super().default(o)
-
-
def apply_decorated_params(
item_props: dict, decorated_params: dict, param_name: str, function_name: str = None
) -> None:
@@ -701,6 +684,7 @@ def get_param_for_funix(
param_widget_whitelist_callable: dict,
param_widget_example_callable: dict,
qualname: str,
+ next_to: Callable | None,
):
new_decorated_function = deepcopy(decorated_function)
if pre_fill is not None:
@@ -876,6 +860,8 @@ def get_param_for_funix(
des = get_global_variable(session_description)
new_decorated_function["description"] = des
new_decorated_function["schema"]["description"] = des
+ if next_to:
+ new_decorated_function["nextToUuid"] = get_uuid_by_callable(next_to)
return Response(
dumps(new_decorated_function, cls=FunixJsonEncoder), mimetype="application/json"
)
diff --git a/backend/funix/decorator/widget.py b/backend/funix/decorator/widget.py
index 8891a0f..aca8462 100644
--- a/backend/funix/decorator/widget.py
+++ b/backend/funix/decorator/widget.py
@@ -7,6 +7,7 @@
from typing import Any, Callable, Union
from funix.app import app
+from funix.decorator.encoder import FunixJsonEncoder
from funix.decorator.lists import (
get_class_method_funix,
get_function_detail_by_uuid,
@@ -345,7 +346,7 @@ def generate_redirect_link_core(
arguments = signature(_function).bind(*args, **kwargs)
arguments.apply_defaults()
dict_args = arguments.arguments
- json_plain = json.dumps(dict_args)
+ json_plain = json.dumps(dict_args, cls=FunixJsonEncoder)
web_safe_args = base64.urlsafe_b64encode(json_plain.encode()).decode()
return f"/{result['path']}?args={web_safe_args}", result
@@ -368,3 +369,68 @@ def generate_redirect_link(
"""
url, result = generate_redirect_link_core(function, *args, **kwargs)
return f"{result['name']}"
+
+def generate_redirect_button(
+ function: Callable,
+ auto_direct: bool = True,
+ use_raw: bool = False,
+ raw_args: dict = {},
+ button_text: str = "Redirect",
+ *args,
+ **kwargs,
+) -> dict:
+ """
+ Generate a redirect button.
+ """
+ function_qualname = function.__qualname__
+ _function = function
+ if "." in function_qualname:
+ class_function = get_class_method_funix(
+ app_name=app.name, method_qualname=function_qualname
+ )
+ if class_function:
+ _function = class_function
+ else:
+ raise ValueError(f"Function {function_qualname} not found.")
+ jump_uuid = get_function_uuid_with_id(app_name=app.name, _id=id(_function))
+ if jump_uuid == "":
+ raise ValueError(f"Function {function_qualname} not found.")
+ result = get_function_detail_by_uuid(app_name=app.name, uuid=jump_uuid)
+ if use_raw:
+ json_plain = json.dumps(raw_args, cls=FunixJsonEncoder)
+ else:
+ arguments = signature(_function).bind(*args, **kwargs)
+ arguments.apply_defaults()
+ dict_args = arguments.arguments
+ json_plain = json.dumps(dict_args, cls=FunixJsonEncoder)
+ return {"path": result["path"], "args": json_plain, "text": button_text, "auto_direct": auto_direct}
+
+def get_function_by_callable(function: Callable) -> Callable | None:
+ function_qualname = function.__qualname__
+ _function = function
+ if "." in function_qualname:
+ class_function = get_class_method_funix(
+ app_name=app.name, method_qualname=function_qualname
+ )
+ if class_function:
+ _function = class_function
+ else:
+ return None
+ return _function
+
+
+def get_uuid_by_callable(function: Callable) -> str | None:
+ _function = get_function_by_callable(function)
+ if _function is None:
+ return None
+ jump_uuid = get_function_uuid_with_id(app_name=app.name, _id=id(_function))
+ if jump_uuid == "":
+ return None
+ return jump_uuid
+
+
+def get_path_by_callable(function: Callable) -> str | None:
+ _function = get_function_by_callable(function)
+ if _function is None:
+ return None
+ return get_function_detail_by_uuid(app_name=app.name, uuid=get_function_uuid_with_id(app_name=app.name, _id=id(_function)))["path"]
diff --git a/backend/funix/hint/__init__.py b/backend/funix/hint/__init__.py
index c84badd..e3f9091 100644
--- a/backend/funix/hint/__init__.py
+++ b/backend/funix/hint/__init__.py
@@ -320,6 +320,13 @@ class CodeConfig(TypedDict):
Support Markdown like "**bold**" and "*italic*"
"""
+_NextTo = NewType("NextTo", type(Optional[dict]))
+NextTo: TypeAlias = _NextTo # type: ignore
+"""
+NextTo type.
+For output.
+"""
+
_HTML = NewType("HTML", type(Optional[str]))
HTML: TypeAlias = _HTML # type: ignore
"""
@@ -390,6 +397,13 @@ class CodeConfig(TypedDict):
For output.
"""
+_RedirectButton = NewType("RedirectButton", type(Optional[dict]))
+RedirectButton: TypeAlias = _RedirectButton # type: ignore
+"""
+RedirectButton type.
+For output.
+"""
+
# ---- Built-in Input Widgets ----
IntInputBox: TypeAlias = builtin.IntInputBox
IntSlider = builtin.IntSlider
@@ -651,8 +665,8 @@ class LimitSource(Enum):
# --- Chem ---
-@new_funix_type(widget={"name": "ketcher"})
-class Ketcher(dict):
+
+class KetcherBase(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.smiles = self.get("smiles")
@@ -662,3 +676,27 @@ def __init__(self, *args, **kwargs):
self.smarts = self.get("smarts")
self.ket = self.get("ket")
+
+@new_funix_type(widget={"name": "ketcher"})
+class InternalKetcher(KetcherBase):
+ pass
+
+
+@new_funix_type(widget={"name": "ketcher_popup"})
+class InternalKetcherPopup(KetcherBase):
+ pass
+
+
+class KetcherMeta(type):
+ def __getitem__(self, pop_up: Literal["popup", Any] = None) -> type:
+ if pop_up == "popup":
+ return InternalKetcherPopup
+ else:
+ return InternalKetcher
+
+ def __new__(cls, *args, **kwargs) -> type:
+ return InternalKetcher
+
+
+class Ketcher(metaclass=KetcherMeta):
+ pass
diff --git a/backend/funix/requirements.txt b/backend/funix/requirements.txt
index b16b200..22e4aba 100644
--- a/backend/funix/requirements.txt
+++ b/backend/funix/requirements.txt
@@ -10,3 +10,4 @@ matplotlib>=3.4.3
pandas>=2.0.3
tornado>=6.4.2
pydantic>=2.10.6
+GitPython>=3.1.31
diff --git a/examples/marvin-test.py b/examples/marvin-test.py
new file mode 100644
index 0000000..c74ddb1
--- /dev/null
+++ b/examples/marvin-test.py
@@ -0,0 +1,5 @@
+import os
+from funix.hint import ChemStr, KetcherPopup
+
+def ketcher_demo(ketcher: KetcherPopup) -> ChemStr:
+ return ketcher.inchi
diff --git a/examples/pydantic_test.py b/examples/pydantic_test.py
index 89655a7..961f423 100644
--- a/examples/pydantic_test.py
+++ b/examples/pydantic_test.py
@@ -1,7 +1,8 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
-from funix import funix, pydantic_ui
+from funix import funix, pydantic_ui, generate_redirect_link, generate_redirect_button
+from funix.hint import RedirectButton
@pydantic_ui(
@@ -58,3 +59,12 @@ def where_is_the_datetime(
)
) -> str:
return f"User: {t.user}, Reason: {t.reason}, Time: {datetime.now()}"
+
+
+def list_test(a: SingleModel) -> str:
+ return f"User: {a.user}, Reason: {a.reason}, Time: {datetime.now()}"
+
+@funix(next_to=list_test)
+def test_generate_redirect_button_2():
+ return {"a": {"links": [{"url": "https://www.google.com", "display": True}]}}
+
diff --git a/frontend/config-overrides.js b/frontend/config-overrides.js
index 9b701fe..350d9eb 100644
--- a/frontend/config-overrides.js
+++ b/frontend/config-overrides.js
@@ -44,7 +44,7 @@ module.exports = function override(config) {
config.plugins.push(
new SaveRemoteFilePlugin([
{
- url: "https://code.jquery.com/jquery-3.7.1.min.js",
+ url: "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js",
filepath: "static/js/jquery-3.7.1.min.js",
hash: false,
},
diff --git a/frontend/src/components/Common/MarkdownDiv.tsx b/frontend/src/components/Common/MarkdownDiv.tsx
index bd64d33..86809e0 100644
--- a/frontend/src/components/Common/MarkdownDiv.tsx
+++ b/frontend/src/components/Common/MarkdownDiv.tsx
@@ -200,13 +200,14 @@ export default function MarkdownDiv(props: MarkdownDivProps) {
useEffect(() => {
const fetchContent = async () => {
- const content = await MarkdownAsync({
+ const _content = await MarkdownAsync({
remarkPlugins: [remarkGfm, remarkMath],
rehypePlugins: [
rehypeRaw,
[rehypeKatex, { output: "mathml" }],
[rehypeMermaid],
],
+ urlTransform: (value: string) => value,
components: {
p: (props) =>
isRenderInline ? (
@@ -364,7 +365,7 @@ export default function MarkdownDiv(props: MarkdownDivProps) {
},
children: props.markdown,
});
- setContent(content);
+ setContent(_content);
};
fetchContent();
}, [props.markdown]);
diff --git a/frontend/src/components/FunixFunction/ChemEditor.tsx b/frontend/src/components/FunixFunction/ChemEditor.tsx
index aee64a1..fe4b61a 100644
--- a/frontend/src/components/FunixFunction/ChemEditor.tsx
+++ b/frontend/src/components/FunixFunction/ChemEditor.tsx
@@ -1,100 +1,124 @@
import { WidgetProps } from "@rjsf/utils";
-import { Editor } from "ketcher-react";
-import { StandaloneStructServiceProvider } from "ketcher-standalone";
-import "miew/dist/miew.min.css";
-import "ketcher-react/dist/index.css";
-import { Ketcher } from "ketcher-core";
-import React, { useCallback, useMemo, useState } from "react";
-import { Box } from "@mui/material";
+import React, { useState } from "react";
+import {
+ Box,
+ Button,
+ Card,
+ CardMedia,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Typography,
+} from "@mui/material";
+import { KetcherEditor } from "./components";
+import { ChemEditorValue } from "./hooks";
+import renderSvg from "../../shared/indigo-render";
interface ChemEditorProps {
widget: WidgetProps;
+ popup?: boolean;
}
-type ChemEditorValue = {
- smiles: string;
- inchi: string | null;
- inchiAuxInfo: string | null;
- inchiKey: string | null;
- smarts: string;
- ket: string;
+const SimpleRenderBox = (props: { data: string | null }) => {
+ return (
+
+ {props.data ? (
+
+ ) : (
+ No data
+ )}
+
+ );
};
const ChemEditor: React.FC = React.memo((props) => {
- const [_, setValue] = useState(
- props.widget.value ?? props.widget.formData ?? null,
+ const [popUpKet, setPopUpKet] = useState(null);
+ const [popUpOpen, setPopUpOpen] = useState(false);
+ const [popUpKetTemp, setPopUpKetTemp] = useState(
+ null,
);
- const syncSetValue = useCallback(
- (newValue: ChemEditorValue) => {
- setValue(newValue);
- props.widget.onChange(newValue);
- },
- [props.widget],
- );
+ const initialValue = props.widget.value ?? props.widget.formData ?? null;
- const structServiceProvider = useMemo(
- () => new StandaloneStructServiceProvider(),
- [],
- );
+ const handleChange = (newValue: ChemEditorValue) => {
+ props.widget.onChange(newValue);
+ setPopUpKet(newValue);
+ };
- return (
-
- {}}
- onInit={(ketcher: Ketcher) => {
- if (window.ketcher) {
- ketcher.editor.clear();
- ketcher.editor.clearHistory();
- }
- window.ketcher = ketcher;
- if (props.widget.value || props.widget.formData) {
- const value = props.widget.value ?? props.widget.formData;
- if (value) {
- ketcher.setMolecule(value.ket);
- }
- }
- ketcher.editor.subscribe("change", async () => {
- const ket = await ketcher.getKet();
- if (ketcher.containsReaction()) {
- const smiles = await ketcher.getSmiles();
- const smarts = await ketcher.getSmarts();
- syncSetValue({
- ket,
- smiles,
- inchi: null,
- inchiAuxInfo: null,
- inchiKey: null,
- smarts,
- });
- } else {
- const smiles = await ketcher.getSmiles();
- const inchi = await ketcher.getInchi();
- const inchiAuxInfo = await ketcher.getInchi(true);
- const inchiKey = await ketcher.getInChIKey();
- const smarts = await ketcher.getSmarts();
- syncSetValue({
- ket,
- smiles,
- inchi,
- inchiAuxInfo,
- inchiKey,
- smarts,
- });
- }
- });
+ if (props.popup) {
+ return (
+
-
- );
+ >
+
+
+
+
+ );
+ }
+
+ return ;
});
export default ChemEditor;
diff --git a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx
index afd55d6..0375e42 100644
--- a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx
+++ b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx
@@ -208,6 +208,11 @@ const ObjectFieldExtendedTemplate = (props: ObjectFieldTemplateProps) => {
type: "config",
element: ,
};
+ } else if (elementContent.props.schema.widget === "ketcher_popup") {
+ return {
+ type: "config",
+ element: ,
+ };
} else {
return {
type: "config",
diff --git a/frontend/src/components/FunixFunction/OutputComponents/OutputChem.tsx b/frontend/src/components/FunixFunction/OutputComponents/OutputChem.tsx
index 05a78bc..f4b14fc 100644
--- a/frontend/src/components/FunixFunction/OutputComponents/OutputChem.tsx
+++ b/frontend/src/components/FunixFunction/OutputComponents/OutputChem.tsx
@@ -1,5 +1,6 @@
import { Card, CardMedia } from "@mui/material";
import React, { useEffect, useState } from "react";
+import renderSvg from "../../../shared/indigo-render";
declare global {
interface Window {
@@ -17,12 +18,8 @@ const OutputChem = (props: { data: string }) => {
const loadAndRender = async () => {
setLoading(true);
try {
- const options = new window.indigo.MapStringString();
- options.set("render-output-format", "svg");
- const rawRender = window.indigo.render(props.data, options);
- const img = new Image();
- img.src = `data:image/svg+xml;base64,${rawRender}`;
- setSrc(img.src);
+ const svg = renderSvg(props.data);
+ setSrc(svg);
} catch (e) {
console.error(e);
}
diff --git a/frontend/src/components/FunixFunction/OutputComponents/OutputKetcher.tsx b/frontend/src/components/FunixFunction/OutputComponents/OutputKetcher.tsx
deleted file mode 100644
index 05832d1..0000000
--- a/frontend/src/components/FunixFunction/OutputComponents/OutputKetcher.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import Box from "@mui/material/Box/Box";
-import { Ketcher } from "ketcher-core";
-import { Editor } from "ketcher-react";
-import { StandaloneStructServiceProvider } from "ketcher-standalone";
-import React from "react";
-import { useState } from "react";
-
-declare global {
- interface Window {
- ketcher: Ketcher;
- }
-}
-
-const svgToText = (svg: Blob) => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
- reader.onerror = () => reject(new Error("Failed to read SVG file"));
- reader.readAsText(svg);
- });
-};
-
-const OutputKetcher = (props: { data: string }) => {
- const structServiceProvider = new StandaloneStructServiceProvider();
- const [svg, setSvg] = useState("");
-
- return (
- <>
-
-
- {}}
- onInit={async (ketcher: Ketcher) => {
- const svg = await ketcher
- .generateImage(props.data, {
- outputFormat: "svg",
- })
- .then((svg) => svgToText(svg));
- setSvg(svg as string);
- }}
- />
-
- >
- );
-};
-
-export default React.memo(OutputKetcher);
diff --git a/frontend/src/components/FunixFunction/OutputComponents/OutputNextTo.tsx b/frontend/src/components/FunixFunction/OutputComponents/OutputNextTo.tsx
new file mode 100644
index 0000000..df18cbb
--- /dev/null
+++ b/frontend/src/components/FunixFunction/OutputComponents/OutputNextTo.tsx
@@ -0,0 +1,39 @@
+import { useAtom } from "jotai";
+import { callableDefaultAtom } from "../../../store";
+import { useNavigate } from "react-router-dom";
+import { useCallback, useEffect } from "react";
+
+interface OutputNextToProps {
+ response: string;
+}
+
+type NextToResponse = {
+ args: any;
+ path: string;
+};
+
+const OutputNextTo = (props: OutputNextToProps) => {
+ const response = JSON.parse(props.response) as NextToResponse;
+ const [callableDefault, setCallableDefault] = useAtom(callableDefaultAtom);
+ const navigate = useNavigate();
+
+ const toPath = useCallback(() => {
+ const newCallableDefault = { ...callableDefault };
+ newCallableDefault[response.path] = response.args;
+ setCallableDefault(newCallableDefault);
+ navigate(response.path);
+ }, [
+ callableDefault,
+ response.args,
+ response.path,
+ setCallableDefault,
+ navigate,
+ ]);
+
+ useEffect(() => {
+ toPath();
+ }, [toPath]);
+ return <>>;
+};
+
+export default OutputNextTo;
diff --git a/frontend/src/components/FunixFunction/OutputComponents/OutputRedirectButton.tsx b/frontend/src/components/FunixFunction/OutputComponents/OutputRedirectButton.tsx
new file mode 100644
index 0000000..7e2df90
--- /dev/null
+++ b/frontend/src/components/FunixFunction/OutputComponents/OutputRedirectButton.tsx
@@ -0,0 +1,58 @@
+import { useAtom } from "jotai";
+import { callableDefaultAtom } from "../../../store";
+import { useNavigate } from "react-router-dom";
+import Button from "@mui/material/Button";
+import { useCallback, useEffect, useMemo } from "react";
+
+type RedirectButtonResponse = {
+ path: string;
+ args: string;
+ text: string;
+ auto_direct: boolean;
+};
+
+interface OutputRedirectButtonProps {
+ response: RedirectButtonResponse;
+}
+
+const OutputRedirectButton = (props: OutputRedirectButtonProps) => {
+ const [callableDefault, setCallableDefault] = useAtom(callableDefaultAtom);
+ const navigate = useNavigate();
+ const { response } = props;
+
+ const parsedArgs = useMemo(() => {
+ try {
+ return JSON.parse(response.args);
+ } catch (error) {
+ console.error("Failed to parse args:", error);
+ return {};
+ }
+ }, [response.args]);
+
+ const toPath = useCallback(() => {
+ const newCallableDefault = { ...callableDefault };
+ newCallableDefault[response.path] = parsedArgs;
+ setCallableDefault(newCallableDefault);
+ navigate(response.path);
+ }, [
+ callableDefault,
+ parsedArgs,
+ response.path,
+ setCallableDefault,
+ navigate,
+ ]);
+
+ useEffect(() => {
+ if (response.auto_direct) {
+ toPath();
+ }
+ }, [response.auto_direct, toPath]);
+
+ return (
+
+ );
+};
+
+export default OutputRedirectButton;
diff --git a/frontend/src/components/FunixFunction/OutputPanel.tsx b/frontend/src/components/FunixFunction/OutputPanel.tsx
index e160b8d..8d27766 100644
--- a/frontend/src/components/FunixFunction/OutputPanel.tsx
+++ b/frontend/src/components/FunixFunction/OutputPanel.tsx
@@ -41,7 +41,9 @@ import InnerHTML from "dangerously-set-html-content";
import { useNavigate } from "react-router-dom";
import OutputPlotMedias from "./OutputComponents/OutputPlotImage";
import _ from "lodash";
-import OutputKetcher from "./OutputComponents/OutputChem";
+import OutputChem from "./OutputComponents/OutputChem";
+import OutputRedirectButton from "./OutputComponents/OutputRedirectButton";
+import OutputNextTo from "./OutputComponents/OutputNextTo";
const guessJSON = (response: string | null): object | false => {
if (response === null) return false;
@@ -361,7 +363,9 @@ const OutputPanel = (props: {
);
case "ChemStr":
- return ;
+ return ;
+ case "RedirectButton":
+ return ;
default:
return (
);
} else {
+ if (returnType === "NextTo") {
+ return ;
+ }
if (
returnType !== undefined &&
(Array.isArray(returnType) || typeof returnType === "string")
diff --git a/frontend/src/components/FunixFunction/PyDanticPanel.tsx b/frontend/src/components/FunixFunction/PyDanticPanel.tsx
index 25b6010..8549817 100644
--- a/frontend/src/components/FunixFunction/PyDanticPanel.tsx
+++ b/frontend/src/components/FunixFunction/PyDanticPanel.tsx
@@ -27,6 +27,7 @@ import DateTimePickerWidget from "./DateTimePickerWidget";
import SwitchWidget from "./SwitchWidget";
import TextExtendedWidget from "./TextExtendedWidget";
import ChemEditor from "./ChemEditor";
+import _ from "lodash";
interface PyDanticPanelProps {
rootContent: ReactElement;
@@ -157,49 +158,101 @@ const PyDanticPanel = (props: PyDanticPanelProps) => {
}
};
- const [pydanticForm, setPydanticForm] = useState