From b8abe9bcf2c10ac8473c0d3e3c44ad80aab6f128 Mon Sep 17 00:00:00 2001 From: forrest Date: Thu, 30 Nov 2023 01:59:01 -0600 Subject: [PATCH 01/26] Update pandas DataFrame demo --- examples/table_and_plot.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/examples/table_and_plot.py b/examples/table_and_plot.py index 7258af8c..2acffed0 100644 --- a/examples/table_and_plot.py +++ b/examples/table_and_plot.py @@ -19,17 +19,33 @@ import pandera # the typing library for pandas dataframes -class InputSchema(pandera.DataFrameModel): - a: pandera.typing.Series[int] - b: pandera.typing.Series[float] +# class InputSchema(pandera.DataFrameModel): +# a: pandera.typing.Series[int] +# b: pandera.typing.Series[float] -@funix.funix() +# @funix.funix() +# def table_and_plot( +# df: pandera.typing.DataFrame[InputSchema] = pandas.DataFrame( +# {"a": [5, 17, 29], "b": [3.1415, 2.6342, 1.98964]} # default values +# ) +# ) -> (matplotlib.figure.Figure, pandas.DataFrame): +# fig = matplotlib.pyplot.figure() +# matplotlib.pyplot.plot(df["a"], df["b"]) + +# output_data_frame = pandas.DataFrame( +# {"a-b": df["a"] - df["b"], "a+b": df["a"] + df["b"]} +# ) + +# return fig, output_data_frame + +# Latest implmentation after a discussion in Nov. 2023 + def table_and_plot( - df: pandera.typing.DataFrame[InputSchema] = pandas.DataFrame( + df: pandas.DataFrame = pandas.DataFrame( {"a": [5, 17, 29], "b": [3.1415, 2.6342, 1.98964]} # default values ) -) -> (matplotlib.figure.Figure, pandas.DataFrame): + ) -> (matplotlib.figure.Figure, pandas.DataFrame): fig = matplotlib.pyplot.figure() matplotlib.pyplot.plot(df["a"], df["b"]) @@ -37,4 +53,4 @@ def table_and_plot( {"a-b": df["a"] - df["b"], "a+b": df["a"] + df["b"]} ) - return fig, output_data_frame + return fig, output_data_frame \ No newline at end of file From f575fc912f458db2003c0802d6aab19d070461d3 Mon Sep 17 00:00:00 2001 From: Colerar <233hbj@gmail.com> Date: Thu, 30 Nov 2023 23:19:20 +0800 Subject: [PATCH 02/26] fix: external scripts load --- frontend/config-overrides.js | 62 ++++++++++++++++++++++++++---------- frontend/package.json | 3 +- frontend/src/index.tsx | 11 ------- frontend/yarn.lock | 8 +++++ 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/frontend/config-overrides.js b/frontend/config-overrides.js index bb393011..f165ba53 100644 --- a/frontend/config-overrides.js +++ b/frontend/config-overrides.js @@ -2,34 +2,64 @@ const ProgressBarPlugin = require("progress-bar-webpack-plugin"); const SaveRemoteFilePlugin = require("save-remote-file-webpack-plugin"); const webpack = require("webpack"); +const scripts = process.env.REACT_APP_IN_FUNIX + ? ` + + + ` + : ` + + + `; + module.exports = function override(config) { if (process.env.MUI_PRO_LICENSE_KEY) { config.plugins.push( new webpack.DefinePlugin({ - "process.env.REACT_APP_MUI_PRO_LICENSE_KEY": JSON.stringify(process.env.MUI_PRO_LICENSE_KEY) + "process.env.REACT_APP_MUI_PRO_LICENSE_KEY": JSON.stringify( + process.env.MUI_PRO_LICENSE_KEY + ), }) - ) + ); } + + config.module.rules.push({ + test: /\.html$/i, + type: "asset/source", + }); + config.module.rules.push({ + test: /index\.html$/, + loader: "string-replace-loader", + options: { + search: "", + replace: scripts, + }, + }); + if (process.env.REACT_APP_IN_FUNIX) { config.plugins.push( - new SaveRemoteFilePlugin([{ - url: "https://d3js.org/d3.v5.js", - filepath: "static/js/d3.v5.js", - hash: false - }, { - url: "https://mpld3.github.io/js/mpld3.v0.5.8.js", - filepath: "static/js/mpld3.v0.5.8.js", - hash: false - }]), - ) + new SaveRemoteFilePlugin([ + { + url: "https://d3js.org/d3.v5.js", + filepath: "static/js/d3.v5.js", + hash: false, + }, + { + url: "https://mpld3.github.io/js/mpld3.v0.5.8.js", + filepath: "static/js/mpld3.v0.5.8.js", + hash: false, + }, + ]) + ); } + config.plugins.push( new ProgressBarPlugin(), new webpack.IgnorePlugin({ checkResource(resource) { return resource === "pdfjs-dist/build/pdf.worker.js"; - } - }), - ) + }, + }) + ); return config; -} +}; diff --git a/frontend/package.json b/frontend/package.json index 3171e48f..626d1bb3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -83,6 +83,7 @@ "prettier": "^2.7.1", "progress-bar-webpack-plugin": "^2.1.0", "react-app-rewired": "^2.2.1", - "save-remote-file-webpack-plugin": "^1.1.0" + "save-remote-file-webpack-plugin": "^1.1.0", + "string-replace-loader": "^3.1.0" } } diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 5eb7ca9d..44b3fe91 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -34,17 +34,6 @@ root.render( )} > - {process.env.REACT_APP_IN_FUNIX === undefined ? ( - <> - - - - ) : ( - <> - - - - )} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 03d7c865..c712a0a7 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -10462,6 +10462,14 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== +string-replace-loader@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-3.1.0.tgz#11ac6ee76bab80316a86af358ab773193dd57a4f" + integrity sha512-5AOMUZeX5HE/ylKDnEa/KKBqvlnFmRZudSOjVJHxhoJg9QYTwl1rECx7SLR8BBH7tfxb4Rp7EM2XVfQFxIhsbQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" From d9291a862af4a5b7e5fba53348ea5127fd355293 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Fri, 1 Dec 2023 01:55:05 +0800 Subject: [PATCH 03/26] docs: update GenAI example --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8f16a62d..3d136d80 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,13 @@ For example, the OpenAI's ChatGPT function below, which str-to-str. import os # Python's native import openai # you cannot skip it -openai.api_key = os.environ.get("OPENAI_KEY") - def ChatGPT(prompt: str) -> str: - completion = openai.ChatCompletion.create( + client = openai.Client() + response = client.chat.completions.create( messages=[{"role": "user", "content": prompt}], model="gpt-3.5-turbo" ) - return completion["choices"][0]["message"]["content"] + return response.choices[0].message.content ``` With the magical command `funix -l chatGPT_lazy.py`, you will get a web app like this: From 97fb02b6038f4422c90dcfb65ec01accef00af1c Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:03:07 +0800 Subject: [PATCH 04/26] feat: support docstring as description --- backend/funix/decorator/__init__.py | 9 +++-- backend/funix/util/text.py | 57 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 backend/funix/util/text.py diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 3ef89fcc..7a560ca9 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -71,6 +71,7 @@ from funix.session import get_global_variable, set_global_variable from funix.theme import get_dict_theme, parse_theme from funix.util.module import funix_menu_to_safe_function_name +from funix.util.text import un_indent from funix.util.uri import is_valid_uri from funix.widget import generate_frontend_widget_config @@ -814,10 +815,10 @@ def decorator(function: callable) -> callable: function_title = title if title is not None else function_name function_description = description - # if function_description == "": - # function_docstring = getattr(function, "__doc__") - # if function_docstring: - # function_description = dedent(function_docstring.strip()) + if function_description == "": + function_docstring = getattr(function, "__doc__") + if function_docstring: + function_description = un_indent(function_docstring) if not theme: if "__default" in __parsed_themes: diff --git a/backend/funix/util/text.py b/backend/funix/util/text.py new file mode 100644 index 00000000..af23abd2 --- /dev/null +++ b/backend/funix/util/text.py @@ -0,0 +1,57 @@ +# Copied from [indoc](https://github.com/dtolnay/indoc) + + +def count_space(line: str) -> None | int: + """ + Document is on the way + """ + for i in range(len(line)): + if line[i] != " " and line[i] != "\t": + return i + return None + + +def un_indent(message: str) -> str: + """ + Document is on the way + """ + new_message = message + + ignore_first_line = new_message.startswith("\n") or new_message.startswith("\r\n") + + lines = new_message.splitlines() + + min_spaces = [] + + for i in lines[1:]: + result = count_space(i) + if result is not None: + min_spaces.append(result) + + if len(min_spaces) > 0: + min_space = sorted(min_spaces)[0] + else: + min_space = 0 + + result = "" + + for i in range(len(lines)): + if i > 1 or (i == 1 and not ignore_first_line): + result += "\n" + + if i == 0: + result += lines[i] + elif len(lines) > min_space: + result += lines[i][min_space:] + + return result.strip() + + +# if __name__ == "__main__": +# a = """ +# Return 0 +# +# Parameters: +# """ +# +# print(un_indent(a)) From 83568e06e78a732635ace0c4bc6d6c05273f4cd6 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:13:11 +0800 Subject: [PATCH 05/26] fix: docstring bug --- backend/funix/decorator/__init__.py | 2 +- backend/funix/util/text.py | 8 ++++---- examples/stream.py | 8 ++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 7a560ca9..69df5d7f 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -815,7 +815,7 @@ def decorator(function: callable) -> callable: function_title = title if title is not None else function_name function_description = description - if function_description == "": + if function_description == "" or function_description is None: function_docstring = getattr(function, "__doc__") if function_docstring: function_description = un_indent(function_docstring) diff --git a/backend/funix/util/text.py b/backend/funix/util/text.py index af23abd2..2c0dae1d 100644 --- a/backend/funix/util/text.py +++ b/backend/funix/util/text.py @@ -35,13 +35,15 @@ def un_indent(message: str) -> str: result = "" + print(lines) + for i in range(len(lines)): if i > 1 or (i == 1 and not ignore_first_line): result += "\n" if i == 0: result += lines[i] - elif len(lines) > min_space: + elif len(lines[i]) > min_space: result += lines[i][min_space:] return result.strip() @@ -49,9 +51,7 @@ def un_indent(message: str) -> str: # if __name__ == "__main__": # a = """ -# Return 0 -# -# Parameters: +# This function is used to test the stream feature of Funix. # """ # # print(un_indent(a)) diff --git a/examples/stream.py b/examples/stream.py index e761ae34..8f974bd4 100644 --- a/examples/stream.py +++ b/examples/stream.py @@ -1,8 +1,12 @@ -import time +import time + def stream() -> str: + """ + This function is used to test the stream feature of Funix. + """ message = "Freedom has many difficulties and democracy is not perfect, but we have never had to put a wall up to keep our people in, to prevent them from leaving us. I want to say, on behalf of my countrymen, who live many miles away on the other side of the Atlantic, who are far distant from you, that they take the greatest pride that they have been able to share with you, even from a distance, the story of the last 18 years. I know of no town, no city, that has been besieged for 18 years that still lives with the vitality and the force and the hope and the determination of the city of West Berlin. While the wall is the most obvious and vivid demonstration of the failures of the communist system, for all the world to see, we take no satisfaction in it, for it is, as your mayor has said, an offense not only against history but an offense against humanity, separating families, dividing husbands and wives and brothers and sisters, and dividing a people who wish to be joined together. -- President John F. Kennedy at the Rudolph Wilde Platz, Berlin, June 26, 1963." for i in range(len(message)): time.sleep(0.01) - yield message[0:i] \ No newline at end of file + yield message[0:i] From 31222aefc146bd5514df07a127efddee05eb5594 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:17:06 +0800 Subject: [PATCH 06/26] feat: support label in sheet --- .../components/FunixFunction/ObjectFieldExtendedTemplate.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx index 47891bcc..f28cb99b 100644 --- a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx +++ b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx @@ -401,6 +401,9 @@ const ObjectFieldExtendedTemplate = (props: ObjectFieldProps) => { type: gridColType, editable: !hasArrayWhitelist, }; + if ("title" in elementProps.schema) { + newColumn["headerName"] = elementProps.schema.title; + } const handleCustomComponentChange = ( rowId: GridRowId, field: string, From 7963f90ad24f2ed9f51779fb95b66e6183cc2105 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:54:43 +0800 Subject: [PATCH 07/26] fix: markdown list in toc --- frontend/public/index.html | 10 ++++++++-- frontend/src/components/Common/MarkdownDiv.tsx | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/public/index.html b/frontend/public/index.html index a2c3bf45..502bf6a7 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -14,8 +14,14 @@ rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> - + + Funix diff --git a/frontend/src/components/Common/MarkdownDiv.tsx b/frontend/src/components/Common/MarkdownDiv.tsx index 3c38c5e5..c3aa7994 100644 --- a/frontend/src/components/Common/MarkdownDiv.tsx +++ b/frontend/src/components/Common/MarkdownDiv.tsx @@ -192,6 +192,24 @@ export default function MarkdownDiv(props: MarkdownDivProps) { components={{ p: "span", pre: React.Fragment, + ul: (props) => + isRenderInline ? ( + + ) : ( + + ), + li: (props) => + isRenderInline ? ( +
  • {props.children}
  • + ) : ( +
  • {props.children}
  • + ), + ol: (props) => + isRenderInline ? ( +
      {props.children}
    + ) : ( +
      {props.children}
    + ), h1: (props) => ( Date: Wed, 6 Dec 2023 13:10:24 +0800 Subject: [PATCH 08/26] feat: sort string in list --- frontend/src/App.tsx | 6 +-- frontend/src/components/FunixFunctionList.tsx | 37 ++++++++----------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4794d916..06eb3efd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -51,7 +51,6 @@ import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; import MuiPaper, { PaperProps as MuiPaperProps } from "@mui/material/Paper"; import HistoryDialog from "./components/History/HistoryDialog"; import HistoryList from "./components/History/HistoryList"; -import MarkdownDiv from "./components/Common/MarkdownDiv"; import InlineBox from "./components/Common/InlineBox"; import useFunixHistory from "./shared/useFunixHistory"; @@ -450,10 +449,7 @@ const App = () => { {theme?.direction === "ltr" ? : } - + {selectedFunction?.name || "Funix"} {selectedFunction && selectedFunction.secret && ( = ({ backend }) => { if (!isTree) { return ( - {state.map((functionPreview) => ( - { - changeRadioGroupValueById(functionPreview.id); - }} - key={functionPreview.name} - selected={radioGroupValue === functionPreview.path} - > - - } - disableTypography - /> - - ))} + {state + .sort((a, b) => a.name.localeCompare(b.name)) + .map((functionPreview) => ( + { + changeRadioGroupValueById(functionPreview.id); + }} + key={functionPreview.name} + selected={radioGroupValue === functionPreview.path} + > + + + ))} ); } @@ -265,7 +258,7 @@ const FunixFunctionList: React.FC = ({ backend }) => { }} > } + primary={name} sx={{ wordWrap: "break-word", }} @@ -299,7 +292,7 @@ const FunixFunctionList: React.FC = ({ backend }) => { }} > } + primary={k} sx={{ wordWrap: "break-word", }} From d084052e0a4d05fc8588c460c01b66557352ee24 Mon Sep 17 00:00:00 2001 From: Colerar <233hbj@gmail.com> Date: Wed, 6 Dec 2023 13:57:07 +0800 Subject: [PATCH 09/26] feat: support "*" syntax sugar --- backend/funix/decorator/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 69df5d7f..8b7c719b 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -1224,11 +1224,20 @@ def parse_widget(widget_info: str | tuple | list) -> list[str] | str: for prop_arg_name in prop[0]: if isinstance(prop_arg_name, str): if prop[1] == "widget": - put_props_in_params( - prop_arg_name, - prop[1], - parse_widget(prop[0][prop_arg_name]), - ) + if prop_arg_name == "*": + widget = parse_widget(prop[0][prop_arg_name]) + for k in list(function_params.keys()): + put_props_in_params( + k, + prop[1], + widget, + ) + else: + put_props_in_params( + prop_arg_name, + prop[1], + parse_widget(prop[0][prop_arg_name]), + ) else: put_props_in_params( prop_arg_name, prop[1], prop[0][prop_arg_name] From 8e431629b393587fbe3b41d84a46256249c17423 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:09:27 +0800 Subject: [PATCH 10/26] feat: support "*" in label --- backend/funix/decorator/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 8b7c719b..024efa78 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -1214,6 +1214,8 @@ def parse_widget(widget_info: str | tuple | list) -> list[str] | str: safe_whitelist = {} if not whitelist else whitelist safe_argument_labels = {} if not argument_labels else argument_labels + function_params_name = list(function_params.keys()) + for prop in [ [safe_widgets, "widget"], [safe_treat_as, "treat_as"], @@ -1223,20 +1225,20 @@ def parse_widget(widget_info: str | tuple | list) -> list[str] | str: ]: for prop_arg_name in prop[0]: if isinstance(prop_arg_name, str): - if prop[1] == "widget": + if prop[1] == "title" or prop[1] == "widget": + value = prop[0][prop_arg_name] if prop[1] == "title" else parse_widget(prop[0][prop_arg_name]) if prop_arg_name == "*": - widget = parse_widget(prop[0][prop_arg_name]) - for k in list(function_params.keys()): + for k in function_params_name: put_props_in_params( k, prop[1], - widget, + value, ) else: put_props_in_params( prop_arg_name, prop[1], - parse_widget(prop[0][prop_arg_name]), + value, ) else: put_props_in_params( From c3b9fc7f33af30bf0281551e4ce95963523f74de Mon Sep 17 00:00:00 2001 From: Colerar <233hbj@gmail.com> Date: Thu, 7 Dec 2023 14:31:47 +0800 Subject: [PATCH 11/26] feat: support glob and regex as key --- backend/funix/decorator/__init__.py | 129 +++++++++++++++------------- 1 file changed, 70 insertions(+), 59 deletions(-) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 024efa78..4fc942de 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -4,6 +4,8 @@ import ast import dataclasses import inspect +import pathlib +import re import sys import time from collections import deque @@ -22,9 +24,6 @@ from uuid import uuid4 from flask import Response, request, session -from requests import post -from requests.structures import CaseInsensitiveDict - from funix.app import app, sock from funix.config import ( banned_function_name_and_path, @@ -74,6 +73,8 @@ from funix.util.text import un_indent from funix.util.uri import is_valid_uri from funix.widget import generate_frontend_widget_config +from requests import post +from requests.structures import CaseInsensitiveDict __ipython_type_convert_dict = { "IPython.core.display.Markdown": "Markdown", @@ -1208,64 +1209,74 @@ def parse_widget(widget_info: str | tuple | list) -> list[str] | str: widget_result.append(widget_item) return widget_result - safe_widgets = {} if not widgets else widgets - safe_treat_as = {} if not treat_as else treat_as - safe_examples = {} if not examples else examples - safe_whitelist = {} if not whitelist else whitelist - safe_argument_labels = {} if not argument_labels else argument_labels - - function_params_name = list(function_params.keys()) - - for prop in [ - [safe_widgets, "widget"], - [safe_treat_as, "treat_as"], - [safe_argument_labels, "title"], - [safe_examples, "example"], - [safe_whitelist, "whitelist"], - ]: - for prop_arg_name in prop[0]: - if isinstance(prop_arg_name, str): - if prop[1] == "title" or prop[1] == "widget": - value = prop[0][prop_arg_name] if prop[1] == "title" else parse_widget(prop[0][prop_arg_name]) - if prop_arg_name == "*": - for k in function_params_name: - put_props_in_params( - k, - prop[1], - value, - ) - else: - put_props_in_params( - prop_arg_name, - prop[1], - value, - ) - else: - put_props_in_params( - prop_arg_name, prop[1], prop[0][prop_arg_name] + def iter_over_prop( + argument_type: str, + argument: dict[str | tuple, Any] | None, + callback, + ): + """ + callback: pass in (argument_type, key, key_idx, value) + """ + if argument is None: + return + + for arg_idx, arg_key in enumerate(argument): + if isinstance(arg_key, str): + callback(argument_type, arg_key, 0, argument[arg_key]) + elif isinstance(arg_key, tuple): + for key_idx, single_key in enumerate(arg_key): + callback( + argument_type, single_key, key_idx, argument[arg_key] ) - if prop[1] in ["example", "whitelist"]: - check_example_whitelist(prop_arg_name) else: - if prop[1] in ["example", "whitelist"]: - for index, single_prop_arg_name in enumerate(prop_arg_name): - put_props_in_params( - single_prop_arg_name, - prop[1], - prop[0][single_prop_arg_name][index], - ) - check_example_whitelist(single_prop_arg_name) - elif prop[1] == "widget": - cached_result = parse_widget(prop[0][prop_arg_name]) - for prop_arg_name_item in prop_arg_name: - put_props_in_params( - prop_arg_name_item, prop[1], cached_result - ) - else: - for prop_arg_name_item in prop_arg_name: - put_props_in_params( - prop_arg_name_item, prop[1], prop[0][prop_arg_name] - ) + raise TypeError( + f"Argument `{argument_type}` has invalid key type {type(argument)}" + ) + + function_params_name: list[str] = list(function_params.keys()) + + def expand_wildcards(origin_key: str, search_list: list[str]) -> list[str]: + keys = [] + if "*" in origin_key or "?" in origin_key: + for param_name in search_list: + if pathlib.PurePath(param_name).match(origin_key): + keys.append(param_name) + elif origin_key.startswith("regex:"): + regex = re.compile(origin_key[6:]) + for param_name in search_list: + if regex.search(param_name) is not None: + keys.append(param_name) + else: + keys.append(origin_key) + return keys + + def process_widgets(arg_type: str, arg_key: str, key_idx: int, value: any): + parsed_widget = parse_widget(value) + for expanded_key in expand_wildcards(arg_key, function_params_name): + put_props_in_params(expanded_key, arg_type, parsed_widget) + + iter_over_prop("widget", widgets, process_widgets) + + def process_title(arg_type: str, arg_key: str, key_idx: int, value: any): + for expanded_key in expand_wildcards(arg_key, function_params_name): + put_props_in_params(expanded_key, arg_type, value) + + iter_over_prop("title", argument_labels, process_title) + + def process_treat_as(arg_type: str, arg_key: str, key_idx: int, value: any): + put_props_in_params(arg_key, arg_type, value) + + iter_over_prop("treat_as", treat_as, process_treat_as) + + def process_examples_and_whitelist( + arg_type: str, arg_key: str, key_idx: int, value: any + ): + put_props_in_params(arg_key, arg_type, value[key_idx]) + check_example_whitelist(arg_key) + + iter_over_prop("example", examples, process_examples_and_whitelist) + iter_over_prop("whitelist", whitelist, process_examples_and_whitelist) + input_attr = "" safe_argument_config = {} if argument_config is None else argument_config From 38dab2807e9c4a2441036279e685fdfd65337ef5 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:56:27 +0800 Subject: [PATCH 12/26] fix: run script in HTML type --- frontend/package.json | 1 + .../ObjectFieldExtendedTemplate.tsx | 7 +++---- .../src/components/FunixFunction/OutputPanel.tsx | 16 ++++++++-------- frontend/yarn.lock | 5 +++++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 626d1bb3..ac675b85 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "@types/react-dom": "^18.0.6", "@types/react-pdf": "^6.2.0", "@types/uuid": "^9.0.2", + "dangerously-set-html-content": "^1.1.0", "jotai": "^1.7.4", "localforage": "^1.10.0", "material-ui-popup-state": "^4.0.2", diff --git a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx index f28cb99b..19d51eeb 100644 --- a/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx +++ b/frontend/src/components/FunixFunction/ObjectFieldExtendedTemplate.tsx @@ -56,6 +56,7 @@ import FileUploadWidget from "./FileUploadWidget"; import { useAtom } from "jotai"; import { storeAtom } from "../../store"; import { DataGrid } from "../../Key"; +import InnerHTML from "dangerously-set-html-content"; let rowIdCounter = 0; @@ -512,10 +513,8 @@ const ObjectFieldExtendedTemplate = (props: ObjectFieldProps) => { break; case "html": rowElement = ( -
    ); break; diff --git a/frontend/src/components/FunixFunction/OutputPanel.tsx b/frontend/src/components/FunixFunction/OutputPanel.tsx index 8a41c186..a3148fdf 100644 --- a/frontend/src/components/FunixFunction/OutputPanel.tsx +++ b/frontend/src/components/FunixFunction/OutputPanel.tsx @@ -31,6 +31,7 @@ import { ExpandMore } from "@mui/icons-material"; import ThemeReactJson from "../Common/ThemeReactJson"; import { DataGrid } from "../../Key"; import OutputDataframe from "./OutputComponents/OutputDataframe"; +import InnerHTML from "dangerously-set-html-content"; const guessJSON = (response: string | null): object | false => { if (response === null) return false; @@ -237,7 +238,7 @@ const OutputPanel = (props: { case "Markdown": return ; case "HTML": - return
    ; + return ; case "Images": case "Videos": case "Audios": @@ -321,13 +322,12 @@ const OutputPanel = (props: { break; case "html": itemElement = ( -
    ); break; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c712a0a7..c3543919 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4178,6 +4178,11 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +dangerously-set-html-content@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/dangerously-set-html-content/-/dangerously-set-html-content-1.1.0.tgz#cffd83f2a8e3d59eca7302feecad1b068e94a4c3" + integrity sha512-kUHpnYZ9EgT6BKUEgrgccg17Pa0YdI9MlWdDYeu49HIXYONCxZpKr6Tj24q+LwFmbmtL3IJ1Rvj+aaTTzFOepg== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" From 8f022739fa82bc797a2f48747eae9dda041e22da Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:48:53 +0800 Subject: [PATCH 13/26] feat: add wordle example --- backend/funix/decorator/runtime.py | 6 + examples/files/words.json | 639 +++++++++++++++++++++++++++++ examples/wordle.py | 80 ++++ frontend/src/store.ts | 2 +- 4 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 examples/files/words.json create mode 100644 examples/wordle.py diff --git a/backend/funix/decorator/runtime.py b/backend/funix/decorator/runtime.py index fefacaec..0766db82 100644 --- a/backend/funix/decorator/runtime.py +++ b/backend/funix/decorator/runtime.py @@ -39,6 +39,8 @@ def __init__(self, cls_name: str, funix_: Any, cls: Any): self._cls = cls self._imports = [] + self.open_function = False + def visit_Import(self, node): self._imports.append(node) @@ -50,9 +52,13 @@ def visit_ClassDef(self, node: ClassDef) -> Any: return for cls_function in node.body: if isinstance(cls_function, FunctionDef): + self.open_function = True self.visit_FunctionDef(cls_function) + self.open_function = False def visit_FunctionDef(self, node: FunctionDef) -> Any: + if not self.open_function: + return args = node.args is_static_method = False diff --git a/examples/files/words.json b/examples/files/words.json new file mode 100644 index 00000000..a3c7b7cf --- /dev/null +++ b/examples/files/words.json @@ -0,0 +1,639 @@ +[ + "acorn", + "acrid", + "actor", + "adept", + "adobe", + "adorn", + "adult", + "agent", + "agony", + "aisle", + "alder", + "alien", + "alike", + "alive", + "alone", + "aloud", + "amber", + "ample", + "amuck", + "angel", + "angry", + "ankle", + "antic", + "arise", + "aspen", + "aspic", + "audio", + "awful", + "azure", + "balmy", + "bandy", + "basic", + "basin", + "batch", + "baton", + "bawdy", + "beady", + "beamy", + "beast", + "being", + "bight", + "bigot", + "binge", + "bingo", + "biped", + "birch", + "birth", + "bison", + "biter", + "blame", + "bland", + "blank", + "bleak", + "bleat", + "blind", + "bloat", + "blond", + "blunt", + "bodge", + "bogie", + "bogus", + "boned", + "bonus", + "bound", + "boxer", + "brack", + "braid", + "brand", + "brash", + "brave", + "brawl", + "brawn", + "brick", + "brief", + "brisk", + "broad", + "broke", + "brute", + "bugle", + "built", + "bulky", + "burly", + "bushy", + "butch", + "cadet", + "cadre", + "calyx", + "camel", + "caste", + "cedar", + "chaos", + "chard", + "cheap", + "chest", + "chief", + "china", + "chirp", + "chive", + "choir", + "choky", + "chore", + "churl", + "clave", + "cleft", + "clerk", + "clove", + "clown", + "clung", + "comer", + "conga", + "congé", + "coral", + "corny", + "corps", + "corse", + "court", + "couth", + "cover", + "covey", + "crake", + "cramp", + "craps", + "crash", + "crawl", + "crazy", + "cream", + "crime", + "crimp", + "croup", + "crown", + "crude", + "cruel", + "cruet", + "crump", + "crypt", + "curia", + "curst", + "curve", + "daily", + "datum", + "delta", + "demon", + "depth", + "diary", + "dicky", + "dinar", + "diner", + "dinge", + "disco", + "diver", + "dogma", + "doing", + "dough", + "downy", + "dowry", + "dozen", + "draft", + "drake", + "drift", + "drove", + "drunk", + "ducat", + "dumpy", + "dusky", + "dusty", + "dwarf", + "dying", + "debut", + "early", + "elfin", + "entry", + "envoy", + "epoch", + "equal", + "ergot", + "ethic", + "exact", + "exist", + "extra", + "faint", + "fairy", + "faker", + "fakir", + "false", + "fancy", + "fated", + "feint", + "felon", + "femur", + "feral", + "field", + "fiend", + "fiery", + "filet", + "final", + "finch", + "first", + "fishy", + "fitch", + "fiver", + "fixed", + "fixer", + "flair", + "flaky", + "flank", + "flask", + "flesh", + "flint", + "flirt", + "flora", + "fluid", + "fluke", + "flush", + "forte", + "forum", + "fount", + "frail", + "frank", + "frisk", + "frond", + "fusil", + "fusty", + "gamut", + "gaper", + "garth", + "gaunt", + "gauze", + "genus", + "getup", + "giant", + "glade", + "gland", + "glare", + "gloat", + "gnome", + "grail", + "grand", + "grate", + "grave", + "gravy", + "great", + "grief", + "grise", + "groat", + "group", + "grove", + "guild", + "gumbo", + "hardy", + "haver", + "hawse", + "hazel", + "heady", + "heavy", + "hoary", + "honey", + "horde", + "hover", + "human", + "husky", + "idler", + "inert", + "inlet", + "irony", + "ivory", + "jerky", + "jihad", + "joust", + "juicy", + "jumbo", + "knave", + "labor", + "laity", + "laker", + "large", + "laser", + "later", + "latex", + "laver", + "leafy", + "leaky", + "lemon", + "letch", + "limbo", + "lined", + "liner", + "lithe", + "liver", + "loath", + "locus", + "lofty", + "logic", + "lotus", + "lover", + "lower", + "lucid", + "lucky", + "lumen", + "lurid", + "lusty", + "lying", + "lyric", + "macro", + "madly", + "magus", + "maize", + "major", + "manes", + "mango", + "manly", + "manor", + "manse", + "maybe", + "mealy", + "meaty", + "media", + "medic", + "midge", + "miner", + "minor", + "mirth", + "miser", + "misty", + "mixed", + "mixer", + "mocha", + "modal", + "model", + "moiré", + "moist", + "molar", + "monad", + "moral", + "morel", + "mould", + "mousy", + "mover", + "movie", + "mufti", + "murky", + "mushy", + "music", + "musty", + "nadir", + "naive", + "naked", + "nasty", + "nates", + "navel", + "neigh", + "nervy", + "night", + "noble", + "noisy", + "north", + "noted", + "nymph", + "oared", + "ocean", + "ocher", + "odium", + "often", + "olive", + "omega", + "opera", + "optic", + "ounce", + "outer", + "ovary", + "ovine", + "palsy", + "panic", + "pants", + "party", + "pasty", + "paten", + "peach", + "pecan", + "pedal", + "penal", + "phony", + "piano", + "piety", + "piker", + "pilot", + "pinch", + "pinky", + "pious", + "pithy", + "plain", + "plumb", + "plush", + "poker", + "pokey", + "polar", + "polka", + "porch", + "porgy", + "poser", + "prawn", + "prime", + "primo", + "privy", + "prize", + "prone", + "prose", + "proud", + "proxy", + "pubes", + "pylon", + "quack", + "qualm", + "quart", + "quick", + "quiet", + "quint", + "quirk", + "quite", + "quota", + "rabid", + "radix", + "raphe", + "rapid", + "ratio", + "raven", + "rayon", + "ready", + "regal", + "reign", + "reins", + "relax", + "relay", + "relic", + "rheum", + "right", + "rocky", + "rogue", + "roman", + "rouge", + "rough", + "royal", + "runic", + "rusty", + "sable", + "sabre", + "salon", + "salty", + "salvo", + "sandy", + "satin", + "satyr", + "saucy", + "scald", + "scaly", + "scant", + "scape", + "scary", + "scion", + "scrim", + "scrip", + "setup", + "shady", + "shaky", + "shank", + "shard", + "shine", + "shiny", + "shire", + "shlep", + "shoal", + "shock", + "short", + "showy", + "sigma", + "siren", + "skate", + "skein", + "skimp", + "slate", + "slave", + "slick", + "slimy", + "sloth", + "smock", + "smoky", + "snail", + "snake", + "snaky", + "snowy", + "soapy", + "sober", + "solar", + "solid", + "sough", + "south", + "spare", + "spate", + "spelt", + "spent", + "sperm", + "spick", + "spicy", + "spiny", + "splat", + "splay", + "split", + "spore", + "sport", + "sprat", + "sprue", + "spume", + "spunk", + "squab", + "squat", + "squid", + "stair", + "stale", + "stang", + "stark", + "steam", + "stern", + "stich", + "stile", + "stock", + "stoic", + "stole", + "stoma", + "stone", + "straw", + "stria", + "stuck", + "suite", + "sulky", + "sumac", + "super", + "swain", + "swale", + "swank", + "sward", + "swart", + "swing", + "sworn", + "sylph", + "synod", + "syrup", + "tache", + "talon", + "talus", + "tango", + "tawny", + "teary", + "tempo", + "tenor", + "thick", + "thief", + "thing", + "think", + "thong", + "throb", + "thunk", + "tiger", + "tiler", + "timer", + "tipsy", + "tired", + "topaz", + "topic", + "torus", + "tough", + "touse", + "trace", + "trial", + "trice", + "tripe", + "trope", + "truck", + "truly", + "trunk", + "tuber", + "tulip", + "tumid", + "tumor", + "tuner", + "tunic", + "twice", + "twink", + "ultra", + "umber", + "umbra", + "unfit", + "unite", + "upset", + "urban", + "valor", + "velar", + "velum", + "venal", + "video", + "vinyl", + "viola", + "viper", + "vista", + "vital", + "vixen", + "vizor", + "vocal", + "vogue", + "wader", + "washy", + "waste", + "waver", + "waxen", + "weald", + "weary", + "weird", + "welsh", + "wench", + "wheat", + "whelk", + "whist", + "white", + "wight", + "wince", + "windy", + "wiper", + "wired", + "wizen", + "world", + "wormy", + "worse", + "worth", + "wound", + "woven", + "wrath", + "wrong", + "yacht", + "zebra" +] diff --git a/examples/wordle.py b/examples/wordle.py new file mode 100644 index 00000000..1e049c85 --- /dev/null +++ b/examples/wordle.py @@ -0,0 +1,80 @@ +import random +import string +from funix import funix_class, funix_method +from funix.hint import HTML + +import json + + +with open("./files/words.json", "r") as f: + words = json.load(f) + + +def random_5_letter() -> str: + """ + Get random 5 letter + """ + return "".join(random.choices(string.ascii_lowercase, k=5)) + + +@funix_class() +class Wordle: + @funix_method( + title="Wordle Settings", + description="Settings for the Wordle", + argument_labels={ + "random_word": "Random 5-letter" + } + ) + def __init__(self, random_word: bool = False): + self.random_word = random_word + + self.word = random_5_letter() if random_word else random.choice(words) + self.history = [] + + self.mismatch = False + + print(self.word) + + def __reset(self): + self.word = random_5_letter() if self.random_word else random.choice(words) + self.history = [] + + def __push(self, word: str): + if len(word) != 5 or not word.isalpha(): + self.mismatch = True + else: + self.mismatch = False + if len(self.history) > 6: + self.__reset() + self.history.append(word.lower()) + + def __generate(self) -> HTML: + html_code = "" + for i in range(6): + html_code += f"
    0 else ''}'>" + if i < len(self.history): + for single_word_index in range(5): + single_word = self.history[i][single_word_index] + single_word_color = "rgb(156, 163, 175)" + if single_word in self.word: + single_word_color = "rgb(251, 191, 36)" + if single_word == self.word[single_word_index]: + single_word_color = "rgb(34, 197, 94)" + html_code += f"
    {single_word}
    " + else: + for j in range(5): + html_code += f"
    " + html_code += "
    " + if self.mismatch: + html_code += "Word mismatch, please enter a 5-letter word" + if len(self.history) > 0 and self.history[-1] == self.word: + self.__reset() + html_code += "Game Over, you win!" + elif len(self.history) == 6: + html_code += f"Game Over, answer is: {self.word}" + return html_code + + def guess(self, word: str) -> HTML: + self.__push(word) + return self.__generate() diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 4d8a5738..126010ba 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -29,7 +29,7 @@ export const storeAtom = atom({ backend: null, backHistory: null, backConsensus: [false, false, false], - saveHistory: true, + saveHistory: false, appSecret: null, histories: [], }); From bf9c04a6c54aeebe819aadc0ad7427827087bbbd Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:33:52 +0800 Subject: [PATCH 14/26] feat: try to support reactive argument --- backend/funix/decorator/__init__.py | 92 ++++++++++++++++++- backend/funix/hint/__init__.py | 5 + examples/reactive.py | 10 ++ frontend/package.json | 2 + .../components/FunixFunction/InputPanel.tsx | 37 +++++++- frontend/src/shared/index.ts | 8 ++ frontend/yarn.lock | 5 + 7 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 examples/reactive.py diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 4fc942de..a7894fef 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -19,7 +19,7 @@ from secrets import token_hex from traceback import format_exc from types import ModuleType -from typing import Any, Optional +from typing import Any, Optional, Callable from urllib.request import urlopen from uuid import uuid4 @@ -66,6 +66,7 @@ TreatAsType, WhitelistType, WidgetsType, + ReactiveType, ) from funix.session import get_global_variable, set_global_variable from funix.theme import get_dict_theme, parse_theme @@ -697,6 +698,7 @@ def funix( menu: Optional[str] = None, default: bool = False, rate_limit: Limiter | list | dict = [], + reactive: ReactiveType = None, print_to_web: bool = False, ): """ @@ -733,6 +735,7 @@ def funix( You don't need to set it unless you are funixing a directory and package. default(bool): whether this function is the default function rate_limit(Limiter | list[Limiter]): rate limiters, an object or a list + reactive(ReactiveType): reactive config print_to_web(bool): handle all stdout to web Returns: @@ -899,6 +902,92 @@ def decorator(function: callable) -> callable: need_websocket = True setattr(function_signature, "_return_annotation", Markdown) + has_reactive_params = False + + reactive_config: dict[str, tuple[Callable, dict[str, str]]] = {} + """ + Empty dict: full form data + Dict argument keys: map + """ + + if isinstance(reactive, dict): + has_reactive_params = True + for reactive_param in reactive.keys(): + if reactive_param not in function_params: + raise ValueError( + f"Reactive param `{reactive_param}` not found in function `{function_name}`" + ) + callable_or_with_config = reactive[reactive_param] + + if isinstance(callable_or_with_config, tuple): + callable_ = callable_or_with_config[0] + full_config = callable_or_with_config[1] + else: + callable_ = callable_or_with_config + full_config = None + + callable_params = signature(callable_).parameters + + for callable_param in dict(callable_params.items()).values(): + if ( + callable_param.kind == Parameter.VAR_KEYWORD + or callable_param.kind == Parameter.VAR_POSITIONAL + ): + reactive_config[reactive_param] = (callable_, {}) + break + + if reactive_param not in reactive_config: + if full_config: + reactive_config[reactive_param] = (callable_, full_config) + else: + reactive_config[reactive_param] = (callable_, {}) + for key in dict(callable_params.items()).keys(): + if key not in function_params: + raise ValueError( + f"The key {key} is not in function, please write full config" + ) + reactive_config[reactive_param][1][key] = key + + def function_reactive_update(): + reactive_param_value = {} + + form_data = request.get_json() + + for key_, item_ in reactive_config.items(): + argument_key: str = key_ + callable_function: Callable = item_[0] + callable_config: dict[str, str] = item_[1] + + try: + if callable_config == {}: + reactive_param_value[argument_key] = callable_function( + **form_data + ) + else: + new_form_data = {} + for key__, value in callable_config.items(): + new_form_data[key__] = form_data[value] + reactive_param_value[argument_key] = callable_function( + **new_form_data + ) + except: + pass + + if reactive_param_value == {}: + return {"result": None} + + return {"result": reactive_param_value} + + function_reactive_update.__name__ = function_name + "_reactive_update" + + if safe_module_now: + function_reactive_update.__name__ = ( + f"{safe_module_now}_{function_reactive_update.__name__}" + ) + + app.post(f"/update/{function_id}")(function_reactive_update) + app.post(f"/update/{endpoint}")(function_reactive_update) + __decorated_functions_list.append( { "name": function_title, @@ -907,6 +996,7 @@ def decorator(function: callable) -> callable: "secret": secret_key, "id": function_id, "websocket": need_websocket, + "reactive": has_reactive_params, } ) diff --git a/backend/funix/hint/__init__.py b/backend/funix/hint/__init__.py index a6a2e9bf..db10a48e 100644 --- a/backend/funix/hint/__init__.py +++ b/backend/funix/hint/__init__.py @@ -244,6 +244,11 @@ class ConditionalVisible(TypedDict): PreFillEmpty = TypeVar("PreFillEmpty") +ReactiveType = Optional[dict[str, Callable | tuple[Callable, dict[str, str]]]] +""" +Document is on the way +""" + class CodeConfig(TypedDict): """ diff --git a/examples/reactive.py b/examples/reactive.py new file mode 100644 index 00000000..3c77eb66 --- /dev/null +++ b/examples/reactive.py @@ -0,0 +1,10 @@ +from funix import funix + + +def add(a: int, b: int) -> int: + return a + b + + +@funix(reactive={"c": add}) +def add_reactive(a: int, b: int, c: int) -> int: + return a + b + c diff --git a/frontend/package.json b/frontend/package.json index ac675b85..f9fde6d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "dangerously-set-html-content": "^1.1.0", "jotai": "^1.7.4", "localforage": "^1.10.0", + "lodash": "^4.17.21", "material-ui-popup-state": "^4.0.2", "notistack": "^3.0.1", "react": "^18.2.0", @@ -73,6 +74,7 @@ ] }, "devDependencies": { + "@types/lodash": "^4.14.202", "@types/markdown-it": "^12.2.3", "@types/react-syntax-highlighter": "^15.5.10", "@typescript-eslint/eslint-plugin": "^5.30.5", diff --git a/frontend/src/components/FunixFunction/InputPanel.tsx b/frontend/src/components/FunixFunction/InputPanel.tsx index 8390fcd5..d46fe5cc 100644 --- a/frontend/src/components/FunixFunction/InputPanel.tsx +++ b/frontend/src/components/FunixFunction/InputPanel.tsx @@ -10,7 +10,12 @@ import Card from "@mui/material/Card"; // eslint-disable-next-line @typescript-e // @ts-ignore import Form from "@rjsf/material-ui/v5"; import React, { useEffect, useState } from "react"; -import { callFunctionRaw, FunctionDetail, FunctionPreview } from "../../shared"; +import { + callFunctionRaw, + FunctionDetail, + FunctionPreview, + UpdateResult, +} from "../../shared"; import ObjectFieldExtendedTemplate from "./ObjectFieldExtendedTemplate"; import SwitchWidget from "./SwitchWidget"; import TextExtendedWidget from "./TextExtendedWidget"; @@ -18,6 +23,7 @@ import { useAtom } from "jotai"; import { storeAtom } from "../../store"; import useFunixHistory from "../../shared/useFunixHistory"; import { useSnackbar } from "notistack"; +import _ from "lodash"; const InputPanel = (props: { detail: FunctionDetail; @@ -69,8 +75,35 @@ const InputPanel = (props: { }, [backConsensus]); const handleChange = ({ formData }: Record) => { - // console.log("Data changed: ", formData); + console.log("Data changed: ", formData); setForm(formData); + + _.debounce(() => { + fetch(new URL(`/update/${props.preview.id}`, props.backend), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }) + .then((body) => { + return body.json(); + }) + .then((data: UpdateResult) => { + const result = data.result; + + if (result !== null) { + for (const [key, value] of Object.entries(result)) { + setForm((form) => { + return { + ...form, + [key]: value, + }; + }); + } + } + }); + }, 100)(); }; const saveOutput = async ( diff --git a/frontend/src/shared/index.ts b/frontend/src/shared/index.ts index 5266383c..25cfe7b2 100644 --- a/frontend/src/shared/index.ts +++ b/frontend/src/shared/index.ts @@ -72,6 +72,10 @@ export type FunctionPreview = { * Is this function need websocket */ websocket: boolean; + /** + * Is this function has reactive argument + */ + reactive: boolean; }; export type GetListResponse = { @@ -96,6 +100,10 @@ export type Param = { example?: any[]; }; +export type UpdateResult = { + result: null | Record; +}; + export type FunctionDetail = { /** * Unique ID that won't make conflict diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c3543919..22aebfdc 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2417,6 +2417,11 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== +"@types/lodash@^4.14.202": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + "@types/markdown-it@^12.2.3": version "12.2.3" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" From d466de5845f3351100a4b05f061442c08ab01619 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Wed, 13 Dec 2023 04:58:59 +0800 Subject: [PATCH 15/26] fix: do not update when function has no reactive --- frontend/src/components/FunixFunction/InputPanel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/FunixFunction/InputPanel.tsx b/frontend/src/components/FunixFunction/InputPanel.tsx index d46fe5cc..4b25b942 100644 --- a/frontend/src/components/FunixFunction/InputPanel.tsx +++ b/frontend/src/components/FunixFunction/InputPanel.tsx @@ -75,9 +75,13 @@ const InputPanel = (props: { }, [backConsensus]); const handleChange = ({ formData }: Record) => { - console.log("Data changed: ", formData); + // console.log("Data changed: ", formData); setForm(formData); + if (!props.preview.reactive) { + return; + } + _.debounce(() => { fetch(new URL(`/update/${props.preview.id}`, props.backend), { method: "POST", From 3db6efdfc78b8c952363c03bdbf2d23160e034e6 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Wed, 13 Dec 2023 05:07:35 +0800 Subject: [PATCH 16/26] feat: better rate limit error display --- backend/funix/decorator/__init__.py | 9 ++++++--- frontend/src/components/FunixFunction/OutputPanel.tsx | 4 ---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index a7894fef..6dadd8a1 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -401,10 +401,13 @@ def rate_limit(self) -> Optional[Response]: if len(queue) >= self.max_calls: time_passed = current_time - queue[0] time_to_wait = int(self.period - time_passed) - error_message = ( - f"Rate limit exceeded. Please try again in {time_to_wait} seconds." + error_message = { + "error_body": f"Rate limit exceeded. Please try again in {time_to_wait} seconds.", + "error_type": "safe_checker", + } + return Response( + dumps(error_message), status=429, mimetype="application/json" ) - return Response(error_message, status=429, mimetype="text/plain") queue.append(current_time) return None diff --git a/frontend/src/components/FunixFunction/OutputPanel.tsx b/frontend/src/components/FunixFunction/OutputPanel.tsx index a3148fdf..3be3314d 100644 --- a/frontend/src/components/FunixFunction/OutputPanel.tsx +++ b/frontend/src/components/FunixFunction/OutputPanel.tsx @@ -284,10 +284,6 @@ const OutputPanel = (props: { button at the bottom of the left, input panel. ); - } else if ( - response.startsWith("Rate limit exceeded. Please try again in ") - ) { - return {response}; } else { if ( typeof returnType !== undefined && From d6508ac69e7e483441ab07d2c8c20750ec5897b7 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Wed, 13 Dec 2023 06:10:50 +0800 Subject: [PATCH 17/26] feat: add empty function list check --- backend/funix/__init__.py | 6 ++++++ backend/funix/decorator/__init__.py | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/backend/funix/__init__.py b/backend/funix/__init__.py index 96fdb6ce..b9fbf717 100644 --- a/backend/funix/__init__.py +++ b/backend/funix/__init__.py @@ -467,6 +467,12 @@ def run( default=default, ) + if decorator.is_empty_function_list(): + print( + "No functions nor classes decorated by Funix. Could you wanna enable the lazy mode (add -l flag)?" + ) + sys.exit(1) + parsed_ip = ip_address(host) parsed_port = get_unused_port_from(port, parsed_ip) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 6dadd8a1..d2c48d97 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -19,11 +19,14 @@ from secrets import token_hex from traceback import format_exc from types import ModuleType -from typing import Any, Optional, Callable +from typing import Any, Callable, Optional from urllib.request import urlopen from uuid import uuid4 from flask import Response, request, session +from requests import post +from requests.structures import CaseInsensitiveDict + from funix.app import app, sock from funix.config import ( banned_function_name_and_path, @@ -63,10 +66,10 @@ OutputLayout, PreFillEmpty, PreFillType, + ReactiveType, TreatAsType, WhitelistType, WidgetsType, - ReactiveType, ) from funix.session import get_global_variable, set_global_variable from funix.theme import get_dict_theme, parse_theme @@ -74,8 +77,6 @@ from funix.util.text import un_indent from funix.util.uri import is_valid_uri from funix.widget import generate_frontend_widget_config -from requests import post -from requests.structures import CaseInsensitiveDict __ipython_type_convert_dict = { "IPython.core.display.Markdown": "Markdown", @@ -464,6 +465,16 @@ def parse_limiter_args(rate_limit: Limiter | list | dict, arg_name: str = "rate_ global_rate_limiters: list[Limiter] = [] +def is_empty_function_list() -> bool: + """ + Check if the function list is empty. + + Returns: + bool: True if empty, False otherwise. + """ + return len(__decorated_functions_list) == 0 + + def set_kumo_info(url: str, token: str) -> None: """ Set the kumo info. From a8993b7e6eba98d4367401f4d84116904b0bf412 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Wed, 13 Dec 2023 06:19:14 +0800 Subject: [PATCH 18/26] feat: if Literal args less than 8 use radio --- backend/funix/decorator/magic.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/funix/decorator/magic.py b/backend/funix/decorator/magic.py index 43e0e297..62f37e77 100644 --- a/backend/funix/decorator/magic.py +++ b/backend/funix/decorator/magic.py @@ -211,8 +211,13 @@ def get_type_widget_prop( break if not widget: if hasattr(function_annotation, "__name__"): - if getattr(function_annotation, "__name__") in builtin_widgets: - widget = builtin_widgets[getattr(function_annotation, "__name__")] + function_annotation_name = getattr(function_annotation, "__name__") + if function_annotation_name == "Literal": + widget = ( + "radio" if len(function_annotation.__args__) < 8 else "inputbox" + ) + elif function_annotation_name in builtin_widgets: + widget = builtin_widgets[function_annotation_name] if widget and anal_result: anal_result["widget"] = widget From 9250251878e2a6890847b14f0e10278e28db3af0 Mon Sep 17 00:00:00 2001 From: Colerar <233hbj@gmail.com> Date: Wed, 13 Dec 2023 12:54:01 +0800 Subject: [PATCH 19/26] fix: improve class init error message --- backend/funix/decorator/__init__.py | 6 ++++++ backend/funix/decorator/runtime.py | 3 ++- backend/funix/hint/__init__.py | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index d2c48d97..3f1a831a 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -70,6 +70,7 @@ TreatAsType, WhitelistType, WidgetsType, + WrapperException, ) from funix.session import get_global_variable, set_global_variable from funix.theme import get_dict_theme, parse_theme @@ -1919,6 +1920,11 @@ def wrapped_function(**wrapped_function_kwargs): try: function_call_result = function(**wrapped_function_kwargs) return pre_anal_result(function_call_result) + except WrapperException as e: + return { + "error_type": "wrapper", + "error_body": str(e), + } except: return { "error_type": "function", diff --git a/backend/funix/decorator/runtime.py b/backend/funix/decorator/runtime.py index 0766db82..e160f8ad 100644 --- a/backend/funix/decorator/runtime.py +++ b/backend/funix/decorator/runtime.py @@ -16,6 +16,7 @@ from typing import Any import funix +from funix.hint import WrapperException from funix.session import get_global_variable, set_global_variable @@ -23,7 +24,7 @@ def get_init_function(cls_name: str): inited_class = get_global_variable("__FUNIX_" + cls_name) if inited_class is None: - raise Exception("Class must be inited first!") + raise WrapperException("Class must be inited first!") else: return inited_class diff --git a/backend/funix/hint/__init__.py b/backend/funix/hint/__init__.py index db10a48e..a1f14249 100644 --- a/backend/funix/hint/__init__.py +++ b/backend/funix/hint/__init__.py @@ -497,3 +497,12 @@ def decorator(cls) -> Any: return cls return decorator + + +class WrapperException(Exception): + """ + A wrapper exception for internal error handling, will be converted to + {"error_type": "wrapper", "error_body": str(exception)} and send to frontend + """ + + pass From 2527d7c57a492c7fde710c876b80777d7b61833d Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Fri, 15 Dec 2023 17:34:33 +0800 Subject: [PATCH 20/26] feat: support camera and microphone input --- .../FunixFunction/FileUploadWidget.tsx | 342 ++++++++++++------ .../OutputComponents/OutputMedias.tsx | 10 +- frontend/src/shared/media.ts | 86 +++++ frontend/src/store.ts | 2 +- 4 files changed, 333 insertions(+), 107 deletions(-) create mode 100644 frontend/src/shared/media.ts diff --git a/frontend/src/components/FunixFunction/FileUploadWidget.tsx b/frontend/src/components/FunixFunction/FileUploadWidget.tsx index c3178e24..880885df 100644 --- a/frontend/src/components/FunixFunction/FileUploadWidget.tsx +++ b/frontend/src/components/FunixFunction/FileUploadWidget.tsx @@ -1,6 +1,6 @@ import { WidgetProps } from "@rjsf/core"; import { DropzoneOptions, useDropzone } from "react-dropzone"; -import React, { useCallback, useEffect } from "react"; +import React, { useCallback, useEffect, useLayoutEffect } from "react"; import { Button, Dialog, @@ -20,11 +20,18 @@ import { TableRow, Typography, } from "@mui/material"; -import { Delete, FileUpload, Preview } from "@mui/icons-material"; +import { + CameraAlt, + Delete, + FileUpload, + KeyboardVoice, + Preview, +} from "@mui/icons-material"; import OutputMedias from "./OutputComponents/OutputMedias"; import { useAtom } from "jotai"; import { storeAtom } from "../../store"; import { enqueueSnackbar } from "notistack"; +import FunixRecorder from "../../shared/media"; interface FileUploadWidgetInterface { widget: WidgetProps; @@ -68,13 +75,37 @@ const base64stringToFile = (base64: string) => { }); }; +const CameraPreviewVideo = () => { + useLayoutEffect(() => { + const video = document.getElementById("videoPreview") as HTMLVideoElement; + navigator.mediaDevices + .getUserMedia({ video: true, audio: false }) + .then((stream) => { + video.srcObject = stream; + video.play().catch((err) => { + console.log(err); + }); + }) + .catch((err) => { + console.log(err); + }); + }); + return