Implementing Prompt Modal with Variables (#616)
This commit is contained in:
commit
86de1ac480
9 changed files with 323 additions and 76 deletions
|
|
@ -71,30 +71,68 @@ def validate_prompt(template: str):
|
|||
except Exception as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
|
||||
# if len(input_variables) > 1:
|
||||
# # If there's more than one input variable
|
||||
|
||||
return input_variables
|
||||
|
||||
|
||||
def check_input_variables(input_variables: list):
|
||||
invalid_chars = []
|
||||
fixed_variables = []
|
||||
wrong_variables = set()
|
||||
empty_variables = []
|
||||
for variable in input_variables:
|
||||
new_var = variable
|
||||
|
||||
# if variable is empty, then we should add that to the wrong variables
|
||||
if not variable:
|
||||
empty_variables.append(variable)
|
||||
continue
|
||||
|
||||
# if variable starts with a number we should add that to the invalid chars
|
||||
# and wrong variables
|
||||
if variable[0].isdigit():
|
||||
invalid_chars.append(variable[0])
|
||||
new_var = new_var.replace(variable[0], "")
|
||||
wrong_variables.add(variable)
|
||||
|
||||
for char in INVALID_CHARACTERS:
|
||||
if char in variable:
|
||||
invalid_chars.append(char)
|
||||
new_var = new_var.replace(char, "")
|
||||
wrong_variables.add(variable)
|
||||
fixed_variables.append(new_var)
|
||||
if new_var != variable:
|
||||
input_variables.remove(variable)
|
||||
input_variables.append(new_var)
|
||||
# if new_var != variable and new_var not in input_variables:
|
||||
# input_variables.remove(variable)
|
||||
# input_variables.append(new_var)
|
||||
# If any of the input_variables is not in the fixed_variables, then it means that
|
||||
# there are invalid characters in the input_variables
|
||||
if any(var not in fixed_variables for var in input_variables):
|
||||
raise ValueError(
|
||||
f"Invalid input variables: {input_variables}. Please, use something like {fixed_variables} instead."
|
||||
)
|
||||
|
||||
if any(var not in fixed_variables for var in input_variables):
|
||||
error_message = build_error_message(
|
||||
input_variables,
|
||||
invalid_chars,
|
||||
wrong_variables,
|
||||
fixed_variables,
|
||||
empty_variables,
|
||||
)
|
||||
raise ValueError(error_message)
|
||||
return input_variables
|
||||
|
||||
|
||||
def build_error_message(
|
||||
input_variables, invalid_chars, wrong_variables, fixed_variables, empty_variables
|
||||
):
|
||||
input_variables_str = ", ".join([f"'{var}'" for var in input_variables])
|
||||
error_string = f"Invalid input variables: {input_variables_str}."
|
||||
|
||||
if wrong_variables and invalid_chars:
|
||||
", ".join([f"'{var}'" for var in wrong_variables])
|
||||
invalid_chars_str = ", ".join([f"'{char}'" for char in invalid_chars])
|
||||
error_string += (
|
||||
f" Please, remove the invalid characters: {invalid_chars_str}"
|
||||
" from the variables: {wrong_variables_str}."
|
||||
)
|
||||
elif empty_variables:
|
||||
error_string += f" There are {len(empty_variables)} empty variable{'s' if len(empty_variables) > 1 else ''}."
|
||||
elif len(set(fixed_variables)) != len(fixed_variables):
|
||||
error_string += " There are duplicate variables."
|
||||
return error_string
|
||||
|
|
|
|||
8
src/frontend/package-lock.json
generated
8
src/frontend/package-lock.json
generated
|
|
@ -37,7 +37,7 @@
|
|||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"dompurify": "^3.0.3",
|
||||
"dompurify": "^3.0.4",
|
||||
"esbuild": "^0.17.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
|
|
@ -5142,9 +5142,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
|
||||
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.4.tgz",
|
||||
"integrity": "sha512-ae0mA+Qiqp6C29pqZX3fQgK+F91+F7wobM/v8DRzDqJdZJELXiFUx4PP4pK/mzUS0xkiSEx3Ncd9gr69jg3YsQ=="
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.440",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"dompurify": "^3.0.3",
|
||||
"dompurify": "^3.0.4",
|
||||
"esbuild": "^0.17.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ShadTooltipProps } from "../../types/components";
|
||||
import { RadialProgressType, ShadToolTipType } from "../../types/components";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
|
@ -6,20 +6,20 @@ import {
|
|||
TooltipTrigger,
|
||||
} from "../ui/tooltip";
|
||||
|
||||
const ShadTooltip = ({
|
||||
delayDuration = 500,
|
||||
side,
|
||||
export default function ShadTooltip({
|
||||
content,
|
||||
side,
|
||||
asChild = true,
|
||||
children,
|
||||
style,
|
||||
}: ShadTooltipProps) => {
|
||||
delayDuration
|
||||
}: ShadToolTipType) {
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
className={style}
|
||||
side={side}
|
||||
avoidCollisions={false}
|
||||
sticky="always"
|
||||
|
|
@ -29,6 +29,5 @@ const ShadTooltip = ({
|
|||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ShadTooltip;
|
||||
|
|
|
|||
|
|
@ -523,3 +523,9 @@ export const NOUNS: string[] = [
|
|||
*
|
||||
*/
|
||||
export const USER_PROJECTS_HEADER = "My Collection";
|
||||
/**
|
||||
* CSS for highlight HTML
|
||||
* @constant
|
||||
*
|
||||
*/
|
||||
export const HIGHLIGH_CSS = "block pl-3 pr-14 py-2 w-full h-full text-sm outline-0 border-0 break-all";
|
||||
|
|
@ -102,7 +102,7 @@ export default function CodeAreaModal({
|
|||
return (
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent className="h-[500px] lg:max-w-[700px]">
|
||||
<DialogContent className="min-w-[80vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Edit Code</span>
|
||||
|
|
@ -115,7 +115,7 @@ export default function CodeAreaModal({
|
|||
<DialogDescription>{CODE_PROMPT_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-2 flex h-full w-full">
|
||||
<div className="flex h-[60vh] w-full mt-2">
|
||||
<AceEditor
|
||||
value={code}
|
||||
mode="python"
|
||||
|
|
@ -129,7 +129,7 @@ export default function CodeAreaModal({
|
|||
onChange={(value) => {
|
||||
setCode(value);
|
||||
}}
|
||||
className="h-[300px] w-full rounded-lg border-[1px] border-ring custom-scroll "
|
||||
className="w-full rounded-lg h-full custom-scroll border-[1px] border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useRef, useState } from "react";
|
||||
import { useContext, useRef, useState, useEffect } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { postValidatePrompt } from "../../controllers/API";
|
||||
|
|
@ -14,9 +14,31 @@ import {
|
|||
} from "../../components/ui/dialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { PROMPT_DIALOG_SUBTITLE, TEXT_DIALOG_SUBTITLE } from "../../constants";
|
||||
import {
|
||||
HIGHLIGH_CSS,
|
||||
PROMPT_DIALOG_SUBTITLE,
|
||||
TEXT_DIALOG_SUBTITLE,
|
||||
} from "../../constants";
|
||||
import { FileText } from "lucide-react";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import {
|
||||
INVALID_CHARACTERS,
|
||||
TypeModal,
|
||||
classNames,
|
||||
getRandomKeyByssmm,
|
||||
regexHighlight,
|
||||
varHighlightHTML,
|
||||
} from "../../utils";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "../../components/ui/tooltip";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import { set } from "lodash";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
export default function GenericModal({
|
||||
field_name = "",
|
||||
|
|
@ -41,7 +63,10 @@ export default function GenericModal({
|
|||
const [myModalTitle] = useState(modalTitle);
|
||||
const [myModalType] = useState(type);
|
||||
const [open, setOpen] = useState(true);
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEdit, setIsEdit] = useState(true);
|
||||
const [wordsHighlightInvalid, setWordsHighlightInvalid] = useState([]);
|
||||
const [wordsHighlight, setWordsHighlight] = useState([]);
|
||||
const { dark } = useContext(darkContext);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const { closePopUp, setCloseEdit } = useContext(PopUpContext);
|
||||
|
|
@ -54,10 +79,108 @@ export default function GenericModal({
|
|||
}
|
||||
}
|
||||
|
||||
function checkVariables(valueToCheck) {
|
||||
const regex = /\{([^{}]+)\}/g;
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = regex.exec(valueToCheck))) {
|
||||
matches.push(`{${match[1]}}`);
|
||||
}
|
||||
|
||||
let invalid_chars = [];
|
||||
let fixed_variables = [];
|
||||
let input_variables = matches;
|
||||
for (let variable of input_variables) {
|
||||
let new_var = variable;
|
||||
for (let char of INVALID_CHARACTERS) {
|
||||
if (variable.includes(char)) {
|
||||
invalid_chars.push(new_var);
|
||||
}
|
||||
}
|
||||
fixed_variables.push(new_var);
|
||||
if (new_var !== variable) {
|
||||
const index = input_variables.indexOf(variable);
|
||||
if (index !== -1) {
|
||||
input_variables.splice(index, 1, new_var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filteredWordsHighlight = matches.filter(
|
||||
(word) => !invalid_chars.includes(word)
|
||||
);
|
||||
|
||||
setWordsHighlightInvalid(invalid_chars);
|
||||
setWordsHighlight(filteredWordsHighlight);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (type == TypeModal.PROMPT && inputValue && inputValue != "") {
|
||||
checkVariables(inputValue);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const coloredContent = (inputValue || "")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(regexHighlight, varHighlightHTML({ name: "$1" }))
|
||||
.replace(/\n/g, "<br />");
|
||||
|
||||
const TextAreaContentView = () => {
|
||||
return (
|
||||
<div
|
||||
className={HIGHLIGH_CSS}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(coloredContent) }}
|
||||
suppressContentEditableWarning={true}
|
||||
onClick={() => {
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function validatePrompt(closeModal: boolean) {
|
||||
postValidatePrompt(field_name, inputValue, nodeClass)
|
||||
.then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
setNodeClass(apiReturn.data.frontend_node);
|
||||
setModalOpen(closeModal);
|
||||
|
||||
let inputVariables = apiReturn.data.input_variables;
|
||||
if (inputVariables.length === 0) {
|
||||
setIsEdit(true);
|
||||
setErrorData({
|
||||
title:
|
||||
"The template you are attempting to use does not contain any variables for data entry.",
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
setSuccessData({
|
||||
title: "Prompt is ready",
|
||||
});
|
||||
setModalOpen(closeModal);
|
||||
setValue(inputValue);
|
||||
}
|
||||
} else {
|
||||
setIsEdit(true);
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsEdit(true);
|
||||
return setErrorData({
|
||||
title: "There is something wrong with this prompt, please review it",
|
||||
list: [error.response.data.detail],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[700px]">
|
||||
<DialogContent className="min-w-[80vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">{myModalTitle}</span>
|
||||
|
|
@ -83,17 +206,78 @@ export default function GenericModal({
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-2 flex h-full w-full">
|
||||
<Textarea
|
||||
ref={ref}
|
||||
className=" form-primary h-[300px] w-full "
|
||||
value={myValue}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder="Type message here."
|
||||
/>
|
||||
{type == TypeModal.PROMPT &&
|
||||
inputValue &&
|
||||
inputValue != "" &&
|
||||
wordsHighlight.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<span className="">Variables: </span>
|
||||
{wordsHighlight.map((word, index) => (
|
||||
<ShadTooltip
|
||||
key={getRandomKeyByssmm() + index}
|
||||
content={word.replace(/[{}]/g, "")}
|
||||
asChild={false}
|
||||
delayDuration={1500}
|
||||
>
|
||||
<Badge
|
||||
key={index}
|
||||
size="lg"
|
||||
className="m-1 max-w-[40vw] cursor-default truncate p-2.5 text-sm"
|
||||
>
|
||||
<div className="relative bottom-[1px]">
|
||||
<span>
|
||||
{word.replace(/[{}]/g, "").length > 59
|
||||
? word.replace(/[{}]/g, "").slice(0, 56) + "..."
|
||||
: word.replace(/[{}]/g, "")}
|
||||
</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</ShadTooltip>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
!isEdit ? "rounded-lg border" : "",
|
||||
"flex h-[60vh] w-full"
|
||||
)}
|
||||
>
|
||||
{type == TypeModal.PROMPT && isEdit ? (
|
||||
<Textarea
|
||||
ref={ref}
|
||||
className="form-input h-full w-full rounded-lg border-gray-300 focus-visible:ring-1 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
value={inputValue}
|
||||
onBlur={() => {
|
||||
blur();
|
||||
validatePrompt(true);
|
||||
}}
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setValue(e.target.value);
|
||||
checkVariables(e.target.value);
|
||||
}}
|
||||
placeholder="Type message here."
|
||||
/>
|
||||
) : type == TypeModal.PROMPT && !isEdit ? (
|
||||
<TextAreaContentView />
|
||||
) : type != TypeModal.PROMPT ? (
|
||||
<Textarea
|
||||
ref={ref}
|
||||
className="form-input h-full w-full rounded-lg border-gray-300 focus-visible:ring-1 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder="Type message here."
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
|
@ -105,39 +289,7 @@ export default function GenericModal({
|
|||
setModalOpen(false);
|
||||
break;
|
||||
case 2:
|
||||
console.log("postValidatePrompt");
|
||||
postValidatePrompt(field_name, myValue, nodeClass)
|
||||
.then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
setNodeClass(apiReturn.data.frontend_node);
|
||||
setModalOpen(false);
|
||||
|
||||
let inputVariables = apiReturn.data.input_variables;
|
||||
if (inputVariables.length === 0) {
|
||||
setErrorData({
|
||||
title:
|
||||
"The template you are attempting to use does not contain any variables for data entry.",
|
||||
});
|
||||
} else {
|
||||
setSuccessData({
|
||||
title: "Prompt is ready",
|
||||
});
|
||||
setModalOpen(false);
|
||||
setValue(myValue);
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
return setErrorData({
|
||||
title:
|
||||
"There is something wrong with this prompt, please review it",
|
||||
list: [error.response.data.detail],
|
||||
});
|
||||
});
|
||||
validatePrompt(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -141,3 +141,25 @@ export type ShadTooltipProps = {
|
|||
children: ReactNode;
|
||||
style?: string;
|
||||
};
|
||||
export type ShadToolTipType = {
|
||||
content?: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
asChild?: boolean;
|
||||
children?: ReactElement;
|
||||
delayDuration?: number;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export type TextHighlightType = {
|
||||
value?: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
asChild?: boolean;
|
||||
children?: ReactElement;
|
||||
delayDuration?: number;
|
||||
};
|
||||
|
||||
export interface IVarHighlightType {
|
||||
name: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import {
|
|||
import { SupabaseIcon } from "./icons/supabase";
|
||||
import { MongoDBIcon } from "./icons/MongoDB";
|
||||
import { VertexAIIcon } from "./icons/VertexAI";
|
||||
import { IVarHighlightType } from "./types/components";
|
||||
|
||||
export function classNames(...classes: Array<string>) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
|
|
@ -1006,3 +1007,32 @@ export function getRandomKeyByssmm(): string {
|
|||
const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
|
||||
return seconds + milliseconds + Math.abs(Math.floor(Math.random() * 10001));
|
||||
}
|
||||
|
||||
|
||||
export const INVALID_CHARACTERS = [
|
||||
" ",
|
||||
",",
|
||||
".",
|
||||
":",
|
||||
";",
|
||||
"!",
|
||||
"?",
|
||||
"/",
|
||||
"\\",
|
||||
"(",
|
||||
")",
|
||||
"[",
|
||||
"]",
|
||||
];
|
||||
|
||||
export const regexHighlight = /\{([^}]+)\}/g;
|
||||
|
||||
|
||||
export const varHighlightHTML = ({ name }: IVarHighlightType) => {
|
||||
const html = `<div class="inline-flex items-center justify-center rounded-md font-medium text-primary bg-muted pb-1">
|
||||
<span class='opacity-60 pl-1'>{</span>
|
||||
<span>${name}</span>
|
||||
<span class='opacity-60 pr-1'>}</span>
|
||||
</div>`
|
||||
return html
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue