refactor: custom api code generator and fix chat locking when saving (#3618)

* Removed saveLoading from the chat

* Fixed initialSetup that couldnt be null

* Added types for get codes type

* Added new types for getCodes and initialSetup on Tweaks store

* Created a use custom api code to return all code

* Passed the getCodes from the custom hook into initialSetup

* Updated tabs array to receive object

* Update every code to get correct props

* Update tweaks store to handle refactored getCodes and createTabsArray

* Added custom api generator component on top of the codetabscomponent
This commit is contained in:
Lucas Oliveira 2024-08-29 16:31:03 -03:00 • committed by GitHub
commit 32d51b69e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 178 additions and 128 deletions

View file

@ -0,0 +1,3 @@
export function CustomAPIGenerator() {
return <></>;
}

View file

@ -0,0 +1,19 @@
import {
getCurlRunCode,
getCurlWebhookCode,
} from "@/modals/apiModal/utils/get-curl-code";
import getJsApiCode from "@/modals/apiModal/utils/get-js-api-code";
import getPythonApiCode from "@/modals/apiModal/utils/get-python-api-code";
import getPythonCode from "@/modals/apiModal/utils/get-python-code";
import getWidgetCode from "@/modals/apiModal/utils/get-widget-code";
export function useCustomAPICode() {
return {
getCurlRunCode,
getCurlWebhookCode,
getJsApiCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
};
}

View file

@ -8,7 +8,6 @@ type ButtonSendWrapperProps = {
send: () => void; send: () => void;
lockChat: boolean; lockChat: boolean;
noInput: boolean; noInput: boolean;
saveLoading: boolean;
chatValue: string; chatValue: string;
files: FilePreviewType[]; files: FilePreviewType[];
}; };
@ -17,7 +16,6 @@ const ButtonSendWrapper = ({
send, send,
lockChat, lockChat,
noInput, noInput,
saveLoading,
chatValue, chatValue,
files, files,
}: ButtonSendWrapperProps) => { }: ButtonSendWrapperProps) => {
@ -31,15 +29,11 @@ const ButtonSendWrapper = ({
? "text-primary" ? "text-primary"
: "bg-chat-send text-background", : "bg-chat-send text-background",
)} )}
disabled={lockChat || saveLoading} disabled={lockChat}
onClick={(): void => send()} onClick={(): void => send()}
unstyled unstyled
> >
<Case <Case condition={lockChat || files.some((file) => file.loading)}>
condition={
lockChat || saveLoading || files.some((file) => file.loading)
}
>
<IconComponent <IconComponent
name="Lock" name="Lock"
className="form-modal-lock-icon" className="form-modal-lock-icon"
@ -57,8 +51,7 @@ const ButtonSendWrapper = ({
<Case <Case
condition={ condition={
!(lockChat || saveLoading || files.some((file) => file.loading)) && !(lockChat || files.some((file) => file.loading)) && !noInput
!noInput
} }
> >
<IconComponent <IconComponent

View file

@ -7,7 +7,6 @@ const TextAreaWrapper = ({
send, send,
lockChat, lockChat,
noInput, noInput,
saveLoading,
chatValue, chatValue,
setChatValue, setChatValue,
CHAT_INPUT_PLACEHOLDER, CHAT_INPUT_PLACEHOLDER,
@ -30,12 +29,11 @@ const TextAreaWrapper = ({
} }
}; };
const lockClass = const lockClass = lockChat
lockChat || saveLoading ? "form-modal-lock-true bg-input"
? "form-modal-lock-true bg-input" : noInput
: noInput ? "form-modal-no-input bg-input"
? "form-modal-no-input bg-input" : "form-modal-lock-false bg-background";
: "form-modal-lock-false bg-background";
const fileClass = const fileClass =
files.length > 0 files.length > 0
@ -45,10 +43,10 @@ const TextAreaWrapper = ({
const additionalClassNames = "form-modal-lockchat pl-14"; const additionalClassNames = "form-modal-lockchat pl-14";
useEffect(() => { useEffect(() => {
if (!lockChat && !noInput && !saveLoading) { if (!lockChat && !noInput) {
inputRef.current?.focus(); inputRef.current?.focus();
} }
}, [lockChat, noInput, saveLoading]); }, [lockChat, noInput]);
return ( return (
<Textarea <Textarea
@ -65,7 +63,7 @@ const TextAreaWrapper = ({
}} }}
rows={1} rows={1}
ref={inputRef} ref={inputRef}
disabled={lockChat || noInput || saveLoading} disabled={lockChat || noInput}
style={{ style={{
resize: "none", resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`, bottom: `${inputRef?.current?.scrollHeight}px`,
@ -76,7 +74,7 @@ const TextAreaWrapper = ({
: "hidden" : "hidden"
}`, }`,
}} }}
value={lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue} value={lockChat ? "Thinking..." : chatValue}
onChange={(event): void => { onChange={(event): void => {
setChatValue(event.target.value); setChatValue(event.target.value);
}} }}

View file

@ -32,8 +32,6 @@ export default function ChatInput({
setFiles, setFiles,
isDragging, isDragging,
}: ChatInputType): JSX.Element { }: ChatInputType): JSX.Element {
const [repeat, setRepeat] = useState(1);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [inputFocus, setInputFocus] = useState<boolean>(false); const [inputFocus, setInputFocus] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@ -128,7 +126,7 @@ export default function ChatInput({
const send = () => { const send = () => {
sendMessage({ sendMessage({
repeat, repeat: 1,
files: files.map((file) => file.path ?? "").filter((file) => file !== ""), files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
}); });
setFiles([]); setFiles([]);
@ -138,7 +136,6 @@ export default function ChatInput({
return ( return (
event.key === "Enter" && event.key === "Enter" &&
!lockChat && !lockChat &&
!saveLoading &&
!event.shiftKey && !event.shiftKey &&
!event.nativeEvent.isComposing !event.nativeEvent.isComposing
); );
@ -158,7 +155,6 @@ export default function ChatInput({
send={send} send={send}
lockChat={lockChat} lockChat={lockChat}
noInput={noInput} noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue} chatValue={chatValue}
setChatValue={setChatValue} setChatValue={setChatValue}
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER} CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
@ -173,7 +169,6 @@ export default function ChatInput({
send={send} send={send}
lockChat={lockChat} lockChat={lockChat}
noInput={noInput} noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue} chatValue={chatValue}
files={files} files={files}
/> />
@ -181,11 +176,11 @@ export default function ChatInput({
<div <div
className={`absolute bottom-2 left-4 ${ className={`absolute bottom-2 left-4 ${
lockChat || saveLoading ? "cursor-not-allowed" : "" lockChat ? "cursor-not-allowed" : ""
}`} }`}
> >
<UploadFileButton <UploadFileButton
lockChat={lockChat || saveLoading} lockChat={lockChat}
fileInputRef={fileInputRef} fileInputRef={fileInputRef}
handleFileChange={handleFileChange} handleFileChange={handleFileChange}
handleButtonClick={handleButtonClick} handleButtonClick={handleButtonClick}

View file

@ -1,3 +1,5 @@
import { CustomAPIGenerator } from "@/customization/components/custom-api-generator";
import { useCustomAPICode } from "@/customization/hooks/use-custom-api-code";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import "ace-builds/src-noconflict/ext-language_tools"; import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python"; import "ace-builds/src-noconflict/mode-python";
@ -33,8 +35,10 @@ export default function ApiModal({
const tabs = useTweaksStore((state) => state.tabs); const tabs = useTweaksStore((state) => state.tabs);
const initialSetup = useTweaksStore((state) => state.initialSetup); const initialSetup = useTweaksStore((state) => state.initialSetup);
const getCodes = useCustomAPICode();
useEffect(() => { useEffect(() => {
if (open) initialSetup(autoLogin, flow); if (open) initialSetup(autoLogin ?? false, flow, getCodes);
setActiveTab("0"); setActiveTab("0");
}, [open]); }, [open]);
@ -50,6 +54,7 @@ export default function ApiModal({
/> />
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content overflowHidden> <BaseModal.Content overflowHidden>
<CustomAPIGenerator />
<CodeTabsComponent <CodeTabsComponent
open={open} open={open}
tabs={tabs!} tabs={tabs!}

View file

@ -1,4 +1,5 @@
import useFlowStore from "@/stores/flowStore"; import useFlowStore from "@/stores/flowStore";
import { GetCodeType } from "@/types/tweaks";
/** /**
* Function to get the curl code for the API * Function to get the curl code for the API
@ -6,12 +7,12 @@ import useFlowStore from "@/stores/flowStore";
* @param {boolean} isAuth - If the API is authenticated * @param {boolean} isAuth - If the API is authenticated
* @returns {string} - The curl code * @returns {string} - The curl code
*/ */
export function getCurlRunCode( export function getCurlRunCode({
flowId: string, flowId,
isAuth: boolean, isAuth,
tweaksBuildedObject?: {}, tweaksBuildedObject,
endpointName?: string | null, endpointName,
): string { }: GetCodeType): string {
let tweaksString = "{}"; let tweaksString = "{}";
const inputs = useFlowStore.getState().inputs; const inputs = useFlowStore.getState().inputs;
const outputs = useFlowStore.getState().outputs; const outputs = useFlowStore.getState().outputs;
@ -43,11 +44,11 @@ export function getCurlRunCode(
* @param {string} options.endpointName - The name of the webhook endpoint. * @param {string} options.endpointName - The name of the webhook endpoint.
* @returns {string} The cURL command. * @returns {string} The cURL command.
*/ */
export function getCurlWebhookCode( export function getCurlWebhookCode({
flowId, flowId,
isAuth, isAuth,
endpointName?: string | null, endpointName,
) { }: GetCodeType) {
return `curl -X POST \\ return `curl -X POST \\
"${window.location.protocol}//${window.location.host}/api/v1/webhook/${ "${window.location.protocol}//${window.location.host}/api/v1/webhook/${
endpointName || flowId endpointName || flowId

View file

@ -1,3 +1,5 @@
import { GetCodeType } from "@/types/tweaks";
/** /**
* Function to generate JavaScript code for interfacing with an API using the LangflowClient class. * Function to generate JavaScript code for interfacing with an API using the LangflowClient class.
* @param {string} flowId - The id of the flow. * @param {string} flowId - The id of the flow.
@ -6,12 +8,12 @@
* @param {string} [endpointName] - Optional endpoint name. * @param {string} [endpointName] - Optional endpoint name.
* @returns {string} - The JavaScript code as a string. * @returns {string} - The JavaScript code as a string.
*/ */
export default function getJsApiCode( export default function getJsApiCode({
flowId: string, flowId,
isAuth: boolean, isAuth,
tweaksBuildedObject: {}, tweaksBuildedObject,
endpointName?: string | null, endpointName,
): string { }: GetCodeType): string {
let tweaksString = "{}"; let tweaksString = "{}";
if (tweaksBuildedObject) if (tweaksBuildedObject)
tweaksString = JSON.stringify(tweaksBuildedObject, null, 2); tweaksString = JSON.stringify(tweaksBuildedObject, null, 2);

View file

@ -1,3 +1,5 @@
import { GetCodeType } from "@/types/tweaks";
/** /**
* Function to get the python code for the API * Function to get the python code for the API
* @param {string} flowId - The id of the flow * @param {string} flowId - The id of the flow
@ -6,12 +8,11 @@
* @param {string} [endpointName] - The optional endpoint name * @param {string} [endpointName] - The optional endpoint name
* @returns {string} - The python code * @returns {string} - The python code
*/ */
export default function getPythonApiCode( export default function getPythonApiCode({
flowId: string, flowId,
isAuth: boolean, tweaksBuildedObject,
tweaksBuildedObject?: {}, endpointName,
endpointName?: string | null, }: GetCodeType): string {
): string {
let tweaksString = "{}"; let tweaksString = "{}";
if (tweaksBuildedObject) if (tweaksBuildedObject)
tweaksString = JSON.stringify(tweaksBuildedObject, null, 2) tweaksString = JSON.stringify(tweaksBuildedObject, null, 2)

View file

@ -1,13 +1,15 @@
import { GetCodeType } from "@/types/tweaks";
/** /**
* Function to get the python code for the API * Function to get the python code for the API
* @param {string} flow - The current flow * @param {string} flow - The current flow
* @param {any[]} tweaksBuildedObject - The tweaks * @param {any[]} tweaksBuildedObject - The tweaks
* @returns {string} - The python code * @returns {string} - The python code
*/ */
export default function getPythonCode( export default function getPythonCode({
flowName: string, flowName,
tweaksBuildedObject: {}, tweaksBuildedObject,
): string { }: GetCodeType): string {
let tweaksString = "{}"; let tweaksString = "{}";
if (tweaksBuildedObject) if (tweaksBuildedObject)
tweaksString = JSON.stringify(tweaksBuildedObject, null, 2) tweaksString = JSON.stringify(tweaksBuildedObject, null, 2)

View file

@ -1,13 +1,15 @@
import { GetCodeType } from "@/types/tweaks";
/** /**
* Function to get the widget code for the API * Function to get the widget code for the API
* @param {string} flow - The current flow. * @param {string} flow - The current flow.
* @returns {string} - The widget code * @returns {string} - The widget code
*/ */
export default function getWidgetCode( export default function getWidgetCode({
flowId: string, flowId,
flowName: string, flowName,
isAuth: boolean, isAuth,
): string { }: GetCodeType): string {
return `<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@v1.0.6/dist/build/static/js/bundle.min.js""></script> return `<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@v1.0.6/dist/build/static/js/bundle.min.js""></script>
<langflow-chat <langflow-chat

View file

@ -1,72 +1,78 @@
import { tabsArrayType } from "@/types/components"; import { tabsArrayType } from "@/types/components";
export function createTabsArray( export function createTabsArray(
codes, codes: { [key: string]: string },
includeWebhookCurl = false,
includeTweaks = false, includeTweaks = false,
) { ) {
const tabs: tabsArrayType[] = [ const tabs: tabsArrayType[] = [];
{ if (codes.runCurlCode) {
tabs.push({
name: "Run cURL", name: "Run cURL",
mode: "bash", mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png", image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh", language: "sh",
code: codes[0], code: codes.runCurlCode,
hasTweaks: includeTweaks, hasTweaks: includeTweaks,
}, });
{ }
if (codes.webhookCurlCode) {
tabs.push({
name: "Webhook cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes.webhookCurlCode,
});
}
if (codes.pythonApiCode) {
tabs.push({
name: "Python API", name: "Python API",
mode: "python", mode: "python",
image: image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w", "https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py", language: "py",
code: codes[2], code: codes.pythonApiCode,
hasTweaks: includeTweaks, hasTweaks: includeTweaks,
}, });
{ }
if (codes.jsApiCode) {
tabs.push({
name: "JS API", name: "JS API",
mode: "javascript", mode: "javascript",
image: "https://cdn-icons-png.flaticon.com/512/136/136530.png", image: "https://cdn-icons-png.flaticon.com/512/136/136530.png",
language: "js", language: "js",
code: codes[3], code: codes.jsApiCode,
hasTweaks: includeTweaks, hasTweaks: includeTweaks,
}, });
{ }
if (codes.pythonCode) {
tabs.push({
name: "Python Code", name: "Python Code",
mode: "python", mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png", image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py", language: "py",
code: codes[4], code: codes.pythonCode,
hasTweaks: includeTweaks, hasTweaks: includeTweaks,
}, });
{ }
if (codes.widgetCode) {
tabs.push({
name: "Chat Widget HTML", name: "Chat Widget HTML",
description: description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.", "Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html", mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png", image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "html", language: "html",
code: codes[5], code: codes.widgetCode,
},
];
if (includeWebhookCurl) {
tabs.splice(1, 0, {
name: "Webhook cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[1],
}); });
} }
if (includeTweaks) { if (includeTweaks) {
tabs.push({ tabs.push({
name: "Tweaks", name: "Tweaks",
mode: "python", mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png", image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py", language: "py",
code: codes[6], code: codes.tweaksCode,
}); });
} }

View file

@ -1,15 +1,8 @@
import { getChangesType } from "@/modals/apiModal/utils/get-changes-types"; import { getChangesType } from "@/modals/apiModal/utils/get-changes-types";
import {
getCurlRunCode,
getCurlWebhookCode,
} from "@/modals/apiModal/utils/get-curl-code";
import getJsApiCode from "@/modals/apiModal/utils/get-js-api-code";
import { getNodesWithDefaultValue } from "@/modals/apiModal/utils/get-nodes-with-default-value"; import { getNodesWithDefaultValue } from "@/modals/apiModal/utils/get-nodes-with-default-value";
import getPythonApiCode from "@/modals/apiModal/utils/get-python-api-code";
import getPythonCode from "@/modals/apiModal/utils/get-python-code";
import getWidgetCode from "@/modals/apiModal/utils/get-widget-code";
import { createTabsArray } from "@/modals/apiModal/utils/tabs-array"; import { createTabsArray } from "@/modals/apiModal/utils/tabs-array";
import { FlowType, NodeDataType } from "@/types/flow"; import { FlowType, NodeDataType } from "@/types/flow";
import { GetCodesType } from "@/types/tweaks";
import { customStringify } from "@/utils/reactflowUtils"; import { customStringify } from "@/utils/reactflowUtils";
import { create } from "zustand"; import { create } from "zustand";
import { TweaksStoreType } from "../types/zustand/tweaks"; import { TweaksStoreType } from "../types/zustand/tweaks";
@ -51,12 +44,18 @@ export const useTweaksStore = create<TweaksStoreType>((set, get) => ({
}, },
autoLogin: false, autoLogin: false,
flow: null, flow: null,
initialSetup: (autoLogin: boolean, flow: FlowType) => { getCodes: {},
initialSetup: (
autoLogin: boolean,
flow: FlowType,
getCodes: GetCodesType,
) => {
useFlowStore.getState().unselectAll(); useFlowStore.getState().unselectAll();
set({ set({
nodes: getNodesWithDefaultValue(flow?.data?.nodes ?? []), nodes: getNodesWithDefaultValue(flow?.data?.nodes ?? []),
autoLogin, autoLogin,
flow, flow,
getCodes,
}); });
get().refreshTabs(); get().refreshTabs();
}, },
@ -90,38 +89,40 @@ export const useTweaksStore = create<TweaksStoreType>((set, get) => ({
tweak[node.id] = currentTweak; tweak[node.id] = currentTweak;
} }
}); });
const codesObj = {};
const getCodes = get().getCodes;
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, tweak); const props = {
const runCurlCode = getCurlRunCode( flowId: flow?.id,
flow?.id, flowName: flow?.name,
autoLogin, isAuth: autoLogin,
tweak, tweaksBuildedObject: tweak,
flow?.endpoint_name, endpointName: flow?.endpoint_name,
); };
const jsApiCode = getJsApiCode(
flow?.id, if (getCodes) {
autoLogin, if (getCodes.getCurlRunCode) {
tweak, codesObj["runCurlCode"] = getCodes.getCurlRunCode(props);
flow?.endpoint_name, }
); if (getCodes.getCurlWebhookCode && !!flow.webhook) {
const webhookCurlCode = getCurlWebhookCode( codesObj["webhookCurlCode"] = getCodes.getCurlWebhookCode(props);
flow?.id, }
autoLogin, if (getCodes.getJsApiCode) {
flow?.endpoint_name, codesObj["jsApiCode"] = getCodes.getJsApiCode(props);
); }
const pythonCode = getPythonCode(flow?.name, tweak); if (getCodes.getPythonApiCode) {
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin); codesObj["pythonApiCode"] = getCodes.getPythonApiCode(props);
}
if (getCodes.getPythonCode) {
codesObj["pythonCode"] = getCodes.getPythonCode(props);
}
if (getCodes.getWidgetCode) {
codesObj["widgetCode"] = getCodes.getWidgetCode(props);
}
}
const codesArray = [
runCurlCode,
webhookCurlCode,
pythonApiCode,
jsApiCode,
pythonCode,
widgetCode,
];
set({ set({
tabs: createTabsArray(codesArray, !!flow.webhook, nodes.length > 0), tabs: createTabsArray(codesObj, nodes.length > 0),
}); });
}, },
})); }));

View file

@ -0,0 +1,16 @@
export type GetCodesType = {
getCurlRunCode?: (GetCodeType) => string;
getCurlWebhookCode?: (GetCodeType) => string;
getJsApiCode?: (GetCodeType) => string;
getPythonApiCode?: (GetCodeType) => string;
getPythonCode?: (GetCodeType) => string;
getWidgetCode?: (GetCodeType) => string;
};
export type GetCodeType = {
flowId: string;
flowName: string;
isAuth: boolean;
tweaksBuildedObject: {};
endpointName?: string | null;
};

View file

@ -1,4 +1,5 @@
import { FlowType, NodeType } from "@/types/flow"; import { FlowType, NodeType } from "@/types/flow";
import { GetCodesType } from "@/types/tweaks";
import { tabsArrayType } from "../../components"; import { tabsArrayType } from "../../components";
export type TweaksStoreType = { export type TweaksStoreType = {
@ -13,9 +14,14 @@ export type TweaksStoreType = {
id: string, id: string,
update: NodeType | ((oldState: NodeType) => NodeType), update: NodeType | ((oldState: NodeType) => NodeType),
) => void; ) => void;
getCodes: GetCodesType;
getNode: (id: string) => NodeType | undefined; getNode: (id: string) => NodeType | undefined;
tabs: tabsArrayType[]; tabs: tabsArrayType[];
initialSetup: (autoLogin: boolean, flow: FlowType) => void; initialSetup: (
autoLogin: boolean,
flow: FlowType,
getCodes: GetCodesType,
) => void;
refreshTabs: () => void; refreshTabs: () => void;
autoLogin: boolean; autoLogin: boolean;
flow: FlowType | null; flow: FlowType | null;