Merge remote-tracking branch 'origin/form_io' into python_custom_node_component
This commit is contained in:
commit
91f8573bed
27 changed files with 221 additions and 1052 deletions
|
|
@ -26,8 +26,9 @@ def remove_api_keys(flow: dict):
|
|||
|
||||
def build_input_keys_response(langchain_object):
|
||||
"""Build the input keys response."""
|
||||
|
||||
input_keys_response = {
|
||||
"input_keys": langchain_object.input_keys,
|
||||
"input_keys": {key: "" for key in langchain_object.input_keys},
|
||||
"memory_keys": [],
|
||||
}
|
||||
# If the object has memory, that memory will have a memory_variables attribute
|
||||
|
|
@ -44,4 +45,7 @@ def build_input_keys_response(langchain_object):
|
|||
# Add memory variables to memory_keys
|
||||
input_keys_response["memory_keys"] = langchain_object.memory.memory_variables
|
||||
|
||||
if hasattr(langchain_object, "prompt"):
|
||||
input_keys_response["template"] = langchain_object.prompt.template
|
||||
|
||||
return input_keys_response
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class ChatMessage(BaseModel):
|
|||
"""Chat message schema."""
|
||||
|
||||
is_bot: bool = False
|
||||
message: Union[str, None] = None
|
||||
message: Union[str, None, dict] = None
|
||||
type: str = "human"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,21 +31,36 @@ def post_validate_code(code: Code):
|
|||
def post_validate_prompt(prompt: ValidatePromptRequest):
|
||||
try:
|
||||
input_variables = validate_prompt(prompt.template)
|
||||
# Reinitialize custom_fields
|
||||
old_custom_fields = prompt.frontend_node.custom_fields.copy()
|
||||
prompt.frontend_node.custom_fields = []
|
||||
# Add new variables to the template
|
||||
for variable in input_variables:
|
||||
try:
|
||||
template_field = TemplateField(
|
||||
name=variable, field_type="str", show=True, advanced=False
|
||||
name=variable,
|
||||
field_type="str",
|
||||
show=True,
|
||||
advanced=False,
|
||||
input_types=["BaseLoader"],
|
||||
)
|
||||
|
||||
prompt.frontend_node.template[variable] = template_field.to_dict()
|
||||
prompt.frontend_node.custom_fields.append(variable)
|
||||
for key in prompt.frontend_node.template:
|
||||
if key not in input_variables:
|
||||
prompt.frontend_node.template.pop(key, None)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
# Remove variables that are not in the template anymore
|
||||
for variable in old_custom_fields:
|
||||
if variable not in input_variables:
|
||||
try:
|
||||
prompt.frontend_node.template.pop(variable, None)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
return PromptValidationResponse(
|
||||
input_variables=input_variables,
|
||||
frontend_node=prompt.frontend_node,
|
||||
|
|
|
|||
|
|
@ -111,9 +111,9 @@ class ChatManager:
|
|||
self, client_id: str, payload: Dict, langchain_object: Any
|
||||
):
|
||||
# Process the graph data and chat message
|
||||
chat_message = payload.pop("message", "")
|
||||
chat_message = ChatMessage(message=chat_message)
|
||||
self.chat_history.add_message(client_id, chat_message)
|
||||
chat_inputs = payload.pop("inputs", "")
|
||||
chat_inputs = ChatMessage(message=chat_inputs)
|
||||
self.chat_history.add_message(client_id, chat_inputs)
|
||||
|
||||
# graph_data = payload
|
||||
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
|
||||
|
|
@ -126,7 +126,7 @@ class ChatManager:
|
|||
|
||||
result, intermediate_steps = await process_graph(
|
||||
langchain_object=langchain_object,
|
||||
chat_message=chat_message,
|
||||
chat_inputs=chat_inputs,
|
||||
websocket=self.active_connections[client_id],
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from langflow.utils.logger import logger
|
|||
|
||||
async def process_graph(
|
||||
langchain_object,
|
||||
chat_message: ChatMessage,
|
||||
chat_inputs: ChatMessage,
|
||||
websocket: WebSocket,
|
||||
):
|
||||
langchain_object = try_setting_streaming_options(langchain_object, websocket)
|
||||
|
|
@ -23,7 +23,7 @@ async def process_graph(
|
|||
try:
|
||||
logger.debug("Generating result and thought")
|
||||
result, intermediate_steps = await get_result_and_steps(
|
||||
langchain_object, chat_message.message or "", websocket=websocket
|
||||
langchain_object, chat_inputs.message or "", websocket=websocket
|
||||
)
|
||||
logger.debug("Generated result and intermediate_steps")
|
||||
return result, intermediate_steps
|
||||
|
|
|
|||
|
|
@ -6,23 +6,12 @@ from langflow.processing.process import fix_memory_inputs, format_actions
|
|||
from langflow.utils.logger import logger
|
||||
|
||||
|
||||
async def get_result_and_steps(langchain_object, message: str, **kwargs):
|
||||
async def get_result_and_steps(langchain_object, inputs: dict, **kwargs):
|
||||
"""Get result and thought from extracted json"""
|
||||
|
||||
try:
|
||||
if hasattr(langchain_object, "verbose"):
|
||||
langchain_object.verbose = True
|
||||
chat_input = None
|
||||
memory_key = ""
|
||||
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
|
||||
memory_key = langchain_object.memory.memory_key
|
||||
|
||||
if hasattr(langchain_object, "input_keys"):
|
||||
for key in langchain_object.input_keys:
|
||||
if key not in [memory_key, "chat_history"]:
|
||||
chat_input = {key: message}
|
||||
else:
|
||||
chat_input = message # type: ignore
|
||||
|
||||
if hasattr(langchain_object, "return_intermediate_steps"):
|
||||
# https://github.com/hwchase17/langchain/issues/2068
|
||||
|
|
@ -33,12 +22,12 @@ async def get_result_and_steps(langchain_object, message: str, **kwargs):
|
|||
fix_memory_inputs(langchain_object)
|
||||
try:
|
||||
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
|
||||
output = await langchain_object.acall(chat_input, callbacks=async_callbacks)
|
||||
output = await langchain_object.acall(inputs, callbacks=async_callbacks)
|
||||
except Exception as exc:
|
||||
# make the error message more informative
|
||||
logger.debug(f"Error: {str(exc)}")
|
||||
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
|
||||
output = langchain_object(chat_input, callbacks=sync_callbacks)
|
||||
output = langchain_object(inputs, callbacks=sync_callbacks)
|
||||
|
||||
intermediate_steps = (
|
||||
output.get("intermediate_steps", []) if isinstance(output, dict) else []
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class TemplateFieldCreator(BaseModel, ABC):
|
|||
name: str = ""
|
||||
display_name: Optional[str] = None
|
||||
advanced: bool = False
|
||||
input_types: list[str] = []
|
||||
|
||||
def to_dict(self):
|
||||
result = self.dict()
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ export default function GenericNode({
|
|||
}
|
||||
|
||||
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||
console.log({data})
|
||||
return (
|
||||
<>
|
||||
<NodeToolbar>
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { ReactElement, useContext, useEffect, useRef, useState } from "react";
|
||||
import { ProgressBarType } from "../../types/components";
|
||||
import { Progress } from "../../components/ui/progress";
|
||||
import { progressContext } from "../../contexts/ProgressContext";
|
||||
import { setInterval } from "timers/promises";
|
||||
|
||||
export default function ProgressBarComponent({
|
||||
value,
|
||||
children,
|
||||
}: ProgressBarType) {
|
||||
const ref = useRef(0);
|
||||
const reff = useRef();
|
||||
const { progress } = useContext(progressContext);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = progress * 100;
|
||||
console.log(progress);
|
||||
}, [progress]);
|
||||
|
||||
return <Progress className="h-2.5" value={ref.current} />;
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { FormInput, MessagesSquare } from "lucide-react";
|
||||
|
||||
import { alertContext } from "../../../contexts/alertContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function FormTrigger({ open, setOpen, isBuilt }) {
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
function handleClick() {
|
||||
if (isBuilt) {
|
||||
setOpen(true);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Flow not built",
|
||||
list: ["Please build the flow before chatting"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
show={!open}
|
||||
appear={true}
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="translate-y-96"
|
||||
enterTo="translate-y-0"
|
||||
leave="transition ease-in duration-300"
|
||||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<div className="fixed bottom-36 right-4">
|
||||
<div
|
||||
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
||||
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<button>
|
||||
<div className="flex gap-3">
|
||||
<FormInput
|
||||
className="pth-6 w-6 stroke-2 stroke-[#5c8be1]"
|
||||
style={{ color: "white" }}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,11 +3,9 @@ import { useNodes } from "reactflow";
|
|||
import { ChatType } from "../../types/chat";
|
||||
import ChatTrigger from "./chatTrigger";
|
||||
import BuildTrigger from "./buildTrigger";
|
||||
import ChatModal from "../../modals/chatModal";
|
||||
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import FormTrigger from "./formTrigger";
|
||||
import FormModal from "../../modals/formModal";
|
||||
|
||||
export default function Chat({ flow }: ChatType) {
|
||||
|
|
@ -46,10 +44,11 @@ export default function Chat({ flow }: ChatType) {
|
|||
const prevNodesRef = useRef<any[] | undefined>();
|
||||
const nodes = useNodes();
|
||||
useEffect(() => {
|
||||
|
||||
const prevNodes = prevNodesRef.current;
|
||||
const currentNodes = nodes.map(
|
||||
(node: NodeType) => node.data.node.template.value
|
||||
);
|
||||
(node: NodeType) => node.data.node.template
|
||||
);
|
||||
|
||||
if (
|
||||
prevNodes &&
|
||||
|
|
@ -66,15 +65,13 @@ export default function Chat({ flow }: ChatType) {
|
|||
{isBuilt ? (
|
||||
<div>
|
||||
<BuildTrigger
|
||||
open={open || openForm}
|
||||
open={open}
|
||||
flow={flow}
|
||||
setIsBuilt={setIsBuilt}
|
||||
isBuilt={isBuilt}
|
||||
/>
|
||||
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||
<FormModal key={flow.id + "form"} flow={flow} open={openForm} setOpen={setOpenForm} />
|
||||
<ChatTrigger open={open || openForm} setOpen={setOpen} isBuilt={isBuilt} />
|
||||
<FormTrigger open={open || openForm} setOpen={setOpenForm} isBuilt={isBuilt} />
|
||||
<FormModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||
<ChatTrigger open={open} setOpen={setOpen} isBuilt={isBuilt} />
|
||||
</div>
|
||||
) : (
|
||||
<BuildTrigger
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export default function IntComponent({
|
|||
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
// console.log(event);
|
||||
if (
|
||||
event.key !== "Backspace" &&
|
||||
event.key !== "Enter" &&
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ export default function PromptAreaComponent({
|
|||
setMyValue(t);
|
||||
onChange(t);
|
||||
}}
|
||||
nodeClass={nodeClass}
|
||||
setNodeClass={setNodeClass}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,25 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "../../utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center border rounded-full px-2.5 h-6 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"inline-flex items-center border rounded-full px-2.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
gray:
|
||||
"bg-border hover:bg-border/80 text-secondary-foreground",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
size: {
|
||||
sm: "h-4 text-xs",
|
||||
md: "h-5 text-sm",
|
||||
lg: "h-6 text-base",
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
|
|
@ -26,9 +33,9 @@ export interface BadgeProps
|
|||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
function Badge({ className, variant, size, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
<div className={cn(badgeVariants({ variant, size }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
|
|||
export const CODE_DIALOG_SUBTITLE =
|
||||
"Export your flow to use it with this code.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Chat Form
|
||||
* @constant
|
||||
*/
|
||||
export const CHAT_FORM_DIALOG_SUBTITLE =
|
||||
"Set up the input variables defined in prompt templates. Interact with agents and chains.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Edit Node Dialog
|
||||
* @constant
|
||||
|
|
|
|||
|
|
@ -87,12 +87,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
Saveflows.forEach((flow) => {
|
||||
if (flow.data && flow.data?.nodes)
|
||||
flow.data?.nodes.forEach((node) => {
|
||||
// console.log(node.data.type);
|
||||
//looking for file fields to prevent saving the content and breaking the flow for exceeding the the data limite for local storage
|
||||
Object.keys(node.data.node.template).forEach((key) => {
|
||||
// console.log(node.data.node.template[key].type);
|
||||
if (node.data.node.template[key].type === "file") {
|
||||
// console.log(node.data.node.template[key]);
|
||||
node.data.node.template[key].content = null;
|
||||
node.data.node.template[key].value = "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
import { classNames } from "../../../utils";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
import { INPUT_STYLE } from "../../../constants";
|
||||
import { Lock, Send } from "lucide-react";
|
||||
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
chatValue,
|
||||
sendMessage,
|
||||
setChatValue,
|
||||
inputRef,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [lockChat, inputRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.height = "inherit"; // Reset the height
|
||||
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
|
||||
}
|
||||
}, [chatValue]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<textarea
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !lockChat && !event.shiftKey) {
|
||||
sendMessage();
|
||||
}
|
||||
}}
|
||||
rows={1}
|
||||
ref={inputRef}
|
||||
disabled={lockChat}
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${inputRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "150px",
|
||||
overflow: `${
|
||||
inputRef.current && inputRef.current.scrollHeight > 150
|
||||
? "auto"
|
||||
: "hidden"
|
||||
}`,
|
||||
}}
|
||||
value={lockChat ? "Thinking..." : chatValue}
|
||||
onChange={(e) => {
|
||||
setChatValue(e.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
lockChat
|
||||
? " bg-input text-black dark:bg-gray-700 dark:text-gray-300"
|
||||
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
|
||||
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
)}
|
||||
placeholder={"Send a message..."}
|
||||
/>
|
||||
<div className="absolute bottom-0.5 right-3">
|
||||
<button disabled={lockChat} onClick={() => sendMessage()}>
|
||||
{lockChat ? (
|
||||
<Lock
|
||||
className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Send
|
||||
className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
||||
import { FC, memo, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import { programmingLanguages } from "../../../../utils";
|
||||
|
||||
interface Props {
|
||||
language: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
const downloadAsFile = () => {
|
||||
const fileExtension = programmingLanguages[language] || ".file";
|
||||
const suggestedFileName = `${"generated-code"}${fileExtension}`;
|
||||
const fileName = window.prompt("enter file name", suggestedFileName);
|
||||
|
||||
if (!fileName) {
|
||||
// user pressed cancel on prompt
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([value], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.download = fileName;
|
||||
link.href = url;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
return (
|
||||
<div className="codeblock font-sans text-[16px]">
|
||||
<div className="flex items-center justify-between py-1.5 px-4">
|
||||
<span className="text-xs lowercase text-white">{language}</span>
|
||||
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center rounded bg-none p-1 text-xs text-white"
|
||||
onClick={downloadAsFile}
|
||||
>
|
||||
<IconDownload size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SyntaxHighlighter
|
||||
className=" w-[570px]"
|
||||
language={language}
|
||||
style={oneDark}
|
||||
customStyle={{ margin: 0 }}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CodeBlock.displayName = "CodeBlock";
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { ChatMessageType } from "../../../types/chat";
|
||||
import { classNames } from "../../../utils";
|
||||
import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg";
|
||||
import AiIconStill from "../../../assets/froze-flow.png";
|
||||
import FileCard from "../fileComponent";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import rehypeMathjax from "rehype-mathjax";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import { CodeBlock } from "./codeBlock";
|
||||
import Convert from "ansi-to-html";
|
||||
import { User2, MessageCircle } from "lucide-react";
|
||||
|
||||
export default function ChatMessage({
|
||||
chat,
|
||||
lockChat,
|
||||
lastMessage,
|
||||
}: {
|
||||
chat: ChatMessageType;
|
||||
lockChat: boolean;
|
||||
lastMessage: boolean;
|
||||
}) {
|
||||
const convert = new Convert({ newline: true });
|
||||
const [message, setMessage] = useState("");
|
||||
const imgRef = useRef(null);
|
||||
useEffect(() => {
|
||||
setMessage(chat.message);
|
||||
}, [chat.message]);
|
||||
const [hidden, setHidden] = useState(true);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"w-full py-2 pl-2 flex",
|
||||
chat.isSend
|
||||
? "bg-background dark:bg-gray-900 "
|
||||
: "bg-input dark:bg-gray-800"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"rounded-full overflow-hidden w-8 h-8 flex items-center my-3 justify-center"
|
||||
)}
|
||||
>
|
||||
{!chat.isSend && (
|
||||
<div className="relative w-8 h-8">
|
||||
<img
|
||||
className={
|
||||
"absolute transition-opacity duration-500 scale-150 " +
|
||||
(lockChat ? "opacity-100" : "opacity-0")
|
||||
}
|
||||
src={lastMessage ? AiIcon : AiIconStill}
|
||||
/>
|
||||
<img
|
||||
className={
|
||||
"absolute transition-opacity duration-500 scale-150 " +
|
||||
(lockChat ? "opacity-0" : "opacity-100")
|
||||
}
|
||||
src={AiIconStill}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{chat.isSend && (
|
||||
<User2 className="w-6 h-6 -mb-1 text-gray-800 dark:text-gray-200" />
|
||||
)}
|
||||
</div>
|
||||
{!chat.isSend ? (
|
||||
<div className="w-full text-start flex items-center">
|
||||
<div className="w-full relative text-start inline-block text-gray-600 dark:text-gray-300 text-sm font-normal">
|
||||
{hidden && chat.thought && chat.thought !== "" && (
|
||||
<div
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
className="absolute -top-1 -left-2 cursor-pointer"
|
||||
>
|
||||
<MessageCircle className="w-5 h-5 animate-bounce dark:text-white" />
|
||||
</div>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && (
|
||||
<div
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500
|
||||
bg-muted dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convert.toHtml(chat.thought),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
|
||||
<div className="w-full px-4 pb-3 pt-3 pr-8">
|
||||
<div className="dark:text-white w-full">
|
||||
<div className="w-full">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
className="markdown prose dark:prose-invert text-gray-600 dark:text-gray-200"
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
if (children.length) {
|
||||
if (children[0] == "▍") {
|
||||
return (
|
||||
<span className="animate-pulse cursor-default mt-1">
|
||||
▍
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
children[0] = (children[0] as string).replace(
|
||||
"`▍`",
|
||||
"▍"
|
||||
);
|
||||
}
|
||||
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
|
||||
return !inline ? (
|
||||
<CodeBlock
|
||||
key={Math.random()}
|
||||
language={(match && match[1]) || ""}
|
||||
value={String(children).replace(/\n$/, "")}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
{chat.files && (
|
||||
<div className="my-2 w-full">
|
||||
{chat.files.map((file, index) => {
|
||||
return (
|
||||
<div key={index} className="my-2 w-full">
|
||||
<FileCard
|
||||
fileName={"Generated File"}
|
||||
fileType={file.data_type}
|
||||
content={file.data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex items-center">
|
||||
<div className="text-start inline-block px-3 text-gray-600 dark:text-white">
|
||||
<span
|
||||
className="text-gray-600 dark:text-gray-200"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: message.replace(/\n/g, "<br>"),
|
||||
}}
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import * as base64js from "base64-js";
|
||||
import { useState } from "react";
|
||||
import { DownloadCloud, File } from "lucide-react";
|
||||
|
||||
export default function FileCard({ fileName, content, fileType }) {
|
||||
const handleDownload = () => {
|
||||
const byteArray = new Uint8Array(base64js.toByteArray(content));
|
||||
const blob = new Blob([byteArray], { type: "application/octet-stream" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName + ".png";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
function handleMouseEnter() {
|
||||
setIsHovered(true);
|
||||
}
|
||||
function handleMouseLeave() {
|
||||
setIsHovered(false);
|
||||
}
|
||||
|
||||
if (fileType === "image") {
|
||||
return (
|
||||
<div
|
||||
className="relative w-1/4 h-1/4"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt="generated image"
|
||||
className="rounded-lg w-full h-full"
|
||||
/>
|
||||
{isHovered && (
|
||||
<div
|
||||
className={`absolute top-0 right-0 bg-muted text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
|
||||
>
|
||||
<button
|
||||
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<DownloadCloud className="hover:scale-110 w-5 h-5 text-current" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="bg-muted shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
|
||||
>
|
||||
<div className="flex gap-2 text-current items-center w-full mr-2">
|
||||
{" "}
|
||||
{fileType === "image" ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt=""
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
) : (
|
||||
<File className="w-8 h-8" />
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
{" "}
|
||||
<div className="truncate text-sm text-current">{fileName}</div>
|
||||
<div className="truncate text-xs text-gray-500">{fileType}</div>
|
||||
</div>
|
||||
<DownloadCloud className="w-6 h-6 text-current ml-auto" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,415 +0,0 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { validateNodes } from "../../utils";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import ChatMessage from "./chatMessage";
|
||||
import { X, MessagesSquare, Eraser } from "lucide-react";
|
||||
import { sendAllProps } from "../../types/api";
|
||||
import { ChatMessageType } from "../../types/chat";
|
||||
import ChatInput from "./chatInput";
|
||||
|
||||
import _ from "lodash";
|
||||
|
||||
export default function ChatModal({
|
||||
flow,
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: Function;
|
||||
flow: FlowType;
|
||||
}) {
|
||||
const [chatValue, setChatValue] = useState("");
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setErrorData, setNoticeData } = useContext(alertContext);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
const isOpen = useRef(open);
|
||||
const messagesRef = useRef(null);
|
||||
const id = useRef(flow.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesRef.current) {
|
||||
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
||||
}
|
||||
}, [chatHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
isOpen.current = open;
|
||||
}, [open]);
|
||||
useEffect(() => {
|
||||
id.current = flow.id;
|
||||
}, [flow.id]);
|
||||
|
||||
var isStream = false;
|
||||
|
||||
const addChatHistory = (
|
||||
message: string,
|
||||
isSend: boolean,
|
||||
thought?: string,
|
||||
files?: Array<any>
|
||||
) => {
|
||||
setChatHistory((old) => {
|
||||
let newChat = _.cloneDeep(old);
|
||||
if (files) {
|
||||
newChat.push({ message, isSend, files, thought });
|
||||
} else if (thought) {
|
||||
newChat.push({ message, isSend, thought });
|
||||
} else {
|
||||
newChat.push({ message, isSend });
|
||||
}
|
||||
return newChat;
|
||||
});
|
||||
};
|
||||
|
||||
//add proper type signature for function
|
||||
|
||||
function updateLastMessage({
|
||||
str,
|
||||
thought,
|
||||
end = false,
|
||||
files,
|
||||
}: {
|
||||
str?: string;
|
||||
thought?: string;
|
||||
// end param default is false
|
||||
end?: boolean;
|
||||
files?: Array<any>;
|
||||
}) {
|
||||
setChatHistory((old) => {
|
||||
let newChat = [...old];
|
||||
if (str) {
|
||||
if (end) {
|
||||
newChat[newChat.length - 1].message = str;
|
||||
} else {
|
||||
newChat[newChat.length - 1].message =
|
||||
newChat[newChat.length - 1].message + str;
|
||||
}
|
||||
}
|
||||
if (thought) {
|
||||
newChat[newChat.length - 1].thought = thought;
|
||||
}
|
||||
if (files) {
|
||||
newChat[newChat.length - 1].files = files;
|
||||
}
|
||||
return newChat;
|
||||
});
|
||||
}
|
||||
|
||||
function handleOnClose(event: CloseEvent) {
|
||||
if (isOpen.current) {
|
||||
setErrorData({ title: event.reason });
|
||||
setTimeout(() => {
|
||||
connectWS();
|
||||
setLockChat(false);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function getWebSocketUrl(chatId, isDevelopment = false) {
|
||||
const isSecureProtocol = window.location.protocol === "https:";
|
||||
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
|
||||
const host = isDevelopment ? "localhost:7860" : window.location.host;
|
||||
const chatEndpoint = `/api/v1/chat/${chatId}`;
|
||||
|
||||
return `${
|
||||
isDevelopment ? "ws" : webSocketProtocol
|
||||
}://${host}${chatEndpoint}`;
|
||||
}
|
||||
|
||||
function handleWsMessage(data: any) {
|
||||
if (Array.isArray(data)) {
|
||||
//set chat history
|
||||
setChatHistory((_) => {
|
||||
let newChatHistory: ChatMessageType[] = [];
|
||||
data.forEach(
|
||||
(chatItem: {
|
||||
intermediate_steps?: "string";
|
||||
is_bot: boolean;
|
||||
message: string;
|
||||
type: string;
|
||||
files?: Array<any>;
|
||||
}) => {
|
||||
if (chatItem.message) {
|
||||
newChatHistory.push(
|
||||
chatItem.files
|
||||
? {
|
||||
isSend: !chatItem.is_bot,
|
||||
message: chatItem.message,
|
||||
thought: chatItem.intermediate_steps,
|
||||
files: chatItem.files,
|
||||
}
|
||||
: {
|
||||
isSend: !chatItem.is_bot,
|
||||
message: chatItem.message,
|
||||
thought: chatItem.intermediate_steps,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
return newChatHistory;
|
||||
});
|
||||
}
|
||||
if (data.type === "start") {
|
||||
addChatHistory("", false);
|
||||
isStream = true;
|
||||
}
|
||||
if (data.type === "end") {
|
||||
if (data.message) {
|
||||
updateLastMessage({ str: data.message, end: true });
|
||||
}
|
||||
if (data.intermediate_steps) {
|
||||
updateLastMessage({
|
||||
str: data.message,
|
||||
thought: data.intermediate_steps,
|
||||
end: true,
|
||||
});
|
||||
}
|
||||
if (data.files) {
|
||||
updateLastMessage({
|
||||
end: true,
|
||||
files: data.files,
|
||||
});
|
||||
}
|
||||
|
||||
setLockChat(false);
|
||||
isStream = false;
|
||||
}
|
||||
if (data.type === "stream" && isStream) {
|
||||
updateLastMessage({ str: data.message });
|
||||
}
|
||||
}
|
||||
|
||||
function connectWS() {
|
||||
try {
|
||||
const urlWs = getWebSocketUrl(
|
||||
id.current,
|
||||
process.env.NODE_ENV === "development"
|
||||
);
|
||||
const newWs = new WebSocket(urlWs);
|
||||
newWs.onopen = () => {
|
||||
console.log("WebSocket connection established!");
|
||||
};
|
||||
newWs.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("Received data:", data);
|
||||
handleWsMessage(data);
|
||||
//get chat history
|
||||
};
|
||||
newWs.onclose = (event) => {
|
||||
handleOnClose(event);
|
||||
};
|
||||
newWs.onerror = (ev) => {
|
||||
console.log(ev, "error");
|
||||
if (flow.id === "") {
|
||||
connectWS();
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "There was an error on web connection, please: ",
|
||||
list: [
|
||||
"Refresh the page",
|
||||
"Use a new flow tab",
|
||||
"Check if the backend is up",
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
ws.current = newWs;
|
||||
} catch (error) {
|
||||
if (flow.id === "") {
|
||||
connectWS();
|
||||
}
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
connectWS();
|
||||
return () => {
|
||||
console.log("unmount");
|
||||
console.log(ws);
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
ws.current &&
|
||||
(ws.current.readyState === ws.current.CLOSED ||
|
||||
ws.current.readyState === ws.current.CLOSING)
|
||||
) {
|
||||
connectWS();
|
||||
setLockChat(false);
|
||||
}
|
||||
}, [lockChat]);
|
||||
|
||||
async function sendAll(data: sendAllProps) {
|
||||
try {
|
||||
if (ws) {
|
||||
ws.current.send(JSON.stringify(data));
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorData({
|
||||
title: "There was an error sending the message",
|
||||
list: [error.message],
|
||||
});
|
||||
setChatValue(data.message);
|
||||
connectWS();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, [chatHistory]);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
function sendMessage() {
|
||||
if (chatValue !== "") {
|
||||
let nodeValidationErrors = validateNodes(reactFlowInstance);
|
||||
if (nodeValidationErrors.length === 0) {
|
||||
setLockChat(true);
|
||||
let message = chatValue;
|
||||
setChatValue("");
|
||||
addChatHistory(message, true);
|
||||
sendAll({
|
||||
...reactFlowInstance.toObject(),
|
||||
message,
|
||||
chatHistory,
|
||||
name: flow.name,
|
||||
description: flow.description,
|
||||
});
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed some required information:",
|
||||
list: nodeValidationErrors,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Error sending message",
|
||||
list: ["The message cannot be empty."],
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearChat() {
|
||||
setChatHistory([]);
|
||||
ws.current.send(JSON.stringify({ clear_history: true }));
|
||||
if (lockChat) setLockChat(false);
|
||||
}
|
||||
|
||||
function setModalOpen(x: boolean) {
|
||||
setOpen(x);
|
||||
}
|
||||
return (
|
||||
<Transition.Root show={open} appear={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black backdrop-blur-sm dark:bg-gray-600 dark:bg-opacity-80 bg-opacity-80 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className=" drop-shadow-2xl relative flex flex-col justify-between transform h-[95%] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all w-[690px]">
|
||||
<div className="relative w-full p-4">
|
||||
<button
|
||||
onClick={() => clearChat()}
|
||||
className="absolute top-2 right-10 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||
>
|
||||
<Eraser className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModalOpen(false)}
|
||||
className="absolute top-1.5 right-2 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={messagesRef}
|
||||
className="w-full h-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center overflow-scroll scrollbar-hide"
|
||||
>
|
||||
{chatHistory.length > 0 ? (
|
||||
chatHistory.map((c, i) => (
|
||||
<ChatMessage
|
||||
lockChat={lockChat}
|
||||
chat={c}
|
||||
lastMessage={chatHistory.length - 1 == i ? true : false}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col h-full text-center justify-center w-full items-center align-middle">
|
||||
<span>
|
||||
👋{" "}
|
||||
<span className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
LangFlow Chat
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="bg-muted dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-base text-gray-500">
|
||||
Start a conversation and click the agent’s thoughts{" "}
|
||||
<span>
|
||||
<MessagesSquare className="w-5 h-5 inline animate-bounce mx-1 " />
|
||||
</span>{" "}
|
||||
to inspect the chaining process.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={ref}></div>
|
||||
</div>
|
||||
<div className="w-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex-col flex items-center justify-between p-3">
|
||||
<div className="relative w-full mt-1 rounded-md shadow-sm">
|
||||
<ChatInput
|
||||
chatValue={chatValue}
|
||||
lockChat={lockChat}
|
||||
sendMessage={sendMessage}
|
||||
setChatValue={setChatValue}
|
||||
inputRef={ref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { classNames } from "../../../utils";
|
|||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
import { INPUT_STYLE } from "../../../constants";
|
||||
import { Lock, Send } from "lucide-react";
|
||||
import { Eraser, Lock, LucideSend, Send } from "lucide-react";
|
||||
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
|
|
@ -58,16 +58,16 @@ export default function ChatInput({
|
|||
)}
|
||||
placeholder={"Send a message..."}
|
||||
/>
|
||||
<div className="absolute bottom-2.5 right-4">
|
||||
<button disabled={lockChat} onClick={() => sendMessage()}>
|
||||
<div className="absolute bottom-2 right-4">
|
||||
<button className={classNames("p-2 px-1 transition-all duration-300 rounded-md",chatValue == "" ? "text-primary" : " bg-indigo-600 text-background")} disabled={lockChat} onClick={() => sendMessage()}>
|
||||
{lockChat ? (
|
||||
<Lock
|
||||
className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse"
|
||||
className="h-5 w-5 ml-1 mr-11 animate-pulse"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Send
|
||||
className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
<LucideSend
|
||||
className="h-5 w-5 mr-2 rotate-[44deg] "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ export default function ChatMessage({
|
|||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"w-full py-2 px-2 pl-4 flex",
|
||||
"w-full py-2 px-2 pl-4 pr-9 flex",
|
||||
chat.isSend
|
||||
? " dark:bg-gray-900 "
|
||||
: " dark:bg-gray-800"
|
||||
? " bg-border"
|
||||
: " "
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { validateNodes } from "../../utils";
|
||||
import { classNames, validateNodes } from "../../utils";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import ChatMessage from "./chatMessage";
|
||||
import { X, MessagesSquare, Eraser, TerminalSquare, MessageCircle, MessageSquareDashed, MessageSquare } from "lucide-react";
|
||||
import { TerminalSquare, MessageSquare, Variable, Eraser } from "lucide-react";
|
||||
import { sendAllProps } from "../../types/api";
|
||||
import { ChatMessageType } from "../../types/chat";
|
||||
import ChatInput from "./chatInput";
|
||||
|
|
@ -15,16 +14,11 @@ import {
|
|||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { dark } from "@mui/material/styles/createPalette";
|
||||
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants";
|
||||
import { postValidateCode } from "../../controllers/API";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import {
|
||||
|
|
@ -33,11 +27,9 @@ import {
|
|||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "../../components/ui/accordion";
|
||||
import { AccordionHeader } from "@radix-ui/react-accordion";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Separator } from "../../components/ui/separator";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import ToggleShadComponent from "../../components/toggleShadComponent";
|
||||
|
||||
export default function FormModal({
|
||||
flow,
|
||||
|
|
@ -72,8 +64,13 @@ export default function FormModal({
|
|||
}, [open]);
|
||||
useEffect(() => {
|
||||
id.current = flow.id;
|
||||
setKeysValue(Array(tabsState[flow.id].formKeysData.input_keys.length).fill(""));
|
||||
}, [flow.id]);
|
||||
|
||||
if (tabsState[flow.id].formKeysData) {
|
||||
setKeysValue(
|
||||
Array(tabsState[flow.id].formKeysData.input_keys.length).fill("")
|
||||
);
|
||||
}
|
||||
}, [flow.id, tabsState[flow.id], tabsState[flow.id].formKeysData]);
|
||||
|
||||
var isStream = false;
|
||||
|
||||
|
|
@ -290,7 +287,7 @@ export default function FormModal({
|
|||
title: "There was an error sending the message",
|
||||
list: [error.message],
|
||||
});
|
||||
setChatValue(data.message);
|
||||
setChatValue(data.inputs);
|
||||
connectWS();
|
||||
}
|
||||
}
|
||||
|
|
@ -306,6 +303,22 @@ export default function FormModal({
|
|||
ref.current.focus();
|
||||
}
|
||||
}, [open]);
|
||||
function formatMessage(inputs: any): string {
|
||||
if (inputs) {
|
||||
// inputs is a object with the keys and values being input_keys and keysValue
|
||||
// so the formated message is a string with the keys and values separated by ": "
|
||||
let message = "";
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
// make key bold
|
||||
// dangerouslySetInnerHTML={{
|
||||
// __html: message.replace(/\n/g, "<br>"),
|
||||
// }}
|
||||
message += `<b>${key}</b>: ${value}\n`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (chatValue !== "") {
|
||||
|
|
@ -314,16 +327,17 @@ export default function FormModal({
|
|||
setLockChat(true);
|
||||
// Message variable makes a object with the keys being the names from tabsState[flow.id].formKeysData.input_keys and the values being the keysValue of the correspondent index
|
||||
let keys = tabsState[flow.id].formKeysData.input_keys; // array of keys
|
||||
let values = keysValue.map((k, i) => i == chatKey ? chatValue : k); // array of values
|
||||
let message = keys.reduce((object, key, index) => {
|
||||
let values = keysValue.map((k, i) => (i == chatKey ? chatValue : k)); // array of values
|
||||
let inputs = keys.reduce((object, key, index) => {
|
||||
object[key] = values[index];
|
||||
return object;
|
||||
}, {});
|
||||
setChatValue("");
|
||||
addChatHistory(message.toString(), true);
|
||||
const message = formatMessage(inputs);
|
||||
addChatHistory(message, true);
|
||||
sendAll({
|
||||
...reactFlowInstance.toObject(),
|
||||
message,
|
||||
inputs: inputs,
|
||||
chatHistory,
|
||||
name: flow.name,
|
||||
description: flow.description,
|
||||
|
|
@ -352,103 +366,134 @@ export default function FormModal({
|
|||
}
|
||||
|
||||
function handleOnCheckedChange(checked: boolean, index: number) {
|
||||
if(checked === true){
|
||||
if (checked === true) {
|
||||
setChatKey(index);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger className="hidden"></DialogTrigger>
|
||||
<DialogContent className="min-w-[95vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Chat Form</span>
|
||||
<TerminalSquare
|
||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{tabsState[flow.id].formKeysData && (
|
||||
<DialogContent className="min-w-[80vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Chat</span>
|
||||
<TerminalSquare
|
||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{CHAT_FORM_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex h-[80vh] w-full mt-2">
|
||||
<div className="w-2/5 h-full flex flex-col justify-start mr-6">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{tabsState[id.current].formKeysData.input_keys.map((i, k) => (
|
||||
<AccordionItem key={k} value={i}>
|
||||
<AccordionTrigger>{i}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="p-1 flex flex-col gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="airplane-mode" checked={chatKey === k } onCheckedChange={(value) => handleOnCheckedChange(value, k)}/>
|
||||
<Label htmlFor="airplane-mode">From Chat</Label>
|
||||
</div>
|
||||
<Textarea value={keysValue[k]} onChange={(e) => setKeysValue({...keysValue, [k]: e.target.value})} disabled={chatKey === k} placeholder="Enter text..."></Textarea>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
{tabsState[id.current].formKeysData.memory_keys.map((i, k) => (
|
||||
<AccordionItem key={k} value={i}>
|
||||
<div className="flex flex-1 items-center justify-between py-4 font-medium transition-all group">
|
||||
<div className="group-hover:underline">{i}</div>
|
||||
<Badge>Memory Key</Badge>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
<div className="w-full ">
|
||||
<div className="flex flex-col rounded-md border bg-muted w-full h-full">
|
||||
<div
|
||||
ref={messagesRef}
|
||||
className="w-full h-full flex-col flex items-center overflow-scroll scrollbar-hide"
|
||||
>
|
||||
{chatHistory.length > 0 ? (
|
||||
chatHistory.map((c, i) => (
|
||||
<ChatMessage
|
||||
lockChat={lockChat}
|
||||
chat={c}
|
||||
lastMessage={chatHistory.length - 1 == i ? true : false}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col h-full text-center justify-center w-full items-center align-middle">
|
||||
<span>
|
||||
👋{" "}
|
||||
<span className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
LangFlow Chat
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="bg-muted dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-base text-gray-500">
|
||||
Start a conversation and click the agent's thoughts{" "}
|
||||
<span>
|
||||
<MessageSquare className="w-5 h-5 inline animate-bounce mx-1 " />
|
||||
</span>{" "}
|
||||
to inspect the chaining process.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={ref}></div>
|
||||
<div className="flex h-[80vh] w-full mt-2">
|
||||
<div className="w-1/4 h-full flex flex-col justify-start mr-6">
|
||||
<div className="flex py-2">
|
||||
<Variable className="w-6 h-6 pe-1 text-gray-700 stroke-2 dark:text-slate-200"></Variable>
|
||||
<span className="text-md font-semibold text-gray-800 dark:text-white">
|
||||
Input Variables
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full px-8 pb-6 flex-col flex items-center justify-between">
|
||||
<div className="relative w-full rounded-md shadow-sm">
|
||||
<ChatInput
|
||||
chatValue={chatValue}
|
||||
lockChat={lockChat}
|
||||
sendMessage={sendMessage}
|
||||
setChatValue={setChatValue}
|
||||
inputRef={ref}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{tabsState[id.current].formKeysData.input_keys.map((i, k) => (
|
||||
<AccordionItem key={k} value={i}>
|
||||
<AccordionTrigger><Badge variant="gray" size="md">{i}</Badge></AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="p-1 flex flex-col gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="airplane-mode" className="-mt-1">From Chat</Label>
|
||||
<ToggleShadComponent
|
||||
enabled={chatKey === k}
|
||||
setEnabled={(value) =>
|
||||
handleOnCheckedChange(value, k)
|
||||
}
|
||||
size="small"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
value={keysValue[k]}
|
||||
onChange={(e) =>
|
||||
setKeysValue((old) => [...old.slice(0, k), e.target.value, ...old.slice(k + 1)])
|
||||
}
|
||||
disabled={chatKey === k}
|
||||
placeholder="Enter text..."
|
||||
></Textarea>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
{tabsState[id.current].formKeysData.memory_keys.map((i, k) => (
|
||||
<AccordionItem key={k} value={i}>
|
||||
<div className="flex flex-1 items-center justify-between py-4 font-normal transition-all group text-muted-foreground text-sm">
|
||||
<div className="group-hover:underline"><Badge size="md" variant="gray">{i}</Badge></div>
|
||||
Used as Memory Key
|
||||
</div>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col rounded-md border bg-muted w-full h-full relative">
|
||||
<div className="absolute right-3 top-3 z-50">
|
||||
<button disabled={lockChat} onClick={() => clearChat()}>
|
||||
<Eraser
|
||||
className={classNames("h-5 w-5", lockChat ? "text-primary animate-pulse" : "text-primary hover:text-gray-600")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={messagesRef}
|
||||
className="w-full h-full flex-col flex items-center overflow-scroll scrollbar-hide"
|
||||
>
|
||||
{chatHistory.length > 0 ? (
|
||||
chatHistory.map((c, i) => (
|
||||
<ChatMessage
|
||||
lockChat={lockChat}
|
||||
chat={c}
|
||||
lastMessage={chatHistory.length - 1 == i ? true : false}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col h-full text-center justify-center w-full items-center align-middle">
|
||||
<span>
|
||||
👋{" "}
|
||||
<span className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
LangFlow Chat
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="bg-muted dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-base text-gray-500">
|
||||
Start a conversation and click the agent's thoughts{" "}
|
||||
<span>
|
||||
<MessageSquare className="w-5 h-5 inline animate-bounce mx-1 " />
|
||||
</span>{" "}
|
||||
to inspect the chaining process.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={ref}></div>
|
||||
</div>
|
||||
<div className="w-full px-8 pb-6 flex-col flex items-center justify-between">
|
||||
<div className="relative w-full rounded-md shadow-sm">
|
||||
<ChatInput
|
||||
chatValue={chatValue}
|
||||
lockChat={lockChat}
|
||||
sendMessage={sendMessage}
|
||||
setChatValue={setChatValue}
|
||||
inputRef={ref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogContent>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ const NodeToolbarComponent = (props) => {
|
|||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
// console.log(reactFlowInstance.getNode(props.data.id));
|
||||
paste(
|
||||
{
|
||||
nodes: [reactFlowInstance.getNode(props.data.id)],
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export type TemplateVariableType = {
|
|||
show: boolean;
|
||||
multiline?: boolean;
|
||||
value?: any;
|
||||
input_types?: Array<string>;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type sendAllProps = {
|
||||
|
|
@ -31,7 +32,7 @@ export type sendAllProps = {
|
|||
name: string;
|
||||
description: string;
|
||||
viewport: Viewport;
|
||||
message: any;
|
||||
inputs: any;
|
||||
|
||||
chatHistory: { message: string; isSend: boolean }[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -656,7 +656,6 @@ export function isValidConnection(
|
|||
|
||||
export function removeApiKeys(flow: FlowType): FlowType {
|
||||
let cleanFLow = _.cloneDeep(flow);
|
||||
console.log(cleanFLow);
|
||||
cleanFLow.data.nodes.forEach((node) => {
|
||||
for (const key in node.data.node.template) {
|
||||
if (node.data.node.template[key].password) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue