mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into aryshare-revid
This commit is contained in:
@@ -24,6 +24,7 @@ from prisma.models import (
|
||||
)
|
||||
from prisma.types import (
|
||||
AgentGraphExecutionCreateInput,
|
||||
AgentGraphExecutionUpdateManyMutationInput,
|
||||
AgentGraphExecutionWhereInput,
|
||||
AgentNodeExecutionCreateInput,
|
||||
AgentNodeExecutionInputOutputCreateInput,
|
||||
@@ -572,9 +573,15 @@ async def update_graph_execution_stats(
|
||||
status: ExecutionStatus,
|
||||
stats: GraphExecutionStats | None = None,
|
||||
) -> GraphExecution | None:
|
||||
data = stats.model_dump() if stats else {}
|
||||
if isinstance(data.get("error"), Exception):
|
||||
data["error"] = str(data["error"])
|
||||
update_data: AgentGraphExecutionUpdateManyMutationInput = {
|
||||
"executionStatus": status
|
||||
}
|
||||
|
||||
if stats:
|
||||
stats_dict = stats.model_dump()
|
||||
if isinstance(stats_dict.get("error"), Exception):
|
||||
stats_dict["error"] = str(stats_dict["error"])
|
||||
update_data["stats"] = Json(stats_dict)
|
||||
|
||||
updated_count = await AgentGraphExecution.prisma().update_many(
|
||||
where={
|
||||
@@ -584,10 +591,7 @@ async def update_graph_execution_stats(
|
||||
{"executionStatus": ExecutionStatus.QUEUED},
|
||||
],
|
||||
},
|
||||
data={
|
||||
"executionStatus": status,
|
||||
"stats": Json(data),
|
||||
},
|
||||
data=update_data,
|
||||
)
|
||||
if updated_count == 0:
|
||||
return None
|
||||
|
||||
@@ -660,11 +660,15 @@ async def _cancel_execution(graph_exec_id: str):
|
||||
exchange=execution_utils.GRAPH_EXECUTION_CANCEL_EXCHANGE,
|
||||
)
|
||||
|
||||
# Update the status of the graph & node executions
|
||||
await execution_db.update_graph_execution_stats(
|
||||
# Update the status of the graph execution
|
||||
graph_execution = await execution_db.update_graph_execution_stats(
|
||||
graph_exec_id,
|
||||
execution_db.ExecutionStatus.TERMINATED,
|
||||
)
|
||||
if graph_execution:
|
||||
await execution_event_bus().publish(graph_execution)
|
||||
|
||||
# Update the status of the node executions
|
||||
node_execs = [
|
||||
node_exec.model_copy(update={"status": execution_db.ExecutionStatus.TERMINATED})
|
||||
for node_exec in await execution_db.get_node_executions(
|
||||
@@ -676,7 +680,6 @@ async def _cancel_execution(graph_exec_id: str):
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
await execution_db.update_node_execution_status_batch(
|
||||
[node_exec.node_exec_id for node_exec in node_execs],
|
||||
execution_db.ExecutionStatus.TERMINATED,
|
||||
|
||||
@@ -39,9 +39,11 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { LoadingSpinner } from "@/components/ui/loading";
|
||||
|
||||
export default function AgentRunsPage(): React.ReactElement {
|
||||
const { id: agentID }: { id: LibraryAgentID } = useParams();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
|
||||
@@ -69,7 +71,6 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
const { state: onboardingState, updateState: updateOnboardingState } =
|
||||
useOnboarding();
|
||||
const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const openRunDraftView = useCallback(() => {
|
||||
selectView({ type: "run" });
|
||||
@@ -120,7 +121,11 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
}
|
||||
}, [selectedRun, onboardingState, updateOnboardingState]);
|
||||
|
||||
const lastRefresh = useRef<number>(0);
|
||||
const refreshPageData = useCallback(() => {
|
||||
if (Date.now() - lastRefresh.current < 2e3) return; // 2 second debounce
|
||||
lastRefresh.current = Date.now();
|
||||
|
||||
api.getLibraryAgent(agentID).then((agent) => {
|
||||
setAgent(agent);
|
||||
|
||||
@@ -156,6 +161,44 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
refreshPageData();
|
||||
|
||||
// Show a toast when the WebSocket connection disconnects
|
||||
let connectionToast: ReturnType<typeof toast> | null = null;
|
||||
const cancelDisconnectHandler = api.onWebSocketDisconnect(() => {
|
||||
connectionToast ??= toast({
|
||||
title: "Connection to server was lost",
|
||||
variant: "destructive",
|
||||
description: (
|
||||
<div className="flex items-center">
|
||||
Trying to reconnect...
|
||||
<LoadingSpinner className="ml-1.5 size-3.5" />
|
||||
</div>
|
||||
),
|
||||
duration: Infinity, // show until connection is re-established
|
||||
dismissable: false,
|
||||
});
|
||||
});
|
||||
const cancelConnectHandler = api.onWebSocketConnect(() => {
|
||||
if (connectionToast)
|
||||
connectionToast.update({
|
||||
id: connectionToast.id,
|
||||
title: "✅ Connection re-established",
|
||||
variant: "default",
|
||||
description: (
|
||||
<div className="flex items-center">
|
||||
Refreshing data...
|
||||
<LoadingSpinner className="ml-1.5 size-3.5" />
|
||||
</div>
|
||||
),
|
||||
duration: 2000,
|
||||
dismissable: true,
|
||||
});
|
||||
connectionToast = null;
|
||||
});
|
||||
return () => {
|
||||
cancelDisconnectHandler();
|
||||
cancelConnectHandler();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Subscribe to WebSocket updates for agent runs
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
@@ -98,7 +98,7 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
if (isUserLoading || user) {
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
return <LoadingBox className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
|
||||
export default function PrivatePage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
@@ -123,7 +123,7 @@ export default function PrivatePage() {
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
return <LoadingBox className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!user || !supabase) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useCallback, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { changePassword, sendResetEmail } from "./actions";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import { getBehaveAs } from "@/lib/utils";
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
return <LoadingBox className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
@@ -94,7 +94,7 @@ export default function SignupPage() {
|
||||
}
|
||||
|
||||
if (isUserLoading || user) {
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
return <LoadingBox className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import "./globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { Providers } from "@/app/providers";
|
||||
import TallyPopupSimple from "@/components/TallyPopup";
|
||||
import OttoChatWidget from "@/components/OttoChatWidget";
|
||||
import { GoogleAnalytics } from "@/components/analytics/google-analytics";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
@@ -57,9 +56,6 @@ export default async function RootLayout({
|
||||
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
|
||||
{children}
|
||||
<TallyPopupSimple />
|
||||
<Suspense fallback={null}>
|
||||
<OttoChatWidget />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Toaster />
|
||||
</Providers>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
MouseEvent,
|
||||
createContext,
|
||||
Suspense,
|
||||
} from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
@@ -48,6 +49,7 @@ import RunnerUIWrapper, {
|
||||
RunnerUIWrapperRef,
|
||||
} from "@/components/RunnerUIWrapper";
|
||||
import PrimaryActionBar from "@/components/PrimaryActionButton";
|
||||
import OttoChatWidget from "@/components/OttoChatWidget";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useCopyPaste } from "../hooks/useCopyPaste";
|
||||
import { CronScheduler } from "./cronScheduler";
|
||||
@@ -676,7 +678,7 @@ const FlowEditor: React.FC<{
|
||||
<Controls />
|
||||
<Background className="dark:bg-slate-800" />
|
||||
<ControlPanel
|
||||
className="absolute z-10"
|
||||
className="absolute z-20"
|
||||
controls={editorControls}
|
||||
topChildren={
|
||||
<BlocksControl
|
||||
@@ -701,6 +703,7 @@ const FlowEditor: React.FC<{
|
||||
}
|
||||
></ControlPanel>
|
||||
<PrimaryActionBar
|
||||
className="absolute bottom-0 left-1/2 z-20 -translate-x-1/2"
|
||||
onClickAgentOutputs={() => runnerUIRef.current?.openRunnerOutput()}
|
||||
onClickRunAgent={() => {
|
||||
if (!savedAgent) {
|
||||
@@ -740,6 +743,12 @@ const FlowEditor: React.FC<{
|
||||
scheduleRunner={scheduleRunner}
|
||||
requestSaveAndRun={requestSaveAndRun}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<OttoChatWidget
|
||||
graphID={flowID}
|
||||
className="fixed bottom-4 right-4 z-20"
|
||||
/>
|
||||
</Suspense>
|
||||
</FlowContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useSearchParams, usePathname } from "next/navigation";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import useAgentGraph from "../hooks/useAgentGraph";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { GraphID } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
import type { GraphID } from "@/lib/autogpt-server-api/types";
|
||||
import { askOtto } from "@/app/(platform)/build/actions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Message {
|
||||
type: "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
const OttoChatWidget = () => {
|
||||
export default function OttoChatWidget({
|
||||
graphID,
|
||||
className,
|
||||
}: {
|
||||
graphID?: GraphID;
|
||||
className?: string;
|
||||
}): React.ReactNode {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [includeGraphData, setIncludeGraphData] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const flowID = searchParams.get("flowID");
|
||||
const { nodes, edges } = useAgentGraph(
|
||||
flowID ? (flowID as GraphID) : undefined,
|
||||
);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
// Add welcome message when component mounts
|
||||
@@ -34,7 +32,7 @@ const OttoChatWidget = () => {
|
||||
setMessages([
|
||||
{
|
||||
type: "assistant",
|
||||
content: "Hello im Otto! Ask me anything about AutoGPT!",
|
||||
content: "Hello, I am Otto! Ask me anything about AutoGPT!",
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -84,7 +82,7 @@ const OttoChatWidget = () => {
|
||||
userMessage,
|
||||
conversationHistory,
|
||||
includeGraphData,
|
||||
flowID || undefined,
|
||||
graphID,
|
||||
);
|
||||
|
||||
// Check if the response contains an error
|
||||
@@ -131,13 +129,13 @@ const OttoChatWidget = () => {
|
||||
};
|
||||
|
||||
// Don't render the chat widget if we're not on the build page or in local mode
|
||||
if (process.env.NEXT_PUBLIC_BEHAVE_AS !== "CLOUD" || pathname !== "/build") {
|
||||
if (process.env.NEXT_PUBLIC_BEHAVE_AS !== "CLOUD") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50">
|
||||
<div className={className}>
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="inline-flex h-14 w-14 items-center justify-center whitespace-nowrap rounded-2xl bg-[rgba(65,65,64,1)] text-neutral-50 shadow transition-colors hover:bg-neutral-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90 dark:focus-visible:ring-neutral-300"
|
||||
@@ -160,7 +158,13 @@ const OttoChatWidget = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 flex h-[600px] w-[600px] flex-col rounded-lg border bg-background shadow-xl">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[600px] w-[600px] flex-col rounded-lg border bg-background shadow-xl",
|
||||
className,
|
||||
"z-40",
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<h2 className="font-semibold">Otto Assistant</h2>
|
||||
@@ -269,7 +273,7 @@ const OttoChatWidget = () => {
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
{nodes && edges && (
|
||||
{graphID && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -303,6 +307,4 @@ const OttoChatWidget = () => {
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OttoChatWidget;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Clock, LogOut, ChevronLeft } from "lucide-react";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
import { Clock, LogOut } from "lucide-react";
|
||||
import { IconPlay, IconSquare } from "@/components/ui/icons";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
|
||||
interface PrimaryActionBarProps {
|
||||
onClickAgentOutputs: () => void;
|
||||
@@ -18,6 +19,7 @@ interface PrimaryActionBarProps {
|
||||
isScheduling: boolean;
|
||||
requestStopRun: () => void;
|
||||
runAgentTooltip: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
@@ -29,6 +31,7 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
isScheduling,
|
||||
requestStopRun,
|
||||
runAgentTooltip,
|
||||
className,
|
||||
}) => {
|
||||
const runButtonLabel = !isRunning ? "Run" : "Stop";
|
||||
|
||||
@@ -37,8 +40,13 @@ const PrimaryActionBar: React.FC<PrimaryActionBarProps> = ({
|
||||
const runButtonOnClick = !isRunning ? onClickRunAgent : requestStopRun;
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-0 left-1/2 z-50 flex w-fit -translate-x-1/2 transform select-none items-center justify-center p-4">
|
||||
<div className={`flex gap-1 md:gap-4`}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-fit select-none items-center justify-center p-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-1 md:gap-4">
|
||||
<Tooltip key="ViewOutputs" delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
export default function Spinner({ className }: { className?: string }) {
|
||||
const spinnerClasses = `mr-2 h-16 w-16 animate-spin ${className || ""}`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<LoaderCircle className={spinnerClasses} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ const TallyPopupSimple = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-1 right-24 z-50 hidden select-none items-center gap-4 p-3 transition-all duration-300 ease-in-out md:flex">
|
||||
<div className="fixed bottom-1 right-24 z-20 hidden select-none items-center gap-4 p-3 transition-all duration-300 ease-in-out md:flex">
|
||||
{show_tutorial && (
|
||||
<Button
|
||||
variant="default"
|
||||
|
||||
@@ -133,7 +133,8 @@ export default function AgentRunDetailsView({
|
||||
| null
|
||||
| undefined = useMemo(() => {
|
||||
if (!("outputs" in run)) return undefined;
|
||||
if (!["running", "success", "failed"].includes(runStatus)) return null;
|
||||
if (!["running", "success", "failed", "stopped"].includes(runStatus))
|
||||
return null;
|
||||
|
||||
// Add type info from agent input schema
|
||||
return Object.fromEntries(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { LoadingSpinner } from "@/components/ui/loading";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import Spinner from "../Spinner";
|
||||
|
||||
const variants = {
|
||||
default: "bg-zinc-700 hover:bg-zinc-800",
|
||||
@@ -55,7 +55,7 @@ export default function OnboardingButton({
|
||||
if (href && !disabled) {
|
||||
return (
|
||||
<Link href={href} onClick={onClickInternal} className={buttonClasses}>
|
||||
{isLoading && <Spinner className="h-5 w-5" />}
|
||||
{isLoading && <LoadingSpinner className="mr-2 size-5" />}
|
||||
{icon && !isLoading && <>{icon}</>}
|
||||
{children}
|
||||
</Link>
|
||||
@@ -68,7 +68,7 @@ export default function OnboardingButton({
|
||||
disabled={disabled}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{isLoading && <Spinner className="h-5 w-5" />}
|
||||
{isLoading && <LoadingSpinner className="mr-2 size-5" />}
|
||||
{icon && !isLoading && <>{icon}</>}
|
||||
{children}
|
||||
</button>
|
||||
|
||||
26
autogpt_platform/frontend/src/components/ui/loading.tsx
Normal file
26
autogpt_platform/frontend/src/components/ui/loading.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
export default function LoadingBox({
|
||||
className,
|
||||
spinnerSize,
|
||||
}: {
|
||||
className?: string;
|
||||
spinnerSize?: string | number;
|
||||
}) {
|
||||
const spinnerSizeClass =
|
||||
typeof spinnerSize == "string"
|
||||
? `size-[${spinnerSize}]`
|
||||
: typeof spinnerSize == "number"
|
||||
? `size-${spinnerSize}`
|
||||
: undefined;
|
||||
return (
|
||||
<div className={cn("flex items-center justify-center", className)}>
|
||||
<LoadingSpinner className={spinnerSizeClass} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingSpinner({ className }: { className?: string }) {
|
||||
return <LoaderCircle className={cn("size-16 animate-spin", className)} />;
|
||||
}
|
||||
@@ -13,10 +13,18 @@ import { useToast } from "@/components/ui/use-toast";
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
// This neat little feature makes the toaster buggy due to the following issue:
|
||||
// https://github.com/radix-ui/primitives/issues/2233
|
||||
// TODO: Re-enable when the above issue is fixed:
|
||||
// const swipeThreshold = toasts.some((toast) => toast.dismissable === false)
|
||||
// ? Infinity
|
||||
// : undefined;
|
||||
const swipeThreshold = undefined;
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<ToastProvider swipeThreshold={swipeThreshold}>
|
||||
{toasts.map(
|
||||
({ id, title, description, action, dismissable, ...props }) => (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
@@ -25,10 +33,10 @@ export function Toaster() {
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
{dismissable !== false && <ToastClose />}
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
),
|
||||
)}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ type ToasterToast = ToastProps & {
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
dismissable?: boolean;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
|
||||
@@ -68,6 +68,7 @@ export default class BackendAPI {
|
||||
private webSocket: WebSocket | null = null;
|
||||
private wsConnecting: Promise<void> | null = null;
|
||||
private wsOnConnectHandlers: Set<() => void> = new Set();
|
||||
private wsOnDisconnectHandlers: Set<() => void> = new Set();
|
||||
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
|
||||
|
||||
readonly HEARTBEAT_INTERVAL = 100_000; // 100 seconds
|
||||
@@ -943,43 +944,69 @@ export default class BackendAPI {
|
||||
return () => this.wsOnConnectHandlers.delete(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* All handlers are invoked when the WebSocket disconnects.
|
||||
*
|
||||
* @returns a detacher for the passed handler.
|
||||
*/
|
||||
onWebSocketDisconnect(handler: () => void): () => void {
|
||||
this.wsOnDisconnectHandlers.add(handler);
|
||||
|
||||
// Return detacher
|
||||
return () => this.wsOnDisconnectHandlers.delete(handler);
|
||||
}
|
||||
|
||||
async connectWebSocket(): Promise<void> {
|
||||
this.wsConnecting ??= new Promise(async (resolve, reject) => {
|
||||
return (this.wsConnecting ??= new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const token =
|
||||
(await this.supabaseClient?.auth.getSession())?.data.session
|
||||
?.access_token || "";
|
||||
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
|
||||
this.webSocket = new WebSocket(wsUrlWithToken);
|
||||
this.webSocket.state = "connecting";
|
||||
|
||||
this.webSocket.onopen = () => {
|
||||
this.webSocket!.state = "connected";
|
||||
console.info("[BackendAPI] WebSocket connected to", this.wsUrl);
|
||||
this._startWSHeartbeat(); // Start heartbeat when connection opens
|
||||
this.wsOnConnectHandlers.forEach((handler) => handler());
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.webSocket.onclose = (event) => {
|
||||
console.warn("WebSocket connection closed", event);
|
||||
if (this.webSocket?.state == "connecting") {
|
||||
console.error(
|
||||
`[BackendAPI] WebSocket failed to connect: ${event.reason}`,
|
||||
event,
|
||||
);
|
||||
} else if (this.webSocket?.state == "connected") {
|
||||
console.warn(
|
||||
`[BackendAPI] WebSocket connection closed: ${event.reason}`,
|
||||
event,
|
||||
);
|
||||
}
|
||||
this.webSocket!.state = "closed";
|
||||
|
||||
this._stopWSHeartbeat(); // Stop heartbeat when connection closes
|
||||
this.wsConnecting = null;
|
||||
this.wsOnDisconnectHandlers.forEach((handler) => handler());
|
||||
// Attempt to reconnect after a delay
|
||||
setTimeout(() => this.connectWebSocket(), 1000);
|
||||
setTimeout(() => this.connectWebSocket().then(resolve), 1000);
|
||||
};
|
||||
|
||||
this.webSocket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
this._stopWSHeartbeat(); // Stop heartbeat on error
|
||||
this.wsConnecting = null;
|
||||
reject(error);
|
||||
if (this.webSocket?.state == "connected") {
|
||||
console.error("[BackendAPI] WebSocket error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
this.webSocket.onmessage = (event) => this._handleWSMessage(event);
|
||||
} catch (error) {
|
||||
console.error("Error connecting to WebSocket:", error);
|
||||
console.error("[BackendAPI] Error connecting to WebSocket:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return this.wsConnecting;
|
||||
}));
|
||||
}
|
||||
|
||||
disconnectWebSocket() {
|
||||
@@ -1048,6 +1075,12 @@ export default class BackendAPI {
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface WebSocket {
|
||||
state: "connecting" | "connected" | "closed";
|
||||
}
|
||||
}
|
||||
|
||||
/* *** UTILITY TYPES *** */
|
||||
|
||||
type GraphCreateRequestBody = {
|
||||
|
||||
Reference in New Issue
Block a user