Turn AlertModal into a Provider with useContext

This commit is contained in:
Ian Arawjo 2024-03-11 10:34:43 -04:00
parent 8d24218d63
commit ce282459ca
14 changed files with 301 additions and 186 deletions

View File

@ -268,13 +268,13 @@ def index():
def executepy():
"""
Executes a Python function sent from JavaScript,
over all the `StandardizedLLMResponse` objects passed in from the front-end.
over all the `LLMResponse` objects passed in from the front-end.
POST'd data should be in the form:
{
'id': # a unique ID to refer to this information. Used when cache'ing responses.
'code': str, # the body of the lambda function to evaluate, in form: lambda responses: <body>
'responses': List[StandardizedLLMResponse] # the responses to run on.
'responses': List[LLMResponse] # the responses to run on.
'scope': 'response' | 'batch' # the scope of responses to run on --a single response, or all across each batch.
# If batch, evaluator has access to 'responses'. Only matters if n > 1 for each prompt.
'process_type': 'evaluator' | 'processor' # the type of processing to perform. Evaluators only 'score'/annotate responses. Processors change responses (e.g. text).

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef, useState } from "react";
import React, { useCallback, useContext, useMemo, useState } from "react";
import {
Stack,
NumberInput,
@ -18,7 +18,7 @@ import {
getAIFeaturesModels,
} from "./backend/ai";
import { IconSparkles, IconAlertCircle } from "@tabler/icons-react";
import AlertModal, { AlertModalRef } from "./AlertModal";
import { AlertModalContext } from "./AlertModal";
import useStore from "./store";
import {
INFO_CODEBLOCK_JS,
@ -30,7 +30,7 @@ import { queryLLM } from "./backend/backend";
import { splitText } from "./SplitNode";
import { escapeBraces } from "./backend/template";
import { cleanMetavarsFilterFunc } from "./backend/utils";
import { Dict, TemplateVarInfo, VarsContext } from "./backend/typing";
import { VarsContext } from "./backend/typing";
const zeroGap = { gap: "0rem" };
const popoverShadow = "rgb(38, 57, 77) 0px 10px 30px -14px";
@ -255,7 +255,7 @@ export function AIGenReplaceItemsPopover({
const aiFeaturesProvider = useStore((state) => state.aiFeaturesProvider);
// Alerts
const alertModal = useRef<AlertModalRef>(null);
const showAlert = useContext(AlertModalContext);
// Command Fill state
const [commandFillNumber, setCommandFillNumber] = useState<number>(3);
@ -298,7 +298,7 @@ export function AIGenReplaceItemsPopover({
if (e instanceof AIError) {
setDidCommandFillError(true);
} else {
if (alertModal.current) alertModal.current.trigger(e?.message);
if (showAlert) showAlert(e?.message);
else console.error(e);
}
})
@ -321,7 +321,7 @@ export function AIGenReplaceItemsPopover({
console.log(e);
setDidGenerateAndReplaceError(true);
} else {
if (alertModal.current) alertModal.current.trigger(e?.message);
if (showAlert) showAlert(e?.message);
else console.error(e);
}
})
@ -470,7 +470,6 @@ export function AIGenReplaceItemsPopover({
{replaceUI}
</Tabs.Panel>
</Tabs>
<AlertModal ref={alertModal} />
</AIPopover>
);
}
@ -508,7 +507,7 @@ export function AIGenCodeEvaluatorPopover({
const [awaitingResponse, setAwaitingResponse] = useState(false);
// Alerts
const alertModal = useRef<AlertModalRef>(null);
const showAlert = useContext(AlertModalContext);
const [didEncounterError, setDidEncounterError] = useState(false);
// Handle errors
@ -518,9 +517,9 @@ export function AIGenCodeEvaluatorPopover({
if (onLoadingChange) onLoadingChange(false);
setDidEncounterError(true);
if (typeof err !== "string") console.error(err);
alertModal.current?.trigger(typeof err === "string" ? err : err?.message);
if (showAlert) showAlert(typeof err === "string" ? err : err?.message);
},
[setAwaitingResponse, onLoadingChange, setDidEncounterError, alertModal],
[setAwaitingResponse, onLoadingChange, setDidEncounterError, showAlert],
);
// Generate an evaluate function, given the user-specified prompt, in the proper programming language
@ -731,7 +730,6 @@ ${currentEvalCode}
</Button>
</Tabs.Panel>
</Tabs>
<AlertModal ref={alertModal} />
</AIPopover>
);
}

View File

@ -1,5 +1,12 @@
/** An alert popup for displaying errors */
import React, { useState, forwardRef, useImperativeHandle } from "react";
import React, {
useState,
forwardRef,
useImperativeHandle,
createContext,
useRef,
useMemo,
} from "react";
import { useDisclosure } from "@mantine/hooks";
import { Modal, ModalBaseStylesNames, Styles } from "@mantine/core";
@ -9,35 +16,69 @@ const ALERT_MODAL_STYLE = {
} as Styles<ModalBaseStylesNames>;
export interface AlertModalRef {
trigger: (msg?: string) => void;
trigger: (msg?: string | Error) => void;
}
const AlertModal = forwardRef<AlertModalRef>(function AlertModal(props, ref) {
// Mantine modal popover for alerts
const [opened, { open, close }] = useDisclosure(false);
const [alertMsg, setAlertMsg] = useState("");
/**
* The Alert Modal displays error messages to the user in a pop-up dialog.
*/
export const AlertModal = forwardRef<AlertModalRef>(
function AlertModal(props, ref) {
// Mantine modal popover for alerts
const [opened, { open, close }] = useDisclosure(false);
const [alertMsg, setAlertMsg] = useState("");
// This gives the parent access to triggering the modal alert
const trigger = (msg?: string) => {
if (!msg) msg = "Unknown error.";
console.error(msg);
setAlertMsg(msg);
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
// This gives the parent access to triggering the modal alert
const trigger = (msg?: string | Error) => {
if (!msg) msg = "Unknown error.";
else if (typeof msg !== "string") msg = msg.message;
console.error(msg);
setAlertMsg(msg);
open();
};
useImperativeHandle(ref, () => ({
trigger,
}));
return (
<Modal
opened={opened}
onClose={close}
title="Error"
styles={ALERT_MODAL_STYLE}
>
<p style={{ whiteSpace: "pre-line" }}>{alertMsg}</p>
</Modal>
);
},
);
export default AlertModal;
export const AlertModalContext = createContext<
((msg?: string | Error) => void) | undefined
>(undefined);
/**
* Wraps children components to provide the same AlertModal to everywhere in the component tree.
* Saves space and reduces duplicate declarations.
*/
export const AlertModalProvider = ({
children,
}: {
children: React.ReactNode[];
}) => {
// Create one AlertModal for the entire application
const alertModal = useRef<AlertModalRef>(null);
// We have to wrap trigger() in a memoized function, as passing it down directly will trigger re-renders every frame.
const showAlert = useMemo(() => {
return (msg?: string | Error) => alertModal?.current?.trigger(msg);
}, [alertModal]);
return (
<Modal
opened={opened}
onClose={close}
title="Error"
styles={ALERT_MODAL_STYLE}
>
<p style={{ whiteSpace: "pre-line" }}>{alertMsg}</p>
</Modal>
<AlertModalContext.Provider value={showAlert}>
<AlertModal ref={alertModal} />
{children}
</AlertModalContext.Provider>
);
});
export default AlertModal;
};

View File

@ -1,4 +1,10 @@
import React, { useState, useCallback, useRef, useEffect } from "react";
import React, {
useState,
useCallback,
useRef,
useEffect,
useContext,
} from "react";
import ReactFlow, { Controls, Background, ReactFlowInstance } from "reactflow";
import {
Button,
@ -31,13 +37,15 @@ import CodeEvaluatorNode from "./CodeEvaluatorNode";
import VisNode from "./VisNode";
import InspectNode from "./InspectorNode";
import ScriptNode from "./ScriptNode";
import AlertModal, { AlertModalRef } from "./AlertModal";
import { AlertModalProvider, AlertModalContext } from "./AlertModal";
import ItemsNode from "./ItemsNode";
import TabularDataNode from "./TabularDataNode";
import JoinNode from "./JoinNode";
import SplitNode from "./SplitNode";
import CommentNode from "./CommentNode";
import GlobalSettingsModal, { GlobalSettingsModalRef } from "./GlobalSettingsModal";
import GlobalSettingsModal, {
GlobalSettingsModalRef,
} from "./GlobalSettingsModal";
import ExampleFlowsModal, { ExampleFlowsModalRef } from "./ExampleFlowsModal";
import AreYouSureModal, { AreYouSureModalRef } from "./AreYouSureModal";
import LLMEvaluatorNode from "./LLMEvalNode";
@ -60,7 +68,13 @@ import useStore, { StoreHandles } from "./store";
import StorageCache from "./backend/cache";
import { APP_IS_RUNNING_LOCALLY, browserTabIsActive } from "./backend/utils";
import { Dict, JSONCompatible, LLMSpec } from "./backend/typing";
import { exportCache, fetchEnvironAPIKeys, fetchExampleFlow, fetchOpenAIEval, importCache } from "./backend/backend";
import {
exportCache,
fetchEnvironAPIKeys,
fetchExampleFlow,
fetchOpenAIEval,
importCache,
} from "./backend/backend";
// Device / Browser detection
import {
@ -217,7 +231,9 @@ const App = () => {
// For saving / loading
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [autosavingInterval, setAutosavingInterval] = useState<NodeJS.Timeout | undefined>(undefined);
const [autosavingInterval, setAutosavingInterval] = useState<
NodeJS.Timeout | undefined
>(undefined);
// For 'share' button
const clipboard = useClipboard({ timeout: 1500 });
@ -232,9 +248,16 @@ const App = () => {
// For an info pop-up that welcomes new users
// const [welcomeModalOpened, { open: openWelcomeModal, close: closeWelcomeModal }] = useDisclosure(false);
// For displaying alerts
const showAlert = useContext(AlertModalContext);
// For confirmation popup
const confirmationModal = useRef<AreYouSureModalRef>(null);
const [confirmationDialogProps, setConfirmationDialogProps] = useState<{title: string, message: string, onConfirm?: () => void}>({
const [confirmationDialogProps, setConfirmationDialogProps] = useState<{
title: string;
message: string;
onConfirm?: () => void;
}>({
title: "Confirm action",
message: "Are you sure?",
});
@ -266,13 +289,22 @@ const App = () => {
return { x: -(x / zoom) + centerX / zoom, y: -(y / zoom) + centerY / zoom };
};
const addNode = (id: string, type?: string, data?: Dict, offsetX?: number, offsetY?: number) => {
const addNode = (
id: string,
type?: string,
data?: Dict,
offsetX?: number,
offsetY?: number,
) => {
const { x, y } = getViewportCenter();
addNodeToStore({
id: `${id}-` + Date.now(),
type: type ?? id,
data: data ?? {},
position: { x: x - 200 + (offsetX ? offsetX : 0), y: y - 100 + (offsetY ? offsetY : 0)},
position: {
x: x - 200 + (offsetX || 0),
y: y - 100 + (offsetY || 0),
},
});
};
@ -317,7 +349,7 @@ const App = () => {
const msg = typeof err === "string" ? err : err.message;
setIsLoading(false);
setWaitingForShare(false);
if (alertModal.current) alertModal.current.trigger(msg);
if (showAlert) showAlert(msg);
console.error(msg);
};
@ -529,39 +561,44 @@ const App = () => {
input.accept = ".cforge, .json";
// Handle file selection
// @ts-expect-error The event is correctly typed here, but for some reason TS doesn't pick up on it.
input.addEventListener("change", function (event: React.ChangeEvent<HTMLInputElement>) {
// Start loading spinner
setIsLoading(false);
input.addEventListener(
"change",
// @ts-expect-error The event is correctly typed here, but for some reason TS doesn't pick up on it.
function (event: React.ChangeEvent<HTMLInputElement>) {
// Start loading spinner
setIsLoading(false);
const files = event.target.files;
if (!files || !Array.isArray(files) || files.length === 0) {
console.error("No files found to load.");
return;
}
const file = files[0];
const reader = new window.FileReader();
// Handle file load event
reader.addEventListener("load", function () {
try {
if (typeof reader.result !== "string")
throw new Error("File could not be read: Unknown format or empty.");
// We try to parse the JSON response
const flow_and_cache = JSON.parse(reader.result);
// Import it to React Flow and import cache data on the backend
importFlowFromJSON(flow_and_cache);
} catch (error) {
handleError(error as Error);
const files = event.target.files;
if (!files || !Array.isArray(files) || files.length === 0) {
console.error("No files found to load.");
return;
}
});
// Read the selected file as text
reader.readAsText(file);
});
const file = files[0];
const reader = new window.FileReader();
// Handle file load event
reader.addEventListener("load", function () {
try {
if (typeof reader.result !== "string")
throw new Error(
"File could not be read: Unknown format or empty.",
);
// We try to parse the JSON response
const flow_and_cache = JSON.parse(reader.result);
// Import it to React Flow and import cache data on the backend
importFlowFromJSON(flow_and_cache);
} catch (error) {
handleError(error as Error);
}
});
// Read the selected file as text
reader.readAsText(file);
},
);
// Trigger the file selector
input.click();
@ -571,9 +608,7 @@ const App = () => {
const importFlowFromOpenAIEval = (evalname: string) => {
setIsLoading(true);
fetchOpenAIEval(evalname)
.then(importFlowFromJSON)
.catch(handleError);
fetchOpenAIEval(evalname).then(importFlowFromJSON).catch(handleError);
};
// Load flow from examples modal
@ -863,9 +898,8 @@ const App = () => {
);
} else
return (
<div>
<GlobalSettingsModal ref={settingsModal} alertModal={alertModal} />
<AlertModal ref={alertModal} />
<AlertModalProvider>
<GlobalSettingsModal ref={settingsModal} />
<LoadingOverlay visible={isLoading} overlayBlur={1} />
<ExampleFlowsModal
ref={examplesModal}
@ -1189,7 +1223,7 @@ const App = () => {
Send us feedback
</a>
</div>
</div>
</AlertModalProvider>
);
};

View File

@ -6,6 +6,7 @@ import React, {
useMemo,
forwardRef,
useImperativeHandle,
useContext,
} from "react";
import { Handle, NodeProps, Position } from "reactflow";
import {
@ -58,7 +59,7 @@ import {
} from "./backend/typing";
import { Status } from "./StatusIndicatorComponent";
import { executejs, executepy, grabResponses } from "./backend/backend";
import { AlertModalRef } from "./AlertModal";
import { AlertModalContext } from "./AlertModal";
// Whether we are running on localhost or not, and hence whether
// we have access to the Flask backend for, e.g., Python code evaluation.
@ -387,7 +388,7 @@ const CodeEvaluatorNode: React.FC<CodeEvaluatorNodeProps> = ({
const [lastContext, setLastContext] = useState<VarsContext>({});
// For displaying error messages to user
const alertModal = useRef<AlertModalRef>(null);
const showAlert = useContext(AlertModalContext);
// For an info pop-up that explains the type of ResponseInfo
const [infoModalOpened, { open: openInfoModal, close: closeInfoModal }] =
@ -413,9 +414,9 @@ const CodeEvaluatorNode: React.FC<CodeEvaluatorNodeProps> = ({
setStatus(Status.ERROR);
setLastRunSuccess(false);
if (typeof err !== "string") console.error(err);
alertModal.current?.trigger(typeof err === "string" ? err : err?.message);
if (showAlert) showAlert(typeof err === "string" ? err : err?.message);
},
[alertModal],
[showAlert],
);
const pullInputs = useCallback(() => {
@ -788,7 +789,6 @@ The Python interpeter in the browser is Pyodide. You may not be able to run some
onEdit={hideStatusIndicator}
icon={<IconTerminal size="16px" />}
status={status}
alertModal={alertModal}
handleRunClick={handleRunClick}
runButtonTooltip={run_tooltip}
customButtons={customButtons}

View File

@ -4,6 +4,7 @@ import React, {
useImperativeHandle,
useCallback,
useEffect,
useContext,
} from "react";
import {
TextInput,
@ -34,12 +35,15 @@ import {
import { Dropzone, FileWithPath } from "@mantine/dropzone";
import useStore from "./store";
import { APP_IS_RUNNING_LOCALLY } from "./backend/utils";
import fetch_from_backend from "./fetch_from_backend";
import { setCustomProviders } from "./ModelSettingSchemas";
import { getAIFeaturesModelProviders } from "./backend/ai";
import { CustomLLMProviderSpec, Dict } from "./backend/typing";
import { initCustomProvider, loadCachedCustomProviders, removeCustomProvider } from "./backend/backend";
import { AlertModalRef } from "./AlertModal";
import {
initCustomProvider,
loadCachedCustomProviders,
removeCustomProvider,
} from "./backend/backend";
import { AlertModalContext } from "./AlertModal";
const _LINK_STYLE = { color: "#1E90FF", textDecoration: "none" };
@ -47,7 +51,10 @@ const _LINK_STYLE = { color: "#1E90FF", textDecoration: "none" };
let LOADED_CUSTOM_PROVIDERS = false;
// Read a file as text and pass the text to a cb (callback) function
const read_file = (file: FileWithPath, cb: (contents: string | ArrayBuffer | null) => void) => {
const read_file = (
file: FileWithPath,
cb: (contents: string | ArrayBuffer | null) => void,
) => {
const reader = new window.FileReader();
reader.onload = function (event) {
const fileContent = event.target?.result;
@ -67,7 +74,9 @@ interface CustomProviderScriptDropzoneProps {
/** A Dropzone to load a Python `.py` script that registers a `CustomModelProvider` in the Flask backend.
* If successful, the list of custom model providers in the ChainForge UI dropdown is updated.
* */
const CustomProviderScriptDropzone: React.FC<CustomProviderScriptDropzoneProps> = ({ onError, onSetProviders }) => {
const CustomProviderScriptDropzone: React.FC<
CustomProviderScriptDropzoneProps
> = ({ onError, onSetProviders }) => {
const theme = useMantineTheme();
const [isLoading, setIsLoading] = useState(false);
@ -106,9 +115,7 @@ const CustomProviderScriptDropzone: React.FC<CustomProviderScriptDropzoneProps>
onReject={(files) => console.log("rejected files", files)}
maxSize={3 * 1024 ** 2}
>
<Flex
style={{ minHeight: rem(80), pointerEvents: "none" }}
>
<Flex style={{ minHeight: rem(80), pointerEvents: "none" }}>
<Center>
<Dropzone.Accept>
<IconUpload
@ -151,25 +158,19 @@ export interface GlobalSettingsModalRef {
trigger: () => void;
}
export interface GlobalSettingsModalProps {
alertModal?: React.RefObject<AlertModalRef>;
}
const GlobalSettingsModal = forwardRef<GlobalSettingsModalRef, GlobalSettingsModalProps>(
const GlobalSettingsModal = forwardRef<GlobalSettingsModalRef, object>(
function GlobalSettingsModal(props, ref) {
const [opened, { open, close }] = useDisclosure(false);
const setAPIKeys = useStore((state) => state.setAPIKeys);
const getFlag = useStore((state) => state.getFlag);
const setFlag = useStore((state) => state.setFlag);
const AvailableLLMs = useStore((state) => state.AvailableLLMs);
const aiFeaturesProvider = useStore((state) => state.aiFeaturesProvider);
const setAvailableLLMs = useStore((state) => state.setAvailableLLMs);
const nodes = useStore((state) => state.nodes);
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
const alertModal = props?.alertModal;
const setAIFeaturesProvider = useStore(
(state) => state.setAIFeaturesProvider,
);
const aiFeaturesProvider = useStore((state) => state.aiFeaturesProvider);
const showAlert = useContext(AlertModalContext);
const [aiSupportActive, setAISupportActive] = useState<boolean>(
getFlag("aiSupport") as boolean,
@ -204,12 +205,14 @@ const GlobalSettingsModal = forwardRef<GlobalSettingsModalRef, GlobalSettingsMod
const handleError = useCallback(
(err: string | Error) => {
const msg = typeof err === "string" ? err : err.message;
if (alertModal && alertModal.current) alertModal.current.trigger(msg);
if (showAlert) showAlert(msg);
},
[alertModal],
[showAlert],
);
const [customProviders, setLocalCustomProviders] = useState<CustomLLMProviderSpec[]>([]);
const [customProviders, setLocalCustomProviders] = useState<
CustomLLMProviderSpec[]
>([]);
const refreshLLMProviderLists = useCallback(() => {
// We unfortunately have to force all prompt/chat nodes to refresh their LLM lists, bc
// apparently the update to the AvailableLLMs list is not immediately propagated to them.
@ -223,7 +226,8 @@ const GlobalSettingsModal = forwardRef<GlobalSettingsModalRef, GlobalSettingsMod
const handleRemoveCustomProvider = useCallback(
(name: string) => {
removeCustomProvider(name).then(() => {
removeCustomProvider(name)
.then(() => {
// Successfully deleted the custom provider from backend;
// now updated the front-end UI to reflect this:
setAvailableLLMs(AvailableLLMs.filter((p) => p.name !== name));
@ -243,13 +247,13 @@ const GlobalSettingsModal = forwardRef<GlobalSettingsModalRef, GlobalSettingsMod
LOADED_CUSTOM_PROVIDERS = true;
// Is running locally; try to load any custom providers.
// Soft fails if it encounters error:
loadCachedCustomProviders().then(
(providers) => {
loadCachedCustomProviders()
.then((providers) => {
// Success; pass custom providers list to store:
setCustomProviders(providers);
setLocalCustomProviders(providers);
},
).catch(console.error);
})
.catch(console.error);
}
}, []);

View File

@ -5,6 +5,7 @@ import React, {
useEffect,
forwardRef,
useImperativeHandle,
useContext,
} from "react";
import { Handle } from "reactflow";
import { Group, NativeSelect, Progress, Text, Textarea } from "@mantine/core";
@ -20,6 +21,7 @@ import LLMResponseInspectorModal from "./LLMResponseInspectorModal";
import InspectFooter from "./InspectFooter";
import LLMResponseInspectorDrawer from "./LLMResponseInspectorDrawer";
import { stripLLMDetailsFromResponses } from "./backend/utils";
import { AlertModalContext } from "./AlertModal";
// The default prompt shown in gray highlights to give people a good example of an evaluation prompt.
const PLACEHOLDER_PROMPT =
@ -211,7 +213,7 @@ const LLMEvaluatorNode = ({ data, id }) => {
const llmEvaluatorRef = useRef(null);
const [status, setStatus] = useState("none");
const alertModal = useRef(null);
const showAlert = useContext(AlertModalContext);
const inspectModal = useRef(null);
// eslint-disable-next-line
@ -243,7 +245,7 @@ const LLMEvaluatorNode = ({ data, id }) => {
setStatus("error");
setProgress(undefined);
if (typeof err !== "string") console.error(err);
alertModal.current.trigger(typeof err === "string" ? err : err?.message);
if (showAlert) showAlert(typeof err === "string" ? err : err?.message);
};
// Fetch info about the number of queries we'll need to make
@ -296,7 +298,7 @@ const LLMEvaluatorNode = ({ data, id }) => {
pingOutputNodes,
setStatus,
showDrawer,
alertModal,
showAlert,
]);
const showResponseInspector = useCallback(() => {
@ -334,7 +336,6 @@ const LLMEvaluatorNode = ({ data, id }) => {
nodeId={id}
icon={<IconRobot size="16px" />}
status={status}
alertModal={alertModal}
handleRunClick={handleRunClick}
runButtonTooltip="Run scorer over inputs"
/>

View File

@ -4,13 +4,13 @@ import React, {
useState,
useEffect,
useCallback,
useContext,
} from "react";
import { Tooltip } from "@mantine/core";
import { EditText, onSaveProps } from "react-edit-text";
import "react-edit-text/dist/index.css";
import useStore from "./store";
import StatusIndicator, { Status } from "./StatusIndicatorComponent";
import AlertModal, { AlertModalRef } from "./AlertModal";
import AreYouSureModal, { AreYouSureModalRef } from "./AreYouSureModal";
export interface NodeLabelProps {
@ -22,7 +22,6 @@ export interface NodeLabelProps {
editable?: boolean;
status?: Status;
isRunning?: boolean;
alertModal?: React.Ref<AlertModalRef>;
customButtons?: React.ReactElement[];
handleRunClick?: () => void;
handleStopClick?: (nodeId: string) => void;
@ -45,7 +44,6 @@ export const NodeLabel: React.FC<NodeLabelProps> = ({
editable,
status,
isRunning,
alertModal,
customButtons,
handleRunClick,
handleStopClick,
@ -167,7 +165,6 @@ export const NodeLabel: React.FC<NodeLabelProps> = ({
readonly={editable !== undefined ? !editable : false}
/>
{statusIndicator}
<AlertModal ref={alertModal} />
<div className="node-header-btns-container">
{customButtons ?? <></>}
{isRunning ? stopButton : runButton}

View File

@ -4,6 +4,7 @@ import React, {
useRef,
useCallback,
useMemo,
useContext,
} from "react";
import { Handle, Position } from "reactflow";
import { v4 as uuid } from "uuid";
@ -51,7 +52,7 @@ import {
LLMResponse,
TemplateVarInfo,
} from "./backend/typing";
import { AlertModalRef } from "./AlertModal";
import { AlertModalContext } from "./AlertModal";
import { Status } from "./StatusIndicatorComponent";
const getUniqueLLMMetavarKey = (responses: LLMResponse[]) => {
@ -167,6 +168,8 @@ const PromptNode = ({ data, id, type: node_type }) => {
[node_type],
);
console.log("re-render");
// Get state from the Zustand store:
const edges = useStore((state) => state.edges);
const pullInputData = useStore((state) => state.pullInputData);
@ -199,7 +202,7 @@ const PromptNode = ({ data, id, type: node_type }) => {
const [llmItemsCurrState, setLLMItemsCurrState] = useState<LLMSpec[]>([]);
// For displaying error messages to user
const alertModal = useRef<AlertModalRef | null>(null);
const showAlert = useContext(AlertModalContext);
// For a way to inspect responses without having to attach a dedicated node
const inspectModal = useRef(null);
@ -241,9 +244,9 @@ const PromptNode = ({ data, id, type: node_type }) => {
(msg: string) => {
setProgress(undefined);
llmListContainer?.current?.resetLLMItemsProgress();
alertModal?.current?.trigger(msg);
if (showAlert) showAlert(msg);
},
[llmListContainer, alertModal],
[llmListContainer, showAlert],
);
const showResponseInspector = useCallback(() => {
@ -313,7 +316,6 @@ const PromptNode = ({ data, id, type: node_type }) => {
const pulled_data = pullInputData(templateVars, id);
updateShowContToggle(pulled_data);
} catch (err) {
// alertModal.current?.trigger(err.message);
console.error(err);
}
}, [templateVars, id, pullInputData, updateShowContToggle]);
@ -705,7 +707,7 @@ Soft failing by replacing undefined with empty strings.`,
// Try to pull inputs
pulled_data = pullInputData(templateVars, id);
} catch (err) {
alertModal.current?.trigger((err as Error)?.message ?? err);
if (showAlert) showAlert((err as Error)?.message ?? err);
console.error(err);
return; // early exit
}
@ -923,10 +925,11 @@ Soft failing by replacing undefined with empty strings.`,
"\n";
});
// We trigger the alert directly (don't use triggerAlert) here because we want to keep the progress bar:
alertModal?.current?.trigger(
"Errors collecting responses. Re-run prompt node to retry.\n\n" +
combined_err_msg,
);
if (showAlert)
showAlert(
"Errors collecting responses. Re-run prompt node to retry.\n\n" +
combined_err_msg,
);
return;
}
@ -1039,7 +1042,6 @@ Soft failing by replacing undefined with empty strings.`,
icon={node_icon}
status={status}
isRunning={status === "loading"}
alertModal={alertModal}
handleRunClick={handleRunClick}
handleStopClick={handleStopClick}
handleRunHover={handleRunHover}

View File

@ -1,4 +1,10 @@
import React, { useState, useCallback, useEffect, useRef } from "react";
import React, {
useState,
useCallback,
useEffect,
useRef,
useContext,
} from "react";
import { Handle } from "reactflow";
import {
NativeSelect,
@ -29,6 +35,7 @@ import {
toStandardResponseFormat,
} from "./backend/utils";
import LLMResponseInspectorDrawer from "./LLMResponseInspectorDrawer";
import { AlertModalContext } from "./AlertModal";
const createJSEvalCodeFor = (responseFormat, operation, value, valueType) => {
let responseObj = "r.text";
@ -80,7 +87,7 @@ const SimpleEvalNode = ({ data, id }) => {
const [pastInputs, setPastInputs] = useState([]);
const [status, setStatus] = useState("none");
const alertModal = useRef(null);
const showAlert = useContext(AlertModalContext);
const inspectModal = useRef(null);
// eslint-disable-next-line
@ -156,7 +163,7 @@ const SimpleEvalNode = ({ data, id }) => {
const rejected = (err_msg) => {
setStatus("error");
alertModal.current.trigger(err_msg);
if (showAlert) showAlert(err_msg);
};
// Generate JS code for the user's spec
@ -200,7 +207,7 @@ const SimpleEvalNode = ({ data, id }) => {
handlePullInputs,
pingOutputNodes,
setStatus,
alertModal,
showAlert,
status,
varValue,
varValueType,
@ -260,7 +267,6 @@ const SimpleEvalNode = ({ data, id }) => {
nodeId={id}
icon={<IconRuler2 size="16px" />}
status={status}
alertModal={alertModal}
handleRunClick={handleRunClick}
runButtonTooltip="Run evaluator over inputs"
/>

View File

@ -1,4 +1,10 @@
import React, { useState, useRef, useEffect, useCallback } from "react";
import React, {
useState,
useRef,
useEffect,
useCallback,
useContext,
} from "react";
import { Menu, NumberInput, Switch, Text, Tooltip } from "@mantine/core";
import EditableTable from "./EditableTable";
import * as XLSX from "xlsx";
@ -12,7 +18,7 @@ import {
import TemplateHooks from "./TemplateHooksComponent";
import BaseNode from "./BaseNode";
import NodeLabel from "./NodeLabelComponent";
import AlertModal, { AlertModalRef } from "./AlertModal";
import { AlertModalContext } from "./AlertModal";
import RenameValueModal, { RenameValueModalRef } from "./RenameValueModal";
import useStore from "./store";
import { sampleRandomElements } from "./backend/utils";
@ -91,7 +97,7 @@ const TabularDataNode: React.FC<TabularDataNodeProps> = ({ data, id }) => {
const [hooksY, setHooksY] = useState(120);
// For displaying error messages to user
const alertModal = useRef<AlertModalRef>(null);
const showAlert = useContext(AlertModalContext);
// For renaming a column
const renameColumnModal = useRef<RenameValueModalRef>(null);
@ -467,7 +473,7 @@ const TabularDataNode: React.FC<TabularDataNodeProps> = ({ data, id }) => {
}, [tableData, tableColumns, shouldSample, sampleNum]);
const handleError = (err: Error) => {
if (alertModal.current) alertModal.current?.trigger(err.message);
if (showAlert) showAlert(err.message);
console.error(err.message);
};
@ -520,8 +526,6 @@ const TabularDataNode: React.FC<TabularDataNodeProps> = ({ data, id }) => {
]}
/>
<AlertModal ref={alertModal} />
<RenameValueModal
ref={renameColumnModal}
initialValue={

View File

@ -1440,13 +1440,18 @@ export async function fetchExampleFlow(evalname: string): Promise<Dict> {
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({ name: evalname }),
}).then(function (res) {
return res.json();
}).then(function (json) {
if (json?.error !== undefined || !json?.data)
throw new Error(json.error as string ?? "Request to fetch example flow was sent to backend server, but there was no response.");
return json.data as Dict;
});
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (json?.error !== undefined || !json?.data)
throw new Error(
(json.error as string) ??
"Request to fetch example flow was sent to backend server, but there was no response.",
);
return json.data as Dict;
});
}
// App is not running locally, but hosted on a site.
@ -1476,13 +1481,18 @@ export async function fetchOpenAIEval(evalname: string): Promise<Dict> {
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({ name: evalname }),
}).then(function (res) {
return res.json();
}).then(function (json) {
if (json?.error !== undefined || !json?.data)
throw new Error(json.error as string ?? "Request to fetch OpenAI eval was sent to backend server, but there was no response.");
return json.data as Dict;
});
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (json?.error !== undefined || !json?.data)
throw new Error(
(json.error as string) ??
"Request to fetch OpenAI eval was sent to backend server, but there was no response.",
);
return json.data as Dict;
});
}
// App is not running locally, but hosted on a site.
@ -1500,7 +1510,9 @@ export async function fetchOpenAIEval(evalname: string): Promise<Dict> {
* @returns a Promise with the JSON of the response. Will include 'error' key if error'd; if success,
* a 'providers' key with a list of all loaded custom provider callbacks, as dicts.
*/
export async function initCustomProvider(code: string): Promise<CustomLLMProviderSpec[]> {
export async function initCustomProvider(
code: string,
): Promise<CustomLLMProviderSpec[]> {
// Attempt to fetch the example flow from the local filesystem
// by querying the Flask server:
return fetch(`${FLASK_BASE_URL}app/initCustomProvider`, {
@ -1510,13 +1522,15 @@ export async function initCustomProvider(code: string): Promise<CustomLLMProvide
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({ code }),
}).then(function (res) {
return res.json();
}).then(function (json) {
if (!json || json.error || !json.providers)
throw new Error(json.error ?? "Unknown error");
return json.providers as CustomLLMProviderSpec[];
});
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (!json || json.error || !json.providers)
throw new Error(json.error ?? "Unknown error");
return json.providers as CustomLLMProviderSpec[];
});
}
/**
@ -1536,13 +1550,15 @@ export async function removeCustomProvider(name: string): Promise<boolean> {
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({ name }),
}).then(function (res) {
return res.json();
}).then(function (json) {
if (!json || json.error || !json.success)
throw new Error(json.error ?? "Unknown error");
return true;
});
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (!json || json.error || !json.success)
throw new Error(json.error ?? "Unknown error");
return true;
});
}
/**
@ -1551,7 +1567,9 @@ export async function removeCustomProvider(name: string): Promise<boolean> {
* @returns a Promise with the JSON of the response. Will include 'error' key if error'd; if success,
* a 'providers' key with all loaded custom providers in an array. If there were none, returns empty array.
*/
export async function loadCachedCustomProviders(): Promise<CustomLLMProviderSpec[]> {
export async function loadCachedCustomProviders(): Promise<
CustomLLMProviderSpec[]
> {
return fetch(`${FLASK_BASE_URL}app/loadCachedCustomProviders`, {
method: "POST",
headers: {
@ -1559,11 +1577,16 @@ export async function loadCachedCustomProviders(): Promise<CustomLLMProviderSpec
"Access-Control-Allow-Origin": "*",
},
body: "{}",
}).then(function (res) {
return res.json();
}).then(function (json) {
if (!json || json.error || !json.providers)
throw new Error(json.error ?? "Could not load custom provider scripts: Error contacting backend.");
return json.providers as CustomLLMProviderSpec[];
});
})
.then(function (res) {
return res.json();
})
.then(function (json) {
if (!json || json.error || !json.providers)
throw new Error(
json.error ??
"Could not load custom provider scripts: Error contacting backend.",
);
return json.providers as CustomLLMProviderSpec[];
});
}

View File

@ -98,7 +98,10 @@ export default class StorageCache {
console.warn("Storage quota exceeded");
} else {
// Handle other types of storage-related errors
console.error("Error storing data in localStorage:", (error as Error).message);
console.error(
"Error storing data in localStorage:",
(error as Error).message,
);
}
return false;
}

View File

@ -718,7 +718,9 @@ const useStore = create<StoreHandles>((set, get) => ({
},
onConnect: (connection) => {
// Get the target node information
const target = connection.target ? get().getNode(connection.target) : undefined;
const target = connection.target
? get().getNode(connection.target)
: undefined;
if (target === undefined) return;
if (