Adding Building Progress Bar with DaisyUI and Radial Progress (#522)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-06-22 00:20:01 +00:00 • committed by GitHub
commit 05bd7b415a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 2378 additions and 1471 deletions

View file

@ -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")
graph = Graph.from_payload(graph_data) try:
# Some error could happen when building the graph
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")

View file

@ -1,6 +1,7 @@
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):
@ -101,3 +102,11 @@ class InitResponse(BaseModel):
class BuiltResponse(BaseModel): class BuiltResponse(BaseModel):
built: bool built: bool
class StreamData(BaseModel):
event: str
data: dict
def __str__(self) -> str:
return f"event: {self.event}\ndata: {json.dumps(self.data)}\n\n"

File diff suppressed because it is too large Load diff

View file

@ -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",

View 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} />;
}

View 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>
);
}

View file

@ -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" />
)} )}

View file

@ -70,35 +70,35 @@ export default function Header() {
</div> </div>
<div className="flex justify-end px-2 w-96"> <div className="flex justify-end px-2 w-96">
<div className="ml-auto mr-2 flex gap-5 items-center"> <div className="ml-auto mr-2 flex gap-5 items-center">
<a <a
href="https://github.com/logspace-ai/langflow" href="https://github.com/logspace-ai/langflow"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="inline-flex shadow-sm items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background text-gray-600 dark:text-gray-300 border border-input hover:bg-accent hover:text-accent-foreground h-9 px-3 pr-0 rounded-md" className="inline-flex shadow-sm items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background text-gray-600 dark:text-gray-300 border border-input hover:bg-accent hover:text-accent-foreground h-9 px-3 pr-0 rounded-md"
> >
<FaGithub className="h-5 w-5 mr-2" /> <FaGithub className="h-5 w-5 mr-2" />
Star Star
<div className="ml-2 flex text-sm bg-background rounded-md rounded-l-none border px-2 h-9 -mr-px items-center justify-center"> <div className="ml-2 flex text-sm bg-background rounded-md rounded-l-none border px-2 h-9 -mr-px items-center justify-center">
{stars} {stars}
</div> </div>
</a> </a>
<a <a
href="https://twitter.com/logspace_ai" href="https://twitter.com/logspace_ai"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-muted-foreground" className="text-muted-foreground"
> >
<FaTwitter className="h-5 w-5" /> <FaTwitter className="h-5 w-5" />
</a> </a>
<a <a
href="https://discord.gg/EqksyE2EX9" href="https://discord.gg/EqksyE2EX9"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-muted-foreground" className="text-muted-foreground"
> >
<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>) => {

View 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 };

View file

@ -22,7 +22,9 @@ const GITHUB_API_URL = "https://api.github.com";
export async function getRepoStars(owner, repo) { export async function getRepoStars(owner, repo) {
try { try {
const response = await axios.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`); const response = await axios.get(
`${GITHUB_API_URL}/repos/${owner}/${repo}`
);
return response.data.stargazers_count; return response.data.stargazers_count;
} catch (error) { } catch (error) {
console.error("Error fetching repository data:", error); console.error("Error fetching repository data:", error);

View file

@ -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;
};

View file

@ -241,5 +241,6 @@ module.exports = {
}); });
}), }),
require("@tailwindcss/typography"), require("@tailwindcss/typography"),
require("daisyui"),
], ],
}; };