diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 3f1a831a..d6e7ea2d 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -715,6 +715,7 @@ def funix( rate_limit: Limiter | list | dict = [], reactive: ReactiveType = None, print_to_web: bool = False, + autorun: bool = False, ): """ Decorator for functions to convert them to web apps @@ -752,6 +753,7 @@ def funix( 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 + autorun(bool): allow users to use continuity runs on the front end Returns: function: the decorated function @@ -1012,6 +1014,7 @@ def function_reactive_update(): "id": function_id, "websocket": need_websocket, "reactive": has_reactive_params, + "autorun": autorun, } ) diff --git a/frontend/src/components/FunixFunction/InputPanel.tsx b/frontend/src/components/FunixFunction/InputPanel.tsx index 4b25b942..e509c5fe 100644 --- a/frontend/src/components/FunixFunction/InputPanel.tsx +++ b/frontend/src/components/FunixFunction/InputPanel.tsx @@ -43,6 +43,18 @@ const InputPanel = (props: { const { enqueueSnackbar } = useSnackbar(); const [tempOutput, setTempOutput] = useState(null); + const [autoRun, setAutoRun] = useState(false); + + const isLarge = + Object.values(props.detail.schema.properties).findIndex((value) => { + const newValue = value as unknown as any; + const largeWidgets = ["image", "video", "audio", "file"]; + if ("items" in newValue) { + return largeWidgets.includes(newValue.items.widget); + } else { + return largeWidgets.includes(newValue.widget); + } + }) !== -1; useEffect(() => { setWaiting(() => !requestDone); @@ -78,36 +90,40 @@ const InputPanel = (props: { // console.log("Data changed: ", formData); setForm(formData); - if (!props.preview.reactive) { - return; - } - - _.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(); + if (props.preview.reactive) { + _.debounce(() => { + fetch(new URL(`/update/${props.preview.id}`, props.backend), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), }) - .then((data: UpdateResult) => { - const result = data.result; + .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, - }; - }); + if (result !== null) { + for (const [key, value] of Object.entries(result)) { + setForm((form) => { + return { + ...form, + [key]: value, + }; + }); + } } - } - }); - }, 100)(); + }); + }, 100)(); + } + + if (props.preview.autorun && autoRun) { + _.debounce(() => { + handleSubmitWithoutHistory().then(); + }, 100)(); + } }; const saveOutput = async ( @@ -148,9 +164,8 @@ const InputPanel = (props: { }, 300); }; - const handleSubmit = async () => { - const now = new Date().getTime(); - const newForm = props.preview.secret + const getNewForm = () => { + return props.preview.secret ? props.preview.name in functionSecret && functionSecret[props.preview.path] !== null ? { @@ -164,16 +179,48 @@ const InputPanel = (props: { } : form : form; - const isLarge = - Object.values(props.detail.schema.properties).findIndex((value) => { - const newValue = value as unknown as any; - const largeWidgets = ["image", "video", "audio", "file"]; - if ("items" in newValue) { - return largeWidgets.includes(newValue.items.widget); - } else { - return largeWidgets.includes(newValue.widget); - } - }) !== -1; + }; + + const getWebsocketUrl = () => { + return props.backend.protocol === "https:" + ? "wss" + : "ws" + "://" + props.backend.host + "/call/" + props.detail.id; + }; + + const handleSubmitWithoutHistory = async () => { + const newForm = getNewForm(); + setRequestDone(() => false); + checkResponse().then(); + if (props.preview.websocket) { + const socket = new WebSocket(getWebsocketUrl()); + socket.addEventListener("open", function () { + socket.send(JSON.stringify(newForm)); + }); + + socket.addEventListener("message", function (event) { + props.setResponse(() => event.data); + setTempOutput(() => event.data); + }); + + socket.addEventListener("close", async function () { + setWaiting(() => false); + setRequestDone(() => true); + }); + } else { + const response = await callFunctionRaw( + new URL(`/call/${props.detail.id}`, props.backend), + newForm + ); + const result = response.toString(); + props.setResponse(() => result); + setWaiting(() => false); + setRequestDone(() => true); + } + }; + + const handleSubmit = async () => { + const now = new Date().getTime(); + const newForm = getNewForm(); if (saveHistory && !isLarge) { try { @@ -193,11 +240,7 @@ const InputPanel = (props: { setRequestDone(() => false); checkResponse().then(); if (props.preview.websocket) { - const websocketUrl = - props.backend.protocol === "https:" - ? "wss" - : "ws" + "://" + props.backend.host + "/call/" + props.detail.id; - const socket = new WebSocket(websocketUrl); + const socket = new WebSocket(getWebsocketUrl()); socket.addEventListener("open", function () { socket.send(JSON.stringify(newForm)); }); @@ -265,7 +308,16 @@ const InputPanel = (props: { > } + control={ + { + setAutoRun(() => event.target.checked); + }} + disabled={!props.preview.autorun} + /> + } label="Continuously Run" /> diff --git a/frontend/src/shared/index.ts b/frontend/src/shared/index.ts index 25cfe7b2..bc3a1fa2 100644 --- a/frontend/src/shared/index.ts +++ b/frontend/src/shared/index.ts @@ -76,6 +76,10 @@ export type FunctionPreview = { * Is this function has reactive argument */ reactive: boolean; + /** + * autorun + */ + autorun: boolean; }; export type GetListResponse = {