diff --git a/src/app/auth/p/spotify/page.tsx b/src/app/auth/p/spotify/page.tsx index c4eb76e..b09a9d0 100644 --- a/src/app/auth/p/spotify/page.tsx +++ b/src/app/auth/p/spotify/page.tsx @@ -1,25 +1,7 @@ "use client"; import { signIn, useSession } from "next-auth/react"; import { useEffect } from "react"; - -const LoadingSVG = () => ( - -); +import { LoadingSpinner } from "~/components/LoadingSpinner"; const SignInPage = () => { const { data: session, status } = useSession(); @@ -54,7 +36,7 @@ const SignInPage = () => {
- + Loading...

{renderContent()}

diff --git a/src/app/auth/providerButtons.tsx b/src/app/auth/providerButtons.tsx index b69557e..123b950 100644 --- a/src/app/auth/providerButtons.tsx +++ b/src/app/auth/providerButtons.tsx @@ -3,6 +3,7 @@ import { getProviders, signIn, useSession } from "next-auth/react"; import { redirect } from "next/navigation"; import { useEffect, useState } from "react"; +import { LoadingSpinner } from "~/components/LoadingSpinner"; import { Button } from "~/components/ui/button"; type Provider = { @@ -60,25 +61,6 @@ const popupCenter = ({ return newWindow; }; -const LoadingSVG = () => ( - -); - // million-ignore export default function ProviderButtons() { const [providers, setProviders] = useState>({}); @@ -144,7 +126,7 @@ export default function ProviderButtons() { )} {status === "loading" ? ( - + Authenticating... ) : ( diff --git a/src/app/states/store.ts b/src/app/states/store.ts index c1b6664..dceff90 100644 --- a/src/app/states/store.ts +++ b/src/app/states/store.ts @@ -110,9 +110,6 @@ const useStore = create((set, get) => ({ }, setEdges: (edges) => { console.debug("setEdges", edges); - edges.forEach((edge) => { - edge.type = "smoothstep"; - }); set({ edges: edges, }); @@ -278,4 +275,25 @@ const useStore = create((set, get) => ({ }, })); +type WorkflowRunState = { + workflowRun: QueueResponse | null; + setWorkflowRun: (workflowRun: QueueResponse) => void; + resetWorkflowRun: () => void; +}; + +export const workflowRunStore = create((set, get) => ({ + workflowRun: null, + setWorkflowRun: (workflowRun) => { + console.info("workflowRun", workflowRun); + set({ + workflowRun: workflowRun, + }); + }, + resetWorkflowRun: () => { + set({ + workflowRun: null, + }); + }, +})); + export default useStore; diff --git a/src/app/utils/runWorkflow.ts b/src/app/utils/runWorkflow.ts index 198f411..5c87ed8 100644 --- a/src/app/utils/runWorkflow.ts +++ b/src/app/utils/runWorkflow.ts @@ -1,16 +1,20 @@ import { toast } from "sonner"; -import useStore from "../states/store"; +import useStore, { workflowRunStore } from "../states/store"; export async function runWorkflow(workflow: WorkflowResponse) { if (!workflow.id) { throw new Error("Workflow ID is undefined"); } - const dryrun = useStore.getState().flowState.dryrun; + const dryrun = !useStore.getState().flowState.dryrun; const id = workflow.id; - const promise = fetch(`/api/workflow/${id}/run${dryrun ? "?dryrun=true" : ""}`, { - method: "POST", - }) + console.log("dryrun", dryrun); + const promise = fetch( + `/api/workflow/${id}/run${dryrun ? "?dryrun=true" : ""}`, + { + method: "POST", + }, + ) .then((res) => { return res.json(); }) @@ -30,6 +34,7 @@ export async function runWorkflow(workflow: WorkflowResponse) { } const jobId = job.id as string; + const workflowRun = workflowRunStore.getState(); const pollRequest = (id: string) => { return new Promise((resolve, reject) => { @@ -44,9 +49,12 @@ export async function runWorkflow(workflow: WorkflowResponse) { if (data.error) { clearInterval(interval); reject(new Error(data.error)); - } else if (data.status === "completed") { - clearInterval(interval); - resolve(data); + } else { + workflowRun.setWorkflowRun(data); + if (data.status === "completed") { + clearInterval(interval); + resolve(data); + } } }) .catch((err) => { @@ -61,10 +69,14 @@ export async function runWorkflow(workflow: WorkflowResponse) { toast.promise(pollRequest(jobId), { loading: "Running workflow...", success: () => { + setTimeout(() => { + workflowRun.resetWorkflowRun(); + }, 5000); return "Workflow completed successfully"; }, error: (data) => { console.info("data on err", data); + workflowRun.resetWorkflowRun(); return "Failed running workflow: " + data; }, }); diff --git a/src/app/workflow/Builder.tsx b/src/app/workflow/Builder.tsx index 514fa22..ba221bb 100644 --- a/src/app/workflow/Builder.tsx +++ b/src/app/workflow/Builder.tsx @@ -20,6 +20,7 @@ import { ImperativePanelHandle } from "react-resizable-panels"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; +import { LoadingWithText } from "~/components/LoadingSpinner"; import { useWorkflowData } from "~/hooks/useWorkflowData"; function Builder({ @@ -195,28 +196,8 @@ function Builder({ const Loading = memo(() => (
- -
Loading...
+
)); -const LoadingSVG = () => ( - -); - export default Builder; diff --git a/src/app/workflow/Flow.tsx b/src/app/workflow/Flow.tsx index e8f9418..5eac226 100644 --- a/src/app/workflow/Flow.tsx +++ b/src/app/workflow/Flow.tsx @@ -348,7 +348,7 @@ export function App() { ); const edgeOptions = { - animated: false, + animated: true, }; const setDryRun = useCallback( diff --git a/src/app/workflow/settingsDialog/tabs/History.tsx b/src/app/workflow/settingsDialog/tabs/History.tsx index 9d33ffd..b65e58e 100644 --- a/src/app/workflow/settingsDialog/tabs/History.tsx +++ b/src/app/workflow/settingsDialog/tabs/History.tsx @@ -233,7 +233,6 @@ const History = () => {
- {error &&
Error loading history
}
diff --git a/src/app/workflows/WorkflowTable.tsx b/src/app/workflows/WorkflowTable.tsx index 8437081..6bb6e5b 100644 --- a/src/app/workflows/WorkflowTable.tsx +++ b/src/app/workflows/WorkflowTable.tsx @@ -479,7 +479,6 @@ export function WorkflowTable({ workflows }: WorkflowTableProps) { - {error &&
Error loading history
}
diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..aaf51f5 --- /dev/null +++ b/src/components/LoadingSpinner.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { cn } from "~/lib/utils"; + +const loadingMessages = [ + "Tuning the instruments...", + "Setting up the stage...", + "Warming up the band...", + "Mixing the sound...", + "Rolling the drum...", + "Strumming the guitar...", + "Hitting the high notes...", + "Feeling the bass...", + "Cueing the lights...", + "Getting ready to rock...", + "Gathering beats", + "Mixing it up...", + "Blending the vibes...", + "Building the ultimate playlist...", + "Getting the party started...", + "Fine-tuning the flow...", + "Crafting your new jam...", + "Connecting the dots...", + "Creating something fresh...", + "Making music magic...", + "Getting ready to rock...", + "Spinning the tunes...", +]; + +export interface ExtendedSVGProps extends React.SVGProps { + size?: number; + className?: string; +} + +export const LoadingSpinner = ({ + size = 24, + className, + ...props +}: ExtendedSVGProps) => { + return ( + + + + ); +}; + +export const LoadingWithText = ({ + size = 24, + className, + ...props +}: ExtendedSVGProps) => { + const randomIndex = Math.floor(Math.random() * loadingMessages.length); + const [loadingText, setLoadingText] = useState(loadingMessages[randomIndex]); + + useEffect(() => { + const intervalId = setInterval(() => { + const newRandomIndex = Math.floor(Math.random() * loadingMessages.length); + setLoadingText(loadingMessages[newRandomIndex]); + }, 3000); + + return () => clearInterval(intervalId); + }, []); + + return ( +
+ + + {loadingText} + +
+ ); +}; diff --git a/src/components/nodes/Primitives/Card.tsx b/src/components/nodes/Primitives/Card.tsx index 3287d75..83b1e8b 100644 --- a/src/components/nodes/Primitives/Card.tsx +++ b/src/components/nodes/Primitives/Card.tsx @@ -1,14 +1,11 @@ -"use client"; - -import * as React from "react"; - import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; - -import { cn } from "~/lib/utils"; - -import { DotIcon, InfoIcon } from "lucide-react"; +import { CheckIcon, DotIcon, InfoIcon } from "lucide-react"; +import * as React from "react"; +import { workflowRunStore } from "~/app/states/store"; +import { LoadingSpinner } from "~/components/LoadingSpinner"; import { Separator } from "~/components/ui/separator"; +import { cn } from "~/lib/utils"; interface CardWithHeaderProps { children: React.ReactNode; @@ -20,6 +17,49 @@ interface CardWithHeaderProps { className?: string; } +const StatusBadge = React.memo( + ({ operationStatus }: { operationStatus: string | undefined }) => { + const [isVisible, setIsVisible] = React.useState(false); + + React.useEffect(() => { + setIsVisible(true); + }, []); + + const statusMapping = { + completed: ( +
+ + {"Completed"} +
+ ), + default: ( +
+ + {"Pending"} +
+ ), + }; + + return ( + + {statusMapping[operationStatus as keyof typeof statusMapping] || + statusMapping.default} + + ); + }, +); + export function CardWithHeader({ children, id, @@ -29,10 +69,29 @@ export function CardWithHeader({ info, className, }: CardWithHeaderProps) { + const { workflowRun } = workflowRunStore((state) => ({ + workflowRun: state.workflowRun, + })); + + const operationStatus = workflowRun?.operations?.find( + (operation) => operation.id === id, + )?.completedAt + ? "completed" + : undefined; + + const isRunning = workflowRun?.id; + return (
-
+
+ {isRunning && ( + + )} +
nodes.find((node) => node.id === id)?.data; - if (env.NEXT_PUBLIC_ENV !== "development") return null; return (
Debug info
@@ -61,4 +61,58 @@ const DebugInfo = ({ ); }; -export default React.memo(DebugInfo); +const ConditionalDebugInfo = ({ + id, + isValid, + TargetConnections, + SourceConnections, +}: { + id: string; + isValid: boolean; + TargetConnections: any; + SourceConnections: any; +}) => { + const [isDebugVisible, setDebugVisible] = useState(false); + + const toggleDebugInfo = () => { + setDebugVisible(!isDebugVisible); + }; + + if (env.NEXT_PUBLIC_ENV !== "development") { + return null; + } + + if (isDebugVisible) { + return ( +
+ + +
+ ); + } + + return ( + + ); +}; + +export default React.memo(ConditionalDebugInfo); diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 3230be6..a286623 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { cn } from "src/lib/utils"; const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { diff --git a/src/lib/workflow/Workflow.ts b/src/lib/workflow/Workflow.ts index 19eb508..2d0a1ac 100644 --- a/src/lib/workflow/Workflow.ts +++ b/src/lib/workflow/Workflow.ts @@ -534,7 +534,7 @@ export class Runner extends Base { controller: AbortController, operationCallback?: (operationId: string, data: any) => Promise, ) { - const sortedOperations = await this.sortOperations(workflow); + const sortedOperations = this.sortOperations(workflow); log.info( "sortedOperations", sortedOperations.map((op) => op.id), diff --git a/src/lib/workflow/utils/workflowQueue.ts b/src/lib/workflow/utils/workflowQueue.ts index 1804502..8706c10 100644 --- a/src/lib/workflow/utils/workflowQueue.ts +++ b/src/lib/workflow/utils/workflowQueue.ts @@ -6,7 +6,11 @@ import Redis from "ioredis"; import { v4 as uuidv4 } from "uuid"; import { env } from "~/env"; import { db } from "~/server/db"; -import { workflowJobs, workflowRuns, workflowRunOperations } from "~/server/db/schema"; +import { + workflowJobs, + workflowRunOperations, + workflowRuns, +} from "~/server/db/schema"; const log = new Logger("workflowQueue"); @@ -314,7 +318,7 @@ function compressReturnValues(returnValues: any[]) { ); } - if (compressedItem.track) { + if (compressedItem.track?.artists) { compressedItem.track.artists = compressedItem.track.artists.map( (artist: SpotifyApi.ArtistObjectSimplified) => ({ ...artist, diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 2e9950d..2a8ebde 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -2,7 +2,7 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { env } from "~/env.js"; -import * as schema from "./schema"; +import * as schema from "./schema"; import { type PostgresJsDatabase } from "drizzle-orm/postgres-js"; @@ -23,4 +23,4 @@ if (env.NODE_ENV === "production") { db = global.db; } -export { db }; \ No newline at end of file +export { db }; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index ee25453..7e14272 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -137,17 +137,20 @@ export const workflowRuns = pgTable( workflowIdIdx: index("workflowRuns_workflowId_idx").on(workflowRun.id), }), ); -export const workflowRunsRelations = relations(workflowRuns, ({ one, many }) => ({ - workflow: one(workflowJobs, { - fields: [workflowRuns.workflowId], - references: [workflowJobs.id], - }), - operations: many(workflowRunOperations), - worker: one(workerPool, { - fields: [workflowRuns.workerId], - references: [workerPool.deviceHash], +export const workflowRunsRelations = relations( + workflowRuns, + ({ one, many }) => ({ + workflow: one(workflowJobs, { + fields: [workflowRuns.workflowId], + references: [workflowJobs.id], + }), + operations: many(workflowRunOperations), + worker: one(workerPool, { + fields: [workflowRuns.workerId], + references: [workerPool.deviceHash], + }), }), -})); +); export const workflowRunOperations = pgTable( "workflowRunOperation", @@ -165,7 +168,6 @@ export const workflowRunOperations = pgTable( }), ); - export const workflowRunOperationsRelations = relations( workflowRunOperations, ({ one }) => ({ diff --git a/src/styles/globals.css b/src/styles/globals.css index fa73670..841fb6f 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -151,4 +151,151 @@ html{ /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { background: var(--primary-foreground); +} + +.sonner-loading-wrapper { + --size: 16px; + height: var(--size); + width: var(--size); + position: absolute; + inset: 0; + z-index: 10; +} + +.sonner-loading-wrapper[data-visible='false'] { + transform-origin: center; + animation: sonner-fade-out 0.2s ease forwards; +} + +.sonner-spinner { + position: relative; + top: 50%; + left: 50%; + height: var(--size); + width: var(--size); +} + +.sonner-loading-bar { + animation: sonner-spin 1.2s linear infinite; + background: var(--gray11); + border-radius: 6px; + height: 8%; + left: -10%; + position: absolute; + top: -3.9%; + width: 24%; +} + +.sonner-loading-bar:nth-child(1) { + animation-delay: -1.2s; + transform: rotate(0.0001deg) translate(146%); +} + +.sonner-loading-bar:nth-child(2) { + animation-delay: -1.1s; + transform: rotate(30deg) translate(146%); +} + +.sonner-loading-bar:nth-child(3) { + animation-delay: -1s; + transform: rotate(60deg) translate(146%); +} + +.sonner-loading-bar:nth-child(4) { + animation-delay: -0.9s; + transform: rotate(90deg) translate(146%); +} + +.sonner-loading-bar:nth-child(5) { + animation-delay: -0.8s; + transform: rotate(120deg) translate(146%); +} + +.sonner-loading-bar:nth-child(6) { + animation-delay: -0.7s; + transform: rotate(150deg) translate(146%); +} + +.sonner-loading-bar:nth-child(7) { + animation-delay: -0.6s; + transform: rotate(180deg) translate(146%); +} + +.sonner-loading-bar:nth-child(8) { + animation-delay: -0.5s; + transform: rotate(210deg) translate(146%); +} + +.sonner-loading-bar:nth-child(9) { + animation-delay: -0.4s; + transform: rotate(240deg) translate(146%); +} + +.sonner-loading-bar:nth-child(10) { + animation-delay: -0.3s; + transform: rotate(270deg) translate(146%); +} + +.sonner-loading-bar:nth-child(11) { + animation-delay: -0.2s; + transform: rotate(300deg) translate(146%); +} + +.sonner-loading-bar:nth-child(12) { + animation-delay: -0.1s; + transform: rotate(330deg) translate(146%); +} + +@keyframes sonner-fade-in { + 0% { + opacity: 0; + transform: scale(0.8); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes sonner-fade-out { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.8); + } +} + +@keyframes sonner-spin { + 0% { + opacity: 1; + } + 100% { + opacity: 0.15; + } +} + +@media (prefers-reduced-motion) { + [data-sonner-toast], + [data-sonner-toast] > *, + .sonner-loading-bar { + transition: none !important; + animation: none !important; + } +} + +.sonner-loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transform-origin: center; + transition: opacity 200ms, transform 200ms; +} + +.sonner-loader[data-visible='false'] { + opacity: 0; + transform: scale(0.8) translate(-50%, -50%); } \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts index 99fdd54..4a32f0a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -97,6 +97,14 @@ interface WorkflowResponse { modifiedAt?: number; } +type WorkflowRunOperation = { + id: string; + workflowRunId: string; + completedAt: string; + data: string; + startedAt: string; +}; + interface QueueResponse { id: string; workflowId: string; @@ -107,6 +115,7 @@ interface QueueResponse { workerId: string; returnValues?: any; workflow?: WorkflowObject; + operations?: WorkflowRunOperation[]; } interface SystemInfo {