✨ feat(chat.py): add error handling to stream_build function and improve log messages
✨ feat(schemas.py): add StreamData class to handle SSE messages ✨ feat(frontend): add RadialProgress and ProgressBar components 🔧 chore(frontend): add daisyui to project dependencies 🎨 style(headerComponent): comment out unused code ✨ feat(ui/progress.tsx): add Progress component to reuse in other components ✨ feat(types/components): add ProgressBarType and RadialProgressType to use in components 🎨 style(tailwind.config.js): add daisyui plugin to tailwind configuration to use in the project
This commit is contained in:
commit
f3e6db4df1
11 changed files with 2345 additions and 1442 deletions
|
|
@ -1,4 +1,3 @@
|
||||||
import json
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
|
|
@ -7,7 +6,7 @@ from fastapi import (
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from langflow.api.v1.schemas import BuiltResponse, InitResponse
|
from langflow.api.v1.schemas import BuiltResponse, InitResponse, StreamData
|
||||||
|
|
||||||
from langflow.chat.manager import ChatManager
|
from langflow.chat.manager import ChatManager
|
||||||
from langflow.graph.graph.base import Graph
|
from langflow.graph.graph.base import Graph
|
||||||
|
|
@ -71,30 +70,37 @@ async def stream_build(flow_id: str):
|
||||||
"""Stream the build process based on stored flow data."""
|
"""Stream the build process based on stored flow data."""
|
||||||
|
|
||||||
async def event_stream(flow_id):
|
async def event_stream(flow_id):
|
||||||
final_response = json.dumps({"end_of_stream": True})
|
final_response = {"end_of_stream": True}
|
||||||
try:
|
try:
|
||||||
if flow_id not in flow_data_store:
|
if flow_id not in flow_data_store:
|
||||||
error_message = "Invalid session ID"
|
error_message = "Invalid session ID"
|
||||||
yield f"data: {json.dumps({'error': error_message})}\n\n"
|
yield str(StreamData(event="error", data={"error": error_message}))
|
||||||
return
|
return
|
||||||
|
|
||||||
graph_data = flow_data_store[flow_id].get("data")
|
graph_data = flow_data_store[flow_id].get("data")
|
||||||
|
|
||||||
if not graph_data:
|
if not graph_data:
|
||||||
error_message = "No data provided"
|
error_message = "No data provided"
|
||||||
yield f"data: {json.dumps({'error': error_message})}\n\n"
|
yield str(StreamData(event="error", data={"error": error_message}))
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("Building langchain object")
|
logger.debug("Building langchain object")
|
||||||
|
try:
|
||||||
|
# Some error could happen when building the graph
|
||||||
graph = Graph.from_payload(graph_data)
|
graph = Graph.from_payload(graph_data)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception(exc)
|
||||||
|
error_message = str(exc)
|
||||||
|
yield str(StreamData(event="error", data={"error": error_message}))
|
||||||
|
return
|
||||||
|
|
||||||
number_of_nodes = len(graph.nodes)
|
number_of_nodes = len(graph.nodes)
|
||||||
for i, vertex in enumerate(graph.generator_build(), 1):
|
for i, vertex in enumerate(graph.generator_build(), 1):
|
||||||
try:
|
try:
|
||||||
log_dict = {
|
log_dict = {
|
||||||
"log": f"Building node {vertex.vertex_type}",
|
"log": f"Building node {vertex.vertex_type}",
|
||||||
"progress": round(i / number_of_nodes, 2),
|
|
||||||
}
|
}
|
||||||
yield f"data: {json.dumps(log_dict)}\n\n"
|
yield str(StreamData(event="log", data=log_dict))
|
||||||
vertex.build()
|
vertex.build()
|
||||||
params = vertex._built_object_repr()
|
params = vertex._built_object_repr()
|
||||||
valid = True
|
valid = True
|
||||||
|
|
@ -105,21 +111,21 @@ async def stream_build(flow_id: str):
|
||||||
params = str(exc)
|
params = str(exc)
|
||||||
valid = False
|
valid = False
|
||||||
|
|
||||||
response = json.dumps(
|
response = {
|
||||||
{
|
|
||||||
"valid": valid,
|
"valid": valid,
|
||||||
"params": params,
|
"params": params,
|
||||||
"id": vertex.id,
|
"id": vertex.id,
|
||||||
|
"progress": round(i / number_of_nodes, 2),
|
||||||
}
|
}
|
||||||
)
|
|
||||||
yield f"data: {response}\n\n"
|
yield str(StreamData(event="message", data=response))
|
||||||
|
|
||||||
chat_manager.set_cache(flow_id, graph.build())
|
chat_manager.set_cache(flow_id, graph.build())
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Error while building the flow: %s", exc)
|
logger.error("Error while building the flow: %s", exc)
|
||||||
yield f"error: {json.dumps({'error': str(exc)})}\n\n"
|
yield str(StreamData(event="error", data={"error": str(exc)}))
|
||||||
finally:
|
finally:
|
||||||
yield f"data: {final_response}\n\n"
|
yield str(StreamData(event="message", data=final_response))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")
|
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
from langflow.database.models.flow import FlowCreate, FlowRead
|
from langflow.database.models.flow import FlowCreate, FlowRead
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class GraphData(BaseModel):
|
class GraphData(BaseModel):
|
||||||
|
|
@ -97,3 +98,9 @@ class UploadFileResponse(BaseModel):
|
||||||
|
|
||||||
flowId: str
|
flowId: str
|
||||||
file_path: Path
|
file_path: Path
|
||||||
|
class StreamData(BaseModel):
|
||||||
|
event: str
|
||||||
|
data: dict
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"event: {self.event}\ndata: {json.dumps(self.data)}\n\n"
|
||||||
|
|
|
||||||
3600
src/frontend/package-lock.json
generated
3600
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,15 +8,16 @@
|
||||||
"@headlessui/react": "^1.7.10",
|
"@headlessui/react": "^1.7.10",
|
||||||
"@heroicons/react": "^2.0.15",
|
"@heroicons/react": "^2.0.15",
|
||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.9",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
|
||||||
"@radix-ui/react-menubar": "^1.0.3",
|
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-menubar": "^1.0.3",
|
||||||
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@tabler/icons-react": "^2.18.0",
|
"@tabler/icons-react": "^2.18.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
|
|
@ -98,6 +99,7 @@
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
"daisyui": "^3.1.1",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
|
|
|
||||||
21
src/frontend/src/components/ProgressBarComponent/index.tsx
Normal file
21
src/frontend/src/components/ProgressBarComponent/index.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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} />;
|
||||||
|
}
|
||||||
19
src/frontend/src/components/RadialProgress/index.tsx
Normal file
19
src/frontend/src/components/RadialProgress/index.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { RadialProgressType } from "../../types/components";
|
||||||
|
|
||||||
|
export default function RadialProgressComponent({
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
}: RadialProgressType) {
|
||||||
|
const style = {
|
||||||
|
"--value": value * 100,
|
||||||
|
"--size": "1.5rem",
|
||||||
|
"--thickness": "2px",
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"radial-progress " + color} style={style}>
|
||||||
|
<strong className="text-[8px]">{value * 100}%</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useContext } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
import { Zap } from "lucide-react";
|
import { Zap } from "lucide-react";
|
||||||
import { validateNodes } from "../../../utils";
|
import { validateNodes } from "../../../utils";
|
||||||
|
|
@ -9,6 +9,8 @@ import { typesContext } from "../../../contexts/typesContext";
|
||||||
import { alertContext } from "../../../contexts/alertContext";
|
import { alertContext } from "../../../contexts/alertContext";
|
||||||
import { postBuildInit } from "../../../controllers/API";
|
import { postBuildInit } from "../../../controllers/API";
|
||||||
|
|
||||||
|
import RadialProgressComponent from "../../RadialProgress";
|
||||||
|
|
||||||
export default function BuildTrigger({
|
export default function BuildTrigger({
|
||||||
open,
|
open,
|
||||||
flow,
|
flow,
|
||||||
|
|
@ -20,9 +22,12 @@ export default function BuildTrigger({
|
||||||
setIsBuilt: any;
|
setIsBuilt: any;
|
||||||
isBuilt: boolean;
|
isBuilt: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { updateSSEData, isBuilding, setIsBuilding } = useSSE();
|
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
|
||||||
const { reactFlowInstance } = useContext(typesContext);
|
const { reactFlowInstance } = useContext(typesContext);
|
||||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||||
|
const [isIconTouched, setIsIconTouched] = useState(false);
|
||||||
|
const eventClick = isBuilding ? "pointer-events-none" : "";
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
async function handleBuild(flow: FlowType) {
|
async function handleBuild(flow: FlowType) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -58,12 +63,10 @@ export default function BuildTrigger({
|
||||||
setIsBuilding(false);
|
setIsBuilding(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function streamNodeData(flow: FlowType) {
|
async function streamNodeData(flow: FlowType) {
|
||||||
// Step 1: Make a POST request to send the flow data and receive a unique session ID
|
// Step 1: Make a POST request to send the flow data and receive a unique session ID
|
||||||
const response = await postBuildInit(flow);
|
const response = await postBuildInit(flow);
|
||||||
const { flowId } = response.data;
|
const { flowId } = response.data;
|
||||||
|
|
||||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||||
let validationResults = [];
|
let validationResults = [];
|
||||||
let finished = false;
|
let finished = false;
|
||||||
|
|
@ -83,19 +86,23 @@ export default function BuildTrigger({
|
||||||
return;
|
return;
|
||||||
} else if (parsedData.log) {
|
} else if (parsedData.log) {
|
||||||
// If the event is a log, log it
|
// If the event is a log, log it
|
||||||
// TODO: implement the progress
|
|
||||||
setSuccessData({ title: parsedData.log });
|
setSuccessData({ title: parsedData.log });
|
||||||
setSuccessData({ title: parsedData.progress });
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, process the data
|
// Otherwise, process the data
|
||||||
const isValid = processStreamResult(parsedData);
|
const isValid = processStreamResult(parsedData);
|
||||||
|
setProgress(parsedData.progress);
|
||||||
validationResults.push(isValid);
|
validationResults.push(isValid);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
eventSource.onerror = (error: any) => {
|
||||||
console.error("EventSource failed:", error);
|
console.error("EventSource failed:", error);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
|
if (error.data) {
|
||||||
|
const parsedData = JSON.parse(error.data);
|
||||||
|
setErrorData({ title: parsedData.error });
|
||||||
|
setIsBuilding(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Step 3: Wait for the stream to finish
|
// Step 3: Wait for the stream to finish
|
||||||
while (!finished) {
|
while (!finished) {
|
||||||
|
|
@ -129,6 +136,14 @@ export default function BuildTrigger({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
setIsIconTouched(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
setIsIconTouched(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
show={!open}
|
show={!open}
|
||||||
|
|
@ -142,17 +157,23 @@ export default function BuildTrigger({
|
||||||
>
|
>
|
||||||
<div className={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}>
|
<div className={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}>
|
||||||
<div
|
<div
|
||||||
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
className={`${eventClick} 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`}
|
||||||
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleBuild(flow);
|
handleBuild(flow);
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<button>
|
<button>
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
{isBuilding ? (
|
{isBuilding && progress < 1 ? (
|
||||||
// Render your loading animation here when isBuilding is true
|
// Render your loading animation here when isBuilding is true
|
||||||
<Loading strokeWidth={1.5} style={{ color: "white" }} />
|
<RadialProgressComponent
|
||||||
|
color={"text-orange-400"}
|
||||||
|
value={progress}
|
||||||
|
></RadialProgressComponent>
|
||||||
|
) : isBuilding ? (
|
||||||
|
<Loading strokeWidth={1.5} style={{ color: "#fb923c" }} />
|
||||||
) : (
|
) : (
|
||||||
<Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" />
|
<Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export default function Header() {
|
||||||
>
|
>
|
||||||
<FaDiscord className="h-5 w-5" />
|
<FaDiscord className="h-5 w-5" />
|
||||||
</a>
|
</a>
|
||||||
<Separator orientation="vertical" />
|
{/* <Separator orientation="vertical" />
|
||||||
<button
|
<button
|
||||||
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -110,7 +110,7 @@ export default function Header() {
|
||||||
) : (
|
) : (
|
||||||
<MoonIcon className="h-5 w-5" />
|
<MoonIcon className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button> */}
|
||||||
<button
|
<button
|
||||||
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative"
|
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative"
|
||||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
|
|
||||||
27
src/frontend/src/components/ui/progress.tsx
Normal file
27
src/frontend/src/components/ui/progress.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||||
|
import { cn } from "../../utils";
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
));
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
|
|
@ -98,3 +98,14 @@ export type TooltipComponentType = {
|
||||||
| "top-start"
|
| "top-start"
|
||||||
| "top";
|
| "top";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProgressBarType = {
|
||||||
|
children?: ReactElement;
|
||||||
|
value?: number;
|
||||||
|
max?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RadialProgressType = {
|
||||||
|
value?: number;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -241,5 +241,6 @@ module.exports = {
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
require("@tailwindcss/typography"),
|
require("@tailwindcss/typography"),
|
||||||
|
require("daisyui"),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue