chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-25 00:27:24 +05:30
parent 323f7f2b4a
commit c674fb3054
37 changed files with 972 additions and 920 deletions

View file

@ -33,8 +33,8 @@ import { closeReportPanelAtom } from "@/atoms/chat/report-panel.atom";
import { closeEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { membersAtom } from "@/atoms/members/members-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { Thread } from "@/components/assistant-ui/thread";
import { ThinkingStepsDataUI } from "@/components/assistant-ui/thinking-steps";
import { Thread } from "@/components/assistant-ui/thread";
import { MobileEditorPanel } from "@/components/editor-panel/editor-panel";
import { MobileHitlEditPanel } from "@/components/hitl-edit-panel/hitl-edit-panel";
import { MobileReportPanel } from "@/components/report-panel/report-panel";

View file

@ -16,19 +16,47 @@ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
import { CreateConfluencePageToolUI, DeleteConfluencePageToolUI, UpdateConfluencePageToolUI } from "@/components/tool-ui/confluence";
import {
CreateConfluencePageToolUI,
DeleteConfluencePageToolUI,
UpdateConfluencePageToolUI,
} from "@/components/tool-ui/confluence";
import { GenerateImageToolUI } from "@/components/tool-ui/generate-image";
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
import { CreateGmailDraftToolUI, SendGmailEmailToolUI, TrashGmailEmailToolUI, UpdateGmailDraftToolUI } from "@/components/tool-ui/gmail";
import { CreateCalendarEventToolUI, DeleteCalendarEventToolUI, UpdateCalendarEventToolUI } from "@/components/tool-ui/google-calendar";
import { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI } from "@/components/tool-ui/google-drive";
import { CreateJiraIssueToolUI, DeleteJiraIssueToolUI, UpdateJiraIssueToolUI } from "@/components/tool-ui/jira";
import { CreateLinearIssueToolUI, DeleteLinearIssueToolUI, UpdateLinearIssueToolUI } from "@/components/tool-ui/linear";
import { CreateNotionPageToolUI, DeleteNotionPageToolUI, UpdateNotionPageToolUI } from "@/components/tool-ui/notion";
import {
CreateGmailDraftToolUI,
SendGmailEmailToolUI,
TrashGmailEmailToolUI,
UpdateGmailDraftToolUI,
} from "@/components/tool-ui/gmail";
import {
CreateCalendarEventToolUI,
DeleteCalendarEventToolUI,
UpdateCalendarEventToolUI,
} from "@/components/tool-ui/google-calendar";
import {
CreateGoogleDriveFileToolUI,
DeleteGoogleDriveFileToolUI,
} from "@/components/tool-ui/google-drive";
import {
CreateJiraIssueToolUI,
DeleteJiraIssueToolUI,
UpdateJiraIssueToolUI,
} from "@/components/tool-ui/jira";
import {
CreateLinearIssueToolUI,
DeleteLinearIssueToolUI,
UpdateLinearIssueToolUI,
} from "@/components/tool-ui/linear";
import {
CreateNotionPageToolUI,
DeleteNotionPageToolUI,
UpdateNotionPageToolUI,
} from "@/components/tool-ui/notion";
import { SandboxExecuteToolUI } from "@/components/tool-ui/sandbox-execute";
import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory";
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
import { useComments } from "@/hooks/use-comments";
import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils";
@ -246,13 +274,13 @@ const AssistantActionBar: FC = () => {
const isLast = useAuiState((s) => s.message.isLast);
return (
<ActionBarPrimitive.Root
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
autohideFloat="single-branch"
className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground md:data-floating:absolute md:data-floating:rounded-md md:data-floating:p-1 [&>button]:opacity-100 md:[&>button]:opacity-[var(--aui-button-opacity,1)]"
>
<ActionBarPrimitive.Copy asChild>
<ActionBarPrimitive.Copy asChild>
<TooltipIconButton tooltip="Copy">
<AuiIf condition={({ message }) => message.isCopied}>
<CheckIcon />
@ -262,19 +290,19 @@ const AssistantActionBar: FC = () => {
</AuiIf>
</TooltipIconButton>
</ActionBarPrimitive.Copy>
<ActionBarPrimitive.ExportMarkdown asChild>
<ActionBarPrimitive.ExportMarkdown asChild>
<TooltipIconButton tooltip="Download">
<DownloadIcon />
</TooltipIconButton>
</ActionBarPrimitive.ExportMarkdown>
{/* Only allow regenerating the last assistant message */}
{isLast && (
{/* Only allow regenerating the last assistant message */}
{isLast && (
<ActionBarPrimitive.Reload asChild>
<TooltipIconButton tooltip="Refresh">
<RefreshCwIcon />
</TooltipIconButton>
</ActionBarPrimitive.Reload>
)}
</ActionBarPrimitive.Root>
);
</ActionBarPrimitive.Root>
);
};

View file

@ -1,255 +1,221 @@
"use client";
import {
memo,
useState,
useEffect,
useRef,
type PropsWithChildren,
} from "react";
import { createPortal } from "react-dom";
import type { ImageMessagePartComponent } from "@assistant-ui/react";
import { cva, type VariantProps } from "class-variance-authority";
import { ImageIcon, ImageOffIcon } from "lucide-react";
import type { ImageMessagePartComponent } from "@assistant-ui/react";
import { memo, type PropsWithChildren, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
const imageVariants = cva(
"aui-image-root relative overflow-hidden rounded-lg",
{
variants: {
variant: {
outline: "border border-border",
ghost: "",
muted: "bg-muted/50",
},
size: {
sm: "max-w-64",
default: "max-w-96",
lg: "max-w-[512px]",
full: "w-full",
},
},
defaultVariants: {
variant: "outline",
size: "default",
},
},
);
const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", {
variants: {
variant: {
outline: "border border-border",
ghost: "",
muted: "bg-muted/50",
},
size: {
sm: "max-w-64",
default: "max-w-96",
lg: "max-w-[512px]",
full: "w-full",
},
},
defaultVariants: {
variant: "outline",
size: "default",
},
});
export type ImageRootProps = React.ComponentProps<"div"> &
VariantProps<typeof imageVariants>;
export type ImageRootProps = React.ComponentProps<"div"> & VariantProps<typeof imageVariants>;
function ImageRoot({
className,
variant,
size,
children,
...props
}: ImageRootProps) {
return (
<div
data-slot="image-root"
data-variant={variant}
data-size={size}
className={cn(imageVariants({ variant, size, className }))}
{...props}
>
{children}
</div>
);
function ImageRoot({ className, variant, size, children, ...props }: ImageRootProps) {
return (
<div
data-slot="image-root"
data-variant={variant}
data-size={size}
className={cn(imageVariants({ variant, size, className }))}
{...props}
>
{children}
</div>
);
}
type ImagePreviewProps = Omit<React.ComponentProps<"img">, "children"> & {
containerClassName?: string;
containerClassName?: string;
};
function ImagePreview({
className,
containerClassName,
onLoad,
onError,
alt = "Image content",
src,
...props
className,
containerClassName,
onLoad,
onError,
alt = "Image content",
src,
...props
}: ImagePreviewProps) {
const imgRef = useRef<HTMLImageElement>(null);
const [loadedSrc, setLoadedSrc] = useState<string | undefined>(undefined);
const [errorSrc, setErrorSrc] = useState<string | undefined>(undefined);
const imgRef = useRef<HTMLImageElement>(null);
const [loadedSrc, setLoadedSrc] = useState<string | undefined>(undefined);
const [errorSrc, setErrorSrc] = useState<string | undefined>(undefined);
const loaded = loadedSrc === src;
const error = errorSrc === src;
const loaded = loadedSrc === src;
const error = errorSrc === src;
useEffect(() => {
if (
typeof src === "string" &&
imgRef.current?.complete &&
imgRef.current.naturalWidth > 0
) {
setLoadedSrc(src);
}
}, [src]);
useEffect(() => {
if (typeof src === "string" && imgRef.current?.complete && imgRef.current.naturalWidth > 0) {
setLoadedSrc(src);
}
}, [src]);
return (
<div
data-slot="image-preview"
className={cn("relative min-h-32", containerClassName)}
>
{!loaded && !error && (
<div
data-slot="image-preview-loading"
className="absolute inset-0 flex items-center justify-center bg-muted/50"
>
<ImageIcon className="size-8 animate-pulse text-muted-foreground" />
</div>
)}
{error ? (
<div
data-slot="image-preview-error"
className="flex min-h-32 items-center justify-center bg-muted/50 p-4"
>
<ImageOffIcon className="size-8 text-muted-foreground" />
</div>
) : (
// biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs
<img
ref={imgRef}
src={src}
alt={alt}
className={cn(
"block h-auto w-full object-contain",
!loaded && "invisible",
className,
)}
onLoad={(e) => {
if (typeof src === "string") setLoadedSrc(src);
onLoad?.(e);
}}
onError={(e) => {
if (typeof src === "string") setErrorSrc(src);
onError?.(e);
}}
{...props}
/>
)}
</div>
);
return (
<div data-slot="image-preview" className={cn("relative min-h-32", containerClassName)}>
{!loaded && !error && (
<div
data-slot="image-preview-loading"
className="absolute inset-0 flex items-center justify-center bg-muted/50"
>
<ImageIcon className="size-8 animate-pulse text-muted-foreground" />
</div>
)}
{error ? (
<div
data-slot="image-preview-error"
className="flex min-h-32 items-center justify-center bg-muted/50 p-4"
>
<ImageOffIcon className="size-8 text-muted-foreground" />
</div>
) : (
// biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs
<img
ref={imgRef}
src={src}
alt={alt}
className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
onLoad={(e) => {
if (typeof src === "string") setLoadedSrc(src);
onLoad?.(e);
}}
onError={(e) => {
if (typeof src === "string") setErrorSrc(src);
onError?.(e);
}}
{...props}
/>
)}
</div>
);
}
function ImageFilename({
className,
children,
...props
}: React.ComponentProps<"span">) {
if (!children) return null;
function ImageFilename({ className, children, ...props }: React.ComponentProps<"span">) {
if (!children) return null;
return (
<span
data-slot="image-filename"
className={cn(
"block truncate px-2 py-1.5 text-muted-foreground text-xs",
className,
)}
{...props}
>
{children}
</span>
);
return (
<span
data-slot="image-filename"
className={cn("block truncate px-2 py-1.5 text-muted-foreground text-xs", className)}
{...props}
>
{children}
</span>
);
}
type ImageZoomProps = PropsWithChildren<{
src: string;
alt?: string;
src: string;
alt?: string;
}>;
function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
const [isMounted, setIsMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
setIsMounted(true);
}, []);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = originalOverflow;
};
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = originalOverflow;
};
}, [isOpen]);
return (
<>
<button
type="button"
onClick={handleOpen}
className="aui-image-zoom-trigger cursor-zoom-in border-0 bg-transparent p-0 text-left"
aria-label="Click to zoom image"
>
{children}
</button>
{isMounted &&
isOpen &&
createPortal(
<button
type="button"
data-slot="image-zoom-overlay"
className="aui-image-zoom-overlay fade-in fixed inset-0 z-50 flex animate-in cursor-zoom-out items-center justify-center border-0 bg-black/80 p-0 duration-200"
onClick={handleClose}
aria-label="Close zoomed image"
>
{/** biome-ignore lint/performance/noImgElement: <explanation> */}
<img
data-slot="image-zoom-content"
src={src}
alt={alt}
className="aui-image-zoom-content fade-in zoom-in-95 max-h-[90vh] max-w-[90vw] animate-in object-contain duration-200"
onClick={(e) => {
e.stopPropagation();
handleClose();
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.stopPropagation();
handleClose();
}
}}
/>
</button>,
document.body,
)}
</>
);
return (
<>
<button
type="button"
onClick={handleOpen}
className="aui-image-zoom-trigger cursor-zoom-in border-0 bg-transparent p-0 text-left"
aria-label="Click to zoom image"
>
{children}
</button>
{isMounted &&
isOpen &&
createPortal(
<button
type="button"
data-slot="image-zoom-overlay"
className="aui-image-zoom-overlay fade-in fixed inset-0 z-50 flex animate-in cursor-zoom-out items-center justify-center border-0 bg-black/80 p-0 duration-200"
onClick={handleClose}
aria-label="Close zoomed image"
>
{/** biome-ignore lint/performance/noImgElement: <explanation> */}
<img
data-slot="image-zoom-content"
src={src}
alt={alt}
className="aui-image-zoom-content fade-in zoom-in-95 max-h-[90vh] max-w-[90vw] animate-in object-contain duration-200"
onClick={(e) => {
e.stopPropagation();
handleClose();
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.stopPropagation();
handleClose();
}
}}
/>
</button>,
document.body
)}
</>
);
}
const ImageImpl: ImageMessagePartComponent = ({ image, filename }) => {
return (
<ImageRoot>
<ImageZoom src={image} alt={filename || "Image content"}>
<ImagePreview src={image} alt={filename || "Image content"} />
</ImageZoom>
<ImageFilename>{filename}</ImageFilename>
</ImageRoot>
);
return (
<ImageRoot>
<ImageZoom src={image} alt={filename || "Image content"}>
<ImagePreview src={image} alt={filename || "Image content"} />
</ImageZoom>
<ImageFilename>{filename}</ImageFilename>
</ImageRoot>
);
};
const Image = memo(ImageImpl) as unknown as ImageMessagePartComponent & {
Root: typeof ImageRoot;
Preview: typeof ImagePreview;
Filename: typeof ImageFilename;
Zoom: typeof ImageZoom;
Root: typeof ImageRoot;
Preview: typeof ImagePreview;
Filename: typeof ImageFilename;
Zoom: typeof ImageZoom;
};
Image.displayName = "Image";
@ -258,11 +224,4 @@ Image.Preview = ImagePreview;
Image.Filename = ImageFilename;
Image.Zoom = ImageZoom;
export {
Image,
ImageRoot,
ImagePreview,
ImageFilename,
ImageZoom,
imageVariants,
};
export { Image, ImageRoot, ImagePreview, ImageFilename, ImageZoom, imageVariants };

View file

@ -8,23 +8,30 @@ import {
useIsMarkdownCodeBlock,
} from "@assistant-ui/react-markdown";
import { CheckIcon, CopyIcon, ExternalLinkIcon } from "lucide-react";
import { type FC, memo, type ReactNode, useState } from "react";
import { useTheme } from "next-themes";
import type { CSSProperties } from "react";
import { type FC, memo, type ReactNode, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { materialDark, materialLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import type { CSSProperties } from "react";
import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image";
import "katex/dist/katex.min.css";
import { InlineCitation, UrlCitation } from "@/components/assistant-ui/inline-citation";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
function stripThemeBackgrounds(
theme: Record<string, CSSProperties>,
theme: Record<string, CSSProperties>
): Record<string, CSSProperties> {
const cleaned: Record<string, CSSProperties> = {};
for (const key of Object.keys(theme)) {
@ -261,9 +268,7 @@ function MarkdownImage({ src, alt }: { src?: string; alt?: string }) {
{alt && alt !== "Image" && (
<p className="text-sm font-semibold text-foreground line-clamp-2">{alt}</p>
)}
{domain && (
<p className="text-xs text-muted-foreground mt-0.5 truncate">{domain}</p>
)}
{domain && <p className="text-xs text-muted-foreground mt-0.5 truncate">{domain}</p>}
</div>
<a
href={src}
@ -403,9 +408,7 @@ const defaultComponents = memoizeMarkdownComponents({
{processChildrenWithCitations(children)}
</TableCell>
),
tr: ({ className, ...props }) => (
<TableRow className={cn("aui-md-tr", className)} {...props} />
),
tr: ({ className, ...props }) => <TableRow className={cn("aui-md-tr", className)} {...props} />,
sup: ({ className, ...props }) => (
<sup className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)} {...props} />
),
@ -448,6 +451,8 @@ const defaultComponents = memoizeMarkdownComponents({
{processChildrenWithCitations(children)}
</em>
),
img: ({ src, alt }) => <MarkdownImage src={typeof src === "string" ? src : undefined} alt={alt} />,
img: ({ src, alt }) => (
<MarkdownImage src={typeof src === "string" ? src : undefined} alt={alt} />
),
CodeHeader: () => null,
});

View file

@ -126,8 +126,8 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?:
{step.items && step.items.length > 0 && (
<div className="mt-1 space-y-0.5">
{step.items.map((item) => (
<ChainOfThoughtItem key={`${step.id}-${item}`} className="text-xs">
{step.items.map((item) => (
<ChainOfThoughtItem key={`${step.id}-${item}`} className="text-xs">
{item}
</ChainOfThoughtItem>
))}
@ -169,4 +169,3 @@ export const ThinkingStepsDataUI = makeAssistantDataUI({
name: "thinking-steps",
render: ThinkingStepsDataRenderer,
});

View file

@ -101,13 +101,13 @@ export const Thread: FC = () => {
const ThreadContent: FC = () => {
return (
<ThreadPrimitive.Root
<ThreadPrimitive.Root
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-main-panel"
style={{
["--thread-max-width" as string]: "44rem",
}}
>
<ThreadPrimitive.Viewport
<ThreadPrimitive.Viewport
turnAnchor="top"
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4"
>
@ -135,8 +135,8 @@ const ThreadContent: FC = () => {
</AuiIf>
</ThreadPrimitive.ViewportFooter>
</ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>
);
</ThreadPrimitive.Root>
);
};
const ThreadScrollToBottom: FC = () => {
@ -678,8 +678,8 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser;
return (
<div className="aui-composer-action-wrapper relative mx-3 mb-2 flex items-center justify-between">
<div className="flex items-center gap-1">
<div className="aui-composer-action-wrapper relative mx-3 mb-2 flex items-center justify-between">
<div className="flex items-center gap-1">
{!isDesktop ? (
<>
<DropdownMenu>
@ -983,13 +983,13 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</button>
)}
</div>
{!hasModelConfigured && (
{!hasModelConfigured && (
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400 text-xs">
<AlertCircle className="size-3" />
<span>Select a model</span>
</div>
)}
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<AuiIf condition={({ thread }) => !thread.isRunning}>
<ComposerPrimitive.Send asChild disabled={isSendDisabled}>
<TooltipIconButton
@ -1032,8 +1032,8 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</ComposerPrimitive.Cancel>
</AuiIf>
</div>
</div>
);
</div>
);
};
/** Convert snake_case tool names to human-readable labels */

View file

@ -1,13 +1,11 @@
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XCircleIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { getToolIcon } from "@/contracts/enums/toolIcons";
import { cn } from "@/lib/utils";
function formatToolName(name: string): string {
return name
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
return name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
export const ToolFallback: ToolCallMessagePartComponent = ({
@ -42,7 +40,7 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
className={cn(
"my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none",
isCancelled && "opacity-60",
isError && "border-destructive/20 bg-destructive/5",
isError && "border-destructive/20 bg-destructive/5"
)}
>
<button
@ -53,11 +51,7 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
<div
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-lg",
isError
? "bg-destructive/10"
: isCancelled
? "bg-muted"
: "bg-primary/10",
isError ? "bg-destructive/10" : isCancelled ? "bg-muted" : "bg-primary/10"
)}
>
{isError ? (
@ -79,7 +73,7 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
? "text-destructive"
: isCancelled
? "text-muted-foreground line-through"
: "text-foreground",
: "text-foreground"
)}
>
{isRunning
@ -90,9 +84,7 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
? `Failed: ${displayName}`
: displayName}
</p>
{isRunning && (
<p className="text-xs text-muted-foreground mt-0.5">Running...</p>
)}
{isRunning && <p className="text-xs text-muted-foreground mt-0.5">Running...</p>}
{cancelledReason && (
<p className="text-xs text-muted-foreground mt-0.5 truncate">{cancelledReason}</p>
)}

View file

@ -170,12 +170,12 @@ const PublicAssistantMessage: FC = () => {
const PublicAssistantActionBar: FC = () => {
return (
<ActionBarPrimitive.Root
<ActionBarPrimitive.Root
autohide="not-last"
autohideFloat="single-branch"
className="aui-assistant-action-bar-root -ml-1 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm"
>
<ActionBarPrimitive.Copy asChild>
<ActionBarPrimitive.Copy asChild>
<TooltipIconButton tooltip="Copy">
<AuiIf condition={({ message }) => message.isCopied}>
<CheckIcon />
@ -185,6 +185,6 @@ const PublicAssistantActionBar: FC = () => {
</AuiIf>
</TooltipIconButton>
</ActionBarPrimitive.Copy>
</ActionBarPrimitive.Root>
);
</ActionBarPrimitive.Root>
);
};

View file

@ -1,12 +1,6 @@
"use client";
import {
DownloadIcon,
PauseIcon,
PlayIcon,
Volume2Icon,
VolumeXIcon,
} from "lucide-react";
import { DownloadIcon, PauseIcon, PlayIcon, Volume2Icon, VolumeXIcon } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";

View file

@ -457,39 +457,42 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const CreateConfluencePageToolUI = ({ args, result }: ToolCallMessagePartProps<
export const CreateConfluencePageToolUI = ({
args,
result,
}: ToolCallMessagePartProps<
{ title: string; content?: string; space_id?: string },
CreateConfluencePageResult
>) => {
if (!result) return null;
if (!result) return null;
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;
return <SuccessCard result={result as SuccessResult} />;
};

View file

@ -396,41 +396,43 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const DeleteConfluencePageToolUI = ({ result }: ToolCallMessagePartProps<
export const DeleteConfluencePageToolUI = ({
result,
}: ToolCallMessagePartProps<
{ page_title_or_id: string; delete_from_kb?: boolean },
DeleteConfluencePageResult
>) => {
if (!result) return null;
if (!result) return null;
if (isInterruptResult(result)) {
return (
<ApprovalCard
interruptData={result}
onDecision={(decision) => {
const event = new CustomEvent("hitl-decision", {
detail: { decisions: [decision] },
});
window.dispatchEvent(event);
}}
/>
);
}
if (isInterruptResult(result)) {
return (
<ApprovalCard
interruptData={result}
onDecision={(decision) => {
const event = new CustomEvent("hitl-decision", {
detail: { decisions: [decision] },
});
window.dispatchEvent(event);
}}
/>
);
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isWarningResult(result)) return <WarningCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isWarningResult(result)) return <WarningCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;
return <SuccessCard result={result as SuccessResult} />;
};

View file

@ -493,7 +493,10 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const UpdateConfluencePageToolUI = ({ args, result }: ToolCallMessagePartProps<
export const UpdateConfluencePageToolUI = ({
args,
result,
}: ToolCallMessagePartProps<
{
page_title_or_id: string;
new_title?: string;
@ -501,36 +504,36 @@ export const UpdateConfluencePageToolUI = ({ args, result }: ToolCallMessagePart
},
UpdateConfluencePageResult
>) => {
if (!result) return null;
if (!result) return null;
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;
return <SuccessCard result={result as SuccessResult} />;
};

View file

@ -84,7 +84,11 @@ function ParsedImage({ result }: { result: unknown }) {
* Tool UI for generate_image renders the generated image directly
* from the tool result directly.
*/
export const GenerateImageToolUI = ({ args, result, status }: ToolCallMessagePartProps<GenerateImageArgs, GenerateImageResult>) => {
export const GenerateImageToolUI = ({
args,
result,
status,
}: ToolCallMessagePartProps<GenerateImageArgs, GenerateImageResult>) => {
const prompt = args.prompt || "Generating image...";
if (status.type === "running" || status.type === "requires-action") {

View file

@ -372,89 +372,91 @@ function PodcastStatusPoller({ podcastId, title }: { podcastId: number; title: s
*
* It polls for task completion and auto-updates when the podcast is ready.
*/
export const GeneratePodcastToolUI = ({ args, result, status }: ToolCallMessagePartProps<GeneratePodcastArgs, GeneratePodcastResult>) => {
const title = args.podcast_title || "SurfSense Podcast";
export const GeneratePodcastToolUI = ({
args,
result,
status,
}: ToolCallMessagePartProps<GeneratePodcastArgs, GeneratePodcastResult>) => {
const title = args.podcast_title || "SurfSense Podcast";
// Loading state - tool is still running (agent processing)
if (status.type === "running" || status.type === "requires-action") {
return <PodcastGeneratingState title={title} />;
}
// Loading state - tool is still running (agent processing)
if (status.type === "running" || status.type === "requires-action") {
return <PodcastGeneratingState title={title} />;
}
// Incomplete/cancelled state
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-muted-foreground">Podcast Cancelled</p>
<p className="text-xs text-muted-foreground mt-0.5">
Podcast generation was cancelled
</p>
</div>
</div>
);
}
if (status.reason === "error") {
return (
<PodcastErrorState
title={title}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
// No result yet
if (!result) {
return <PodcastGeneratingState title={title} />;
}
// Failed result (new: "failed", legacy: "error")
if (result.status === "failed" || result.status === "error") {
return <PodcastErrorState title={title} error={result.error || "Generation failed"} />;
}
// Already generating - show simple warning, don't create another poller
// The FIRST tool call will display the podcast when ready
// (new: "generating", legacy: "already_generating")
if (result.status === "generating" || result.status === "already_generating") {
// Incomplete/cancelled state
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-foreground">Podcast already in progress</p>
<p className="text-xs text-muted-foreground mt-0.5">
Please wait for the current podcast to complete.
</p>
<p className="text-sm font-semibold text-muted-foreground">Podcast Cancelled</p>
<p className="text-xs text-muted-foreground mt-0.5">Podcast generation was cancelled</p>
</div>
</div>
);
}
// Pending - poll for completion (new: "pending" with podcast_id)
if (result.status === "pending" && result.podcast_id) {
return <PodcastStatusPoller podcastId={result.podcast_id} title={result.title || title} />;
}
// Ready with podcast_id (new: "ready", legacy: "success")
if ((result.status === "ready" || result.status === "success") && result.podcast_id) {
return <PodcastPlayer podcastId={result.podcast_id} title={result.title || title} />;
}
// Legacy: old chats with Celery task_id (status: "processing" or "success" without podcast_id)
// These can't be recovered since the old task polling endpoint no longer exists
if (result.task_id && !result.podcast_id) {
if (status.reason === "error") {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-muted-foreground">Podcast Unavailable</p>
<p className="text-xs text-muted-foreground mt-0.5">
This podcast was generated with an older version. Please generate a new one.
</p>
</div>
</div>
<PodcastErrorState
title={title}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
// Fallback - missing required data
return <PodcastErrorState title={title} error="Missing podcast ID" />;
// No result yet
if (!result) {
return <PodcastGeneratingState title={title} />;
}
// Failed result (new: "failed", legacy: "error")
if (result.status === "failed" || result.status === "error") {
return <PodcastErrorState title={title} error={result.error || "Generation failed"} />;
}
// Already generating - show simple warning, don't create another poller
// The FIRST tool call will display the podcast when ready
// (new: "generating", legacy: "already_generating")
if (result.status === "generating" || result.status === "already_generating") {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-foreground">Podcast already in progress</p>
<p className="text-xs text-muted-foreground mt-0.5">
Please wait for the current podcast to complete.
</p>
</div>
</div>
);
}
// Pending - poll for completion (new: "pending" with podcast_id)
if (result.status === "pending" && result.podcast_id) {
return <PodcastStatusPoller podcastId={result.podcast_id} title={result.title || title} />;
}
// Ready with podcast_id (new: "ready", legacy: "success")
if ((result.status === "ready" || result.status === "success") && result.podcast_id) {
return <PodcastPlayer podcastId={result.podcast_id} title={result.title || title} />;
}
// Legacy: old chats with Celery task_id (status: "processing" or "success" without podcast_id)
// These can't be recovered since the old task polling endpoint no longer exists
if (result.task_id && !result.podcast_id) {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-muted-foreground">Podcast Unavailable</p>
<p className="text-xs text-muted-foreground mt-0.5">
This podcast was generated with an older version. Please generate a new one.
</p>
</div>
</div>
);
}
// Fallback - missing required data
return <PodcastErrorState title={title} error="Missing podcast ID" />;
};

View file

@ -273,61 +273,62 @@ function ReportCard({
* Generate Report Tool UI renders custom UI inline in chat
* when the generate_report tool is called by the agent.
*/
export const GenerateReportToolUI = ({ args, result, status }: ToolCallMessagePartProps<GenerateReportArgs, GenerateReportResult>) => {
const params = useParams();
const pathname = usePathname();
const isPublicRoute = pathname?.startsWith("/public/");
const shareToken = isPublicRoute && typeof params?.token === "string" ? params.token : null;
export const GenerateReportToolUI = ({
args,
result,
status,
}: ToolCallMessagePartProps<GenerateReportArgs, GenerateReportResult>) => {
const params = useParams();
const pathname = usePathname();
const isPublicRoute = pathname?.startsWith("/public/");
const shareToken = isPublicRoute && typeof params?.token === "string" ? params.token : null;
const topic = args.topic || "Report";
const topic = args.topic || "Report";
const sawRunningRef = useRef(false);
if (status.type === "running" || status.type === "requires-action") {
sawRunningRef.current = true;
const sawRunningRef = useRef(false);
if (status.type === "running" || status.type === "requires-action") {
sawRunningRef.current = true;
}
if (status.type === "running" || status.type === "requires-action") {
return <ReportGeneratingState topic={topic} />;
}
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return <ReportCancelledState />;
}
if (status.type === "running" || status.type === "requires-action") {
return <ReportGeneratingState topic={topic} />;
}
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return <ReportCancelledState />;
}
if (status.reason === "error") {
return (
<ReportErrorState
title={topic}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
if (!result) {
return <ReportGeneratingState topic={topic} />;
}
if (result.status === "failed") {
if (status.reason === "error") {
return (
<ReportErrorState
title={result.title || topic}
error={result.error || "Generation failed"}
title={topic}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
if (result.status === "ready" && result.report_id) {
return (
<ReportCard
reportId={result.report_id}
title={result.title || topic}
wordCount={result.word_count ?? undefined}
shareToken={shareToken}
autoOpen={sawRunningRef.current}
/>
);
}
if (!result) {
return <ReportGeneratingState topic={topic} />;
}
return <ReportErrorState title={topic} error="Missing report ID" />;
if (result.status === "failed") {
return (
<ReportErrorState title={result.title || topic} error={result.error || "Generation failed"} />
);
}
if (result.status === "ready" && result.report_id) {
return (
<ReportCard
reportId={result.report_id}
title={result.title || topic}
wordCount={result.word_count ?? undefined}
shareToken={shareToken}
autoOpen={sawRunningRef.current}
/>
);
}
return <ReportErrorState title={topic} error="Missing report ID" />;
};

View file

@ -492,7 +492,13 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const CreateGoogleDriveFileToolUI = ({ args, result }: ToolCallMessagePartProps<{ name: string; file_type: string; content?: string }, CreateGoogleDriveFileResult>) => {
export const CreateGoogleDriveFileToolUI = ({
args,
result,
}: ToolCallMessagePartProps<
{ name: string; file_type: string; content?: string },
CreateGoogleDriveFileResult
>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -410,7 +410,12 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const DeleteGoogleDriveFileToolUI = ({ result }: ToolCallMessagePartProps<{ file_name: string; delete_from_kb?: boolean }, DeleteGoogleDriveFileResult>) => {
export const DeleteGoogleDriveFileToolUI = ({
result,
}: ToolCallMessagePartProps<
{ file_name: string; delete_from_kb?: boolean },
DeleteGoogleDriveFileResult
>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -4,9 +4,9 @@ import { ExternalLinkIcon, ImageIcon, SparklesIcon } from "lucide-react";
import NextImage from "next/image";
import { Component, type ReactNode, useState } from "react";
import { z } from "zod";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
import { cn } from "@/lib/utils";
/**
@ -145,14 +145,14 @@ export class ImageErrorBoundary extends Component<
render() {
if (this.state.hasError) {
return (
<Card className="w-full max-w-md overflow-hidden rounded-2xl border-0 shadow-none select-none">
<div className="aspect-square bg-muted flex items-center justify-center">
<div className="flex flex-col items-center gap-2 text-muted-foreground">
<ImageIcon className="size-8" />
<p className="text-sm">Failed to load image</p>
<Card className="w-full max-w-md overflow-hidden rounded-2xl border-0 shadow-none select-none">
<div className="aspect-square bg-muted flex items-center justify-center">
<div className="flex flex-col items-center gap-2 text-muted-foreground">
<ImageIcon className="size-8" />
<p className="text-sm">Failed to load image</p>
</div>
</div>
</div>
</Card>
</Card>
);
}
@ -165,7 +165,10 @@ export class ImageErrorBoundary extends Component<
*/
export function ImageSkeleton({ maxWidth = "512px" }: { maxWidth?: string }) {
return (
<Card className="w-full overflow-hidden rounded-2xl border-0 shadow-none select-none animate-pulse" style={{ maxWidth }}>
<Card
className="w-full overflow-hidden rounded-2xl border-0 shadow-none select-none animate-pulse"
style={{ maxWidth }}
>
<div className="aspect-square bg-muted flex items-center justify-center">
<ImageIcon className="size-12 text-muted-foreground/30" />
</div>
@ -176,9 +179,18 @@ export function ImageSkeleton({ maxWidth = "512px" }: { maxWidth?: string }) {
/**
* Image Loading State
*/
export function ImageLoading({ title = "Loading", maxWidth = "512px" }: { title?: string; maxWidth?: string }) {
export function ImageLoading({
title = "Loading",
maxWidth = "512px",
}: {
title?: string;
maxWidth?: string;
}) {
return (
<Card className="w-full overflow-hidden rounded-2xl border-0 shadow-none select-none" style={{ maxWidth }}>
<Card
className="w-full overflow-hidden rounded-2xl border-0 shadow-none select-none"
style={{ maxWidth }}
>
<div className="aspect-square bg-muted flex items-center justify-center">
<TextShimmerLoader text={title} size="md" />
</div>
@ -212,7 +224,7 @@ export function Image({
const [imageError, setImageError] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const isGenerated = domain === "ai-generated";
const displayDomain = isGenerated ? "AI Generated" : (domain || source?.label);
const displayDomain = isGenerated ? "AI Generated" : domain || source?.label;
const isAutoRatio = !ratio || ratio === "auto";
const handleClick = () => {
@ -224,7 +236,14 @@ export function Image({
if (imageError) {
return (
<Card id={id} className={cn("w-full overflow-hidden rounded-2xl border-0 shadow-none select-none", className)} style={{ maxWidth }}>
<Card
id={id}
className={cn(
"w-full overflow-hidden rounded-2xl border-0 shadow-none select-none",
className
)}
style={{ maxWidth }}
>
<div className="aspect-square bg-muted flex items-center justify-center">
<div className="flex flex-col items-center gap-2 text-muted-foreground">
<ImageIcon className="size-8" />
@ -259,11 +278,11 @@ export function Image({
{isAutoRatio ? (
/* Auto ratio: image renders at natural dimensions, no cropping */
<>
{!imageLoaded && (
<div className="aspect-square flex items-center justify-center">
<TextShimmerLoader text="Loading" size="md" />
</div>
)}
{!imageLoaded && (
<div className="aspect-square flex items-center justify-center">
<TextShimmerLoader text="Loading" size="md" />
</div>
)}
<NextImage
src={src}
alt={alt}

View file

@ -16,7 +16,6 @@ export {
} from "./generate-image";
export { GeneratePodcastToolUI } from "./generate-podcast";
export { GenerateReportToolUI } from "./generate-report";
export { GenerateVideoPresentationToolUI } from "./video-presentation";
export { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI } from "./google-drive";
export {
Image,
@ -62,4 +61,5 @@ export {
SaveMemoryResultSchema,
SaveMemoryToolUI,
} from "./user-memory";
export { GenerateVideoPresentationToolUI } from "./video-presentation";
export { type WriteTodosData, WriteTodosSchema, WriteTodosToolUI } from "./write-todos";

View file

@ -536,7 +536,10 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const CreateJiraIssueToolUI = ({ args, result }: ToolCallMessagePartProps<
export const CreateJiraIssueToolUI = ({
args,
result,
}: ToolCallMessagePartProps<
{
project_key: string;
summary: string;
@ -546,35 +549,35 @@ export const CreateJiraIssueToolUI = ({ args, result }: ToolCallMessagePartProps
},
CreateJiraIssueResult
>) => {
if (!result) return null;
if (!result) return null;
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;
return <SuccessCard result={result as SuccessResult} />;
};

View file

@ -393,41 +393,43 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const DeleteJiraIssueToolUI = ({ result }: ToolCallMessagePartProps<
export const DeleteJiraIssueToolUI = ({
result,
}: ToolCallMessagePartProps<
{ issue_title_or_key: string; delete_from_kb?: boolean },
DeleteJiraIssueResult
>) => {
if (!result) return null;
if (!result) return null;
if (isInterruptResult(result)) {
return (
<ApprovalCard
interruptData={result}
onDecision={(decision) => {
const event = new CustomEvent("hitl-decision", {
detail: { decisions: [decision] },
});
window.dispatchEvent(event);
}}
/>
);
}
if (isInterruptResult(result)) {
return (
<ApprovalCard
interruptData={result}
onDecision={(decision) => {
const event = new CustomEvent("hitl-decision", {
detail: { decisions: [decision] },
});
window.dispatchEvent(event);
}}
/>
);
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isWarningResult(result)) return <WarningCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isWarningResult(result)) return <WarningCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;
return <SuccessCard result={result as SuccessResult} />;
};

View file

@ -553,7 +553,10 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const UpdateJiraIssueToolUI = ({ args, result }: ToolCallMessagePartProps<
export const UpdateJiraIssueToolUI = ({
args,
result,
}: ToolCallMessagePartProps<
{
issue_title_or_key: string;
new_summary?: string;
@ -562,36 +565,36 @@ export const UpdateJiraIssueToolUI = ({ args, result }: ToolCallMessagePartProps
},
UpdateJiraIssueResult
>) => {
if (!result) return null;
if (!result) return null;
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (isInterruptResult(result)) {
return (
<ApprovalCard
args={args}
interruptData={result}
onDecision={(decision) => {
window.dispatchEvent(
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
);
}}
/>
);
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (
typeof result === "object" &&
result !== null &&
"status" in result &&
(result as { status: string }).status === "rejected"
) {
return null;
}
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
if (isInsufficientPermissionsResult(result))
return <InsufficientPermissionsCard result={result} />;
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
if (isErrorResult(result)) return <ErrorCard result={result} />;
return <SuccessCard result={result as SuccessResult} />;
return <SuccessCard result={result as SuccessResult} />;
};

View file

@ -605,7 +605,10 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const CreateLinearIssueToolUI = ({ args, result }: ToolCallMessagePartProps<{ title: string; description?: string }, CreateLinearIssueResult>) => {
export const CreateLinearIssueToolUI = ({
args,
result,
}: ToolCallMessagePartProps<{ title: string; description?: string }, CreateLinearIssueResult>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -360,7 +360,12 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const DeleteLinearIssueToolUI = ({ result }: ToolCallMessagePartProps<{ issue_ref: string; delete_from_kb?: boolean }, DeleteLinearIssueResult>) => {
export const DeleteLinearIssueToolUI = ({
result,
}: ToolCallMessagePartProps<
{ issue_ref: string; delete_from_kb?: boolean },
DeleteLinearIssueResult
>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -739,15 +739,21 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const UpdateLinearIssueToolUI = ({ args, result }: ToolCallMessagePartProps<{
issue_ref: string;
new_title?: string;
new_description?: string;
new_state_name?: string;
new_assignee_email?: string;
new_priority?: number;
new_label_names?: string[];
}, UpdateLinearIssueResult>) => {
export const UpdateLinearIssueToolUI = ({
args,
result,
}: ToolCallMessagePartProps<
{
issue_ref: string;
new_title?: string;
new_description?: string;
new_state_name?: string;
new_assignee_email?: string;
new_priority?: number;
new_label_names?: string[];
},
UpdateLinearIssueResult
>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -445,7 +445,10 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const CreateNotionPageToolUI = ({ args, result }: ToolCallMessagePartProps<{ title: string; content: string }, CreateNotionPageResult>) => {
export const CreateNotionPageToolUI = ({
args,
result,
}: ToolCallMessagePartProps<{ title: string; content: string }, CreateNotionPageResult>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -372,7 +372,12 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const DeleteNotionPageToolUI = ({ result }: ToolCallMessagePartProps<{ page_title: string; delete_from_kb?: boolean }, DeleteNotionPageResult>) => {
export const DeleteNotionPageToolUI = ({
result,
}: ToolCallMessagePartProps<
{ page_title: string; delete_from_kb?: boolean },
DeleteNotionPageResult
>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -395,7 +395,10 @@ function SuccessCard({ result }: { result: SuccessResult }) {
);
}
export const UpdateNotionPageToolUI = ({ args, result }: ToolCallMessagePartProps<{ page_title: string; content: string }, UpdateNotionPageResult>) => {
export const UpdateNotionPageToolUI = ({
args,
result,
}: ToolCallMessagePartProps<{ page_title: string; content: string }, UpdateNotionPageResult>) => {
if (!result) return null;
if (isInterruptResult(result)) {

View file

@ -380,38 +380,42 @@ function ExecuteCompleted({
// Tool UI
// ============================================================================
export const SandboxExecuteToolUI = ({ args, result, status }: ToolCallMessagePartProps<ExecuteArgs, ExecuteResult>) => {
const command = args.command || "…";
export const SandboxExecuteToolUI = ({
args,
result,
status,
}: ToolCallMessagePartProps<ExecuteArgs, ExecuteResult>) => {
const command = args.command || "…";
if (status.type === "running" || status.type === "requires-action") {
return <ExecuteLoading command={command} />;
if (status.type === "running" || status.type === "requires-action") {
return <ExecuteLoading command={command} />;
}
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return <ExecuteCancelledState command={command} />;
}
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return <ExecuteCancelledState command={command} />;
}
if (status.reason === "error") {
return (
<ExecuteErrorState
command={command}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
if (status.reason === "error") {
return (
<ExecuteErrorState
command={command}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
if (!result) {
return <ExecuteLoading command={command} />;
}
if (!result) {
return <ExecuteLoading command={command} />;
}
if (result.error && !result.result && !result.output) {
return <ExecuteErrorState command={command} error={result.error} />;
}
if (result.error && !result.result && !result.output) {
return <ExecuteErrorState command={command} error={result.error} />;
}
const parsed = parseExecuteResult(result);
const threadId = result.thread_id || null;
return <ExecuteCompleted command={command} parsed={parsed} threadId={threadId} />;
const parsed = parseExecuteResult(result);
const threadId = result.thread_id || null;
return <ExecuteCompleted command={command} parsed={parsed} threadId={threadId} />;
};
export { ExecuteArgsSchema, ExecuteResultSchema, type ExecuteArgs, type ExecuteResult };

View file

@ -80,184 +80,192 @@ function CategoryBadge({ category }: { category: string }) {
// Save Memory Tool UI
// ============================================================================
export const SaveMemoryToolUI = ({ args, result, status }: ToolCallMessagePartProps<SaveMemoryArgs, SaveMemoryResult>) => {
const isRunning = status.type === "running" || status.type === "requires-action";
const isComplete = status.type === "complete";
const isError = result?.status === "error";
export const SaveMemoryToolUI = ({
args,
result,
status,
}: ToolCallMessagePartProps<SaveMemoryArgs, SaveMemoryResult>) => {
const isRunning = status.type === "running" || status.type === "requires-action";
const isComplete = status.type === "complete";
const isError = result?.status === "error";
// Parse args safely
const parsedArgs = SaveMemoryArgsSchema.safeParse(args);
const content = parsedArgs.success ? parsedArgs.data.content : "";
const category = parsedArgs.success ? parsedArgs.data.category : "fact";
// Parse args safely
const parsedArgs = SaveMemoryArgsSchema.safeParse(args);
const content = parsedArgs.success ? parsedArgs.data.content : "";
const category = parsedArgs.success ? parsedArgs.data.category : "fact";
// Loading state
if (isRunning) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10">
<Loader2Icon className="size-4 animate-spin text-primary" />
</div>
<div className="flex-1">
<span className="text-sm text-muted-foreground">Saving to memory...</span>
</div>
// Loading state
if (isRunning) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10">
<Loader2Icon className="size-4 animate-spin text-primary" />
</div>
);
}
// Error state
if (isError) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border border-destructive/20 bg-destructive/5 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-destructive/10">
<XIcon className="size-4 text-destructive" />
</div>
<div className="flex-1">
<span className="text-sm text-destructive">Failed to save memory</span>
{result?.error && <p className="mt-1 text-xs text-destructive/70">{result.error}</p>}
</div>
<div className="flex-1">
<span className="text-sm text-muted-foreground">Saving to memory...</span>
</div>
);
}
</div>
);
}
// Success state
if (isComplete && result?.status === "saved") {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border border-primary/20 bg-primary/5 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10">
<BrainIcon className="size-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<CheckIcon className="size-3 text-green-500 shrink-0" />
<span className="text-sm font-medium text-foreground">Memory saved</span>
<CategoryBadge category={category} />
</div>
<p className="mt-1 truncate text-sm text-muted-foreground">{content}</p>
</div>
// Error state
if (isError) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border border-destructive/20 bg-destructive/5 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-destructive/10">
<XIcon className="size-4 text-destructive" />
</div>
);
}
// Default/incomplete state - show what's being saved
if (content) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-muted">
<BrainIcon className="size-4 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Saving memory</span>
<CategoryBadge category={category} />
</div>
<p className="mt-1 truncate text-sm text-muted-foreground">{content}</p>
</div>
<div className="flex-1">
<span className="text-sm text-destructive">Failed to save memory</span>
{result?.error && <p className="mt-1 text-xs text-destructive/70">{result.error}</p>}
</div>
);
}
</div>
);
}
return null;
// Success state
if (isComplete && result?.status === "saved") {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border border-primary/20 bg-primary/5 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10">
<BrainIcon className="size-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<CheckIcon className="size-3 text-green-500 shrink-0" />
<span className="text-sm font-medium text-foreground">Memory saved</span>
<CategoryBadge category={category} />
</div>
<p className="mt-1 truncate text-sm text-muted-foreground">{content}</p>
</div>
</div>
);
}
// Default/incomplete state - show what's being saved
if (content) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-muted">
<BrainIcon className="size-4 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Saving memory</span>
<CategoryBadge category={category} />
</div>
<p className="mt-1 truncate text-sm text-muted-foreground">{content}</p>
</div>
</div>
);
}
return null;
};
// ============================================================================
// Recall Memory Tool UI
// ============================================================================
export const RecallMemoryToolUI = ({ args, result, status }: ToolCallMessagePartProps<RecallMemoryArgs, RecallMemoryResult>) => {
const isRunning = status.type === "running" || status.type === "requires-action";
const isComplete = status.type === "complete";
const isError = result?.status === "error";
export const RecallMemoryToolUI = ({
args,
result,
status,
}: ToolCallMessagePartProps<RecallMemoryArgs, RecallMemoryResult>) => {
const isRunning = status.type === "running" || status.type === "requires-action";
const isComplete = status.type === "complete";
const isError = result?.status === "error";
// Parse args safely
const parsedArgs = RecallMemoryArgsSchema.safeParse(args);
const query = parsedArgs.success ? parsedArgs.data.query : null;
// Parse args safely
const parsedArgs = RecallMemoryArgsSchema.safeParse(args);
const query = parsedArgs.success ? parsedArgs.data.query : null;
// Loading state
if (isRunning) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10">
<Loader2Icon className="size-4 animate-spin text-primary" />
</div>
<div className="flex-1">
<span className="text-sm text-muted-foreground">
{query ? `Searching memories for "${query}"...` : "Recalling memories..."}
</span>
</div>
// Loading state
if (isRunning) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10">
<Loader2Icon className="size-4 animate-spin text-primary" />
</div>
);
}
// Error state
if (isError) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border border-destructive/20 bg-destructive/5 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-destructive/10">
<XIcon className="size-4 text-destructive" />
</div>
<div className="flex-1">
<span className="text-sm text-destructive">Failed to recall memories</span>
{result?.error && <p className="mt-1 text-xs text-destructive/70">{result.error}</p>}
</div>
<div className="flex-1">
<span className="text-sm text-muted-foreground">
{query ? `Searching memories for "${query}"...` : "Recalling memories..."}
</span>
</div>
);
}
</div>
);
}
// Success state with memories
if (isComplete && result?.status === "success") {
const memories = result.memories || [];
const count = result.count || 0;
if (count === 0) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-muted">
<SearchIcon className="size-4 text-muted-foreground" />
</div>
<span className="text-sm text-muted-foreground">No memories found</span>
</div>
);
}
return (
<div className="my-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex items-center gap-2 mb-2">
<BrainIcon className="size-4 text-primary" />
<span className="text-sm font-medium text-foreground">
Recalled {count} {count === 1 ? "memory" : "memories"}
</span>
</div>
<div className="space-y-2">
{memories.slice(0, 5).map((memory: MemoryItem) => (
<div
key={memory.id}
className="flex items-start gap-2 rounded-md bg-muted/50 px-3 py-2"
>
<CategoryBadge category={memory.category} />
<span className="text-sm text-muted-foreground flex-1">{memory.memory_text}</span>
</div>
))}
{memories.length > 5 && (
<p className="text-xs text-muted-foreground">...and {memories.length - 5} more</p>
)}
</div>
// Error state
if (isError) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border border-destructive/20 bg-destructive/5 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-destructive/10">
<XIcon className="size-4 text-destructive" />
</div>
);
}
<div className="flex-1">
<span className="text-sm text-destructive">Failed to recall memories</span>
{result?.error && <p className="mt-1 text-xs text-destructive/70">{result.error}</p>}
</div>
</div>
);
}
// Default/incomplete state
if (query) {
// Success state with memories
if (isComplete && result?.status === "success") {
const memories = result.memories || [];
const count = result.count || 0;
if (count === 0) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-muted">
<SearchIcon className="size-4 text-muted-foreground" />
</div>
<span className="text-sm text-muted-foreground">Searching memories for "{query}"</span>
<span className="text-sm text-muted-foreground">No memories found</span>
</div>
);
}
return null;
return (
<div className="my-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex items-center gap-2 mb-2">
<BrainIcon className="size-4 text-primary" />
<span className="text-sm font-medium text-foreground">
Recalled {count} {count === 1 ? "memory" : "memories"}
</span>
</div>
<div className="space-y-2">
{memories.slice(0, 5).map((memory: MemoryItem) => (
<div
key={memory.id}
className="flex items-start gap-2 rounded-md bg-muted/50 px-3 py-2"
>
<CategoryBadge category={memory.category} />
<span className="text-sm text-muted-foreground flex-1">{memory.memory_text}</span>
</div>
))}
{memories.length > 5 && (
<p className="text-xs text-muted-foreground">...and {memories.length - 5} more</p>
)}
</div>
</div>
);
}
// Default/incomplete state
if (query) {
return (
<div className="my-3 flex items-center gap-3 rounded-lg border bg-card/60 px-4 py-3">
<div className="flex size-8 items-center justify-center rounded-full bg-muted">
<SearchIcon className="size-4 text-muted-foreground" />
</div>
<span className="text-sm text-muted-foreground">Searching memories for "{query}"</span>
</div>
);
}
return null;
};
// ============================================================================

View file

@ -1,9 +1,10 @@
"use client";
import React, { useMemo } from "react";
import { Player } from "@remotion/player";
import { Sequence, AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from "remotion";
import { Audio } from "@remotion/media";
import { Player } from "@remotion/player";
import type React from "react";
import { useMemo } from "react";
import { AbsoluteFill, interpolate, Sequence, useCurrentFrame, useVideoConfig } from "remotion";
import { FPS } from "@/lib/remotion/constants";
export interface CompiledSlide {
@ -64,9 +65,7 @@ function Watermark() {
);
}
export function buildSlideWithWatermark(
SlideComponent: React.ComponentType,
): React.FC {
export function buildSlideWithWatermark(SlideComponent: React.ComponentType): React.FC {
const Wrapped: React.FC = () => (
<AbsoluteFill>
<SlideComponent />
@ -115,7 +114,7 @@ export function CombinedPlayer({ slides }: CombinedPlayerProps) {
const totalFrames = useMemo(
() => slides.reduce((sum, s) => sum + s.durationInFrames, 0),
[slides],
[slides]
);
return (

View file

@ -1,7 +1,11 @@
export function getVideoDownloadErrorToast(err: unknown): { title: string; description: string } {
const msg = err instanceof Error ? err.message.toLowerCase() : "";
if (msg.includes("webcodecs") || msg.includes("canrendermediaonweb") || msg.includes("not support")) {
if (
msg.includes("webcodecs") ||
msg.includes("canrendermediaonweb") ||
msg.includes("not support")
) {
return {
title: "Browser Not Supported",
description: "Video rendering requires Chrome, Edge, or Firefox 130+.",
@ -24,7 +28,11 @@ export function getVideoDownloadErrorToast(err: unknown): { title: string; descr
export function getPptxExportErrorToast(err: unknown): { title: string; description: string } {
const msg = err instanceof Error ? err.message.toLowerCase() : "";
if (msg.includes("dynamically imported") || msg.includes("failed to fetch") || msg.includes("network")) {
if (
msg.includes("dynamically imported") ||
msg.includes("failed to fetch") ||
msg.includes("network")
) {
return {
title: "Export Unavailable",
description: "Could not load the export module. Check your network and try again.",

View file

@ -1,9 +1,9 @@
"use client";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { Dot, Download, Loader2, Presentation, X } from "lucide-react";
import { useParams, usePathname } from "next/navigation";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { z } from "zod";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
@ -13,12 +13,12 @@ import { authenticatedFetch } from "@/lib/auth-utils";
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
import { FPS } from "@/lib/remotion/constants";
import {
CombinedPlayer,
buildCompositionComponent,
buildSlideWithWatermark,
CombinedPlayer,
type CompiledSlide,
} from "./combined-player";
import { getVideoDownloadErrorToast, getPptxExportErrorToast } from "./errors";
import { getPptxExportErrorToast, getVideoDownloadErrorToast } from "./errors";
const GenerateVideoPresentationArgsSchema = z.object({
source_content: z.string(),
@ -50,7 +50,7 @@ const VideoPresentationStatusResponseSchema = z.object({
audio_url: z.string().nullish(),
duration_seconds: z.number().nullish(),
duration_in_frames: z.number().nullish(),
}),
})
)
.nullish(),
scene_codes: z
@ -59,7 +59,7 @@ const VideoPresentationStatusResponseSchema = z.object({
slide_number: z.number(),
code: z.string(),
title: z.string().nullish(),
}),
})
)
.nullish(),
slide_count: z.number().nullish(),
@ -69,7 +69,6 @@ type GenerateVideoPresentationArgs = z.infer<typeof GenerateVideoPresentationArg
type GenerateVideoPresentationResult = z.infer<typeof GenerateVideoPresentationResultSchema>;
type VideoPresentationStatusResponse = z.infer<typeof VideoPresentationStatusResponseSchema>;
function parseStatusResponse(data: unknown): VideoPresentationStatusResponse | null {
const result = VideoPresentationStatusResponseSchema.safeParse(data);
if (!result.success) {
@ -166,9 +165,7 @@ function VideoPresentationPlayer({
const durationInFrames = slide.duration_in_frames ?? 300;
const check = compileCheck(scene.code);
if (!check.success) {
console.warn(
`Slide ${slide.slide_number} failed to compile: ${check.error}`,
);
console.warn(`Slide ${slide.slide_number} failed to compile: ${check.error}`);
continue;
}
@ -179,9 +176,7 @@ function VideoPresentationPlayer({
title: scene.title ?? slide.title,
code: scene.code,
durationInFrames,
audioUrl: slide.audio_url
? `${backendUrl}${slide.audio_url}`
: undefined,
audioUrl: slide.audio_url ? `${backendUrl}${slide.audio_url}` : undefined,
});
}
@ -198,17 +193,13 @@ function VideoPresentationPlayer({
try {
let blob: Blob;
if (shareToken) {
blob = await baseApiService.getBlob(
new URL(slide.audioUrl).pathname,
);
blob = await baseApiService.getBlob(new URL(slide.audioUrl).pathname);
} else {
const resp = await authenticatedFetch(slide.audioUrl, {
method: "GET",
});
if (!resp.ok) {
console.warn(
`Audio fetch ${resp.status} for slide "${slide.title}"`,
);
console.warn(`Audio fetch ${resp.status} for slide "${slide.title}"`);
return { ...slide, audioUrl: undefined };
}
blob = await resp.blob();
@ -220,7 +211,7 @@ function VideoPresentationPlayer({
console.warn(`Failed to fetch audio for "${slide.title}":`, err);
return { ...slide, audioUrl: undefined };
}
}),
})
);
setCompiledSlides(withBlobs);
@ -244,7 +235,7 @@ function VideoPresentationPlayer({
const totalDuration = useMemo(
() => compiledSlides.reduce((sum, s) => sum + s.durationInFrames / FPS, 0),
[compiledSlides],
[compiledSlides]
);
const handleDownload = async () => {
@ -258,9 +249,7 @@ function VideoPresentationPlayer({
abortControllerRef.current = controller;
try {
const { canRenderMediaOnWeb, renderMediaOnWeb } = await import(
"@remotion/web-renderer"
);
const { canRenderMediaOnWeb, renderMediaOnWeb } = await import("@remotion/web-renderer");
const formats = [
{ container: "mp4" as const, videoCodec: "h264" as const, ext: "mp4" },
@ -285,7 +274,7 @@ function VideoPresentationPlayer({
if (!chosen) {
throw new Error(
"Your browser does not support video rendering (WebCodecs). Please use Chrome, Edge, or Firefox 130+.",
"Your browser does not support video rendering (WebCodecs). Please use Chrome, Edge, or Firefox 130+."
);
}
@ -379,7 +368,7 @@ function VideoPresentationPlayer({
durationInFrames: slide.durationInFrames,
fps: FPS,
style: { width: 1920, height: 1080 },
}),
})
);
});
@ -419,7 +408,8 @@ function VideoPresentationPlayer({
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-foreground line-clamp-2">{title}</p>
<p className="text-xs text-muted-foreground mt-0.5 flex items-center">
{compiledSlides.length} slides <Dot className="size-4" /> {totalDuration.toFixed(1)}s <Dot className="size-4" /> {FPS}fps
{compiledSlides.length} slides <Dot className="size-4" /> {totalDuration.toFixed(1)}s{" "}
<Dot className="size-4" /> {FPS}fps
</p>
</div>
@ -440,9 +430,7 @@ function VideoPresentationPlayer({
<Loader2 className="size-3.5 animate-spin text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground">
Rendering {renderFormat ?? ""}{" "}
{renderProgress !== null
? `${Math.round(renderProgress * 100)}%`
: "..."}
{renderProgress !== null ? `${Math.round(renderProgress * 100)}%` : "..."}
</span>
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
<div
@ -493,7 +481,6 @@ function VideoPresentationPlayer({
</>
)}
</div>
</div>
);
}
@ -564,85 +551,85 @@ function StatusPoller({
return <ErrorState title={title} error="Unexpected state" />;
}
export const GenerateVideoPresentationToolUI = ({ args, result, status }: ToolCallMessagePartProps<
GenerateVideoPresentationArgs,
GenerateVideoPresentationResult
>) => {
const params = useParams();
const pathname = usePathname();
const isPublicRoute = pathname?.startsWith("/public/");
const shareToken =
isPublicRoute && typeof params?.token === "string" ? params.token : null;
export const GenerateVideoPresentationToolUI = ({
args,
result,
status,
}: ToolCallMessagePartProps<GenerateVideoPresentationArgs, GenerateVideoPresentationResult>) => {
const params = useParams();
const pathname = usePathname();
const isPublicRoute = pathname?.startsWith("/public/");
const shareToken = isPublicRoute && typeof params?.token === "string" ? params.token : null;
const title = args.video_title || "SurfSense Presentation";
const title = args.video_title || "SurfSense Presentation";
if (status.type === "running" || status.type === "requires-action") {
return <GeneratingState title={title} />;
}
if (status.type === "running" || status.type === "requires-action") {
return <GeneratingState title={title} />;
}
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-muted-foreground">Presentation Cancelled</p>
<p className="text-xs text-muted-foreground mt-0.5">
Presentation generation was cancelled
</p>
</div>
</div>
);
}
if (status.reason === "error") {
return (
<ErrorState
title={title}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
if (!result) {
return <GeneratingState title={title} />;
}
if (result.status === "failed") {
return <ErrorState title={title} error={result.error || "Generation failed"} />;
}
if (result.status === "generating") {
if (status.type === "incomplete") {
if (status.reason === "cancelled") {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-foreground">Presentation already in progress</p>
<p className="text-sm font-semibold text-muted-foreground">Presentation Cancelled</p>
<p className="text-xs text-muted-foreground mt-0.5">
Please wait for the current presentation to complete.
Presentation generation was cancelled
</p>
</div>
</div>
);
}
if (result.status === "pending" && result.video_presentation_id) {
if (status.reason === "error") {
return (
<StatusPoller
presentationId={result.video_presentation_id}
title={result.title || title}
shareToken={shareToken}
<ErrorState
title={title}
error={typeof status.error === "string" ? status.error : "An error occurred"}
/>
);
}
}
if (result.status === "ready" && result.video_presentation_id) {
return (
<VideoPresentationPlayer
presentationId={result.video_presentation_id}
title={result.title || title}
shareToken={shareToken}
/>
);
}
if (!result) {
return <GeneratingState title={title} />;
}
return <ErrorState title={title} error="Missing presentation ID" />;
if (result.status === "failed") {
return <ErrorState title={title} error={result.error || "Generation failed"} />;
}
if (result.status === "generating") {
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
<div className="px-5 pt-5 pb-4">
<p className="text-sm font-semibold text-foreground">Presentation already in progress</p>
<p className="text-xs text-muted-foreground mt-0.5">
Please wait for the current presentation to complete.
</p>
</div>
</div>
);
}
if (result.status === "pending" && result.video_presentation_id) {
return (
<StatusPoller
presentationId={result.video_presentation_id}
title={result.title || title}
shareToken={shareToken}
/>
);
}
if (result.status === "ready" && result.video_presentation_id) {
return (
<VideoPresentationPlayer
presentationId={result.video_presentation_id}
title={result.title || title}
shareToken={shareToken}
/>
);
}
return <ErrorState title={title} error="Missing presentation ID" />;
};

View file

@ -19,8 +19,7 @@ const carouselItems = [
},
{
title: "Video Generation",
description:
"Create short videos with AI-generated visuals and narration from your sources.",
description: "Create short videos with AI-generated visuals and narration from your sources.",
src: "/homepage/hero_tutorial/video_gen_surf.mp4",
},
{

View file

@ -74,7 +74,8 @@ export function useConnectorsElectric(searchSpaceId: number | string | null) {
async function startSync() {
try {
if (IS_DEV) console.log("[useConnectorsElectric] Starting sync for search space:", searchSpaceId);
if (IS_DEV)
console.log("[useConnectorsElectric] Starting sync for search space:", searchSpaceId);
const handle = await electricClient.syncShape({
table: "search_source_connectors",
@ -82,9 +83,10 @@ export function useConnectorsElectric(searchSpaceId: number | string | null) {
primaryKey: ["id"],
});
if (IS_DEV) console.log("[useConnectorsElectric] Sync started:", {
isUpToDate: handle.isUpToDate,
});
if (IS_DEV)
console.log("[useConnectorsElectric] Sync started:", {
isUpToDate: handle.isUpToDate,
});
// Wait for initial sync with timeout
if (!handle.isUpToDate && handle.initialSyncPromise) {

View file

@ -2,12 +2,12 @@ import * as Babel from "@babel/standalone";
import React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
Easing,
interpolate,
Sequence,
Easing,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { DURATION_IN_FRAMES } from "./constants";
@ -21,7 +21,7 @@ function createStagger(totalFrames: number) {
frame: number,
fps: number,
index: number,
total: number,
total: number
): { opacity: number; transform: string } {
const enterPhase = Math.floor(totalFrames * 0.2);
const exitStart = Math.floor(totalFrames * 0.8);
@ -43,9 +43,7 @@ function createStagger(totalFrames: number) {
const opacity = s * (1 - exit);
const translateY =
interpolate(s, [0, 1], [40, 0]) +
interpolate(exit, [0, 1], [0, -30]) +
ambient;
interpolate(s, [0, 1], [40, 0]) + interpolate(exit, [0, 1], [0, -30]) + ambient;
const scale = interpolate(s, [0, 1], [0.97, 1]);
return {
@ -97,7 +95,7 @@ export function prepareSource(code: string): string {
const codeWithoutImports = code.replace(/^import\s+.*$/gm, "").trim();
const match = codeWithoutImports.match(
/export\s+(?:const|function)\s+(\w+)\s*(?::\s*React\.FC\s*)?=?\s*\(\s*\)\s*=>\s*\{([\s\S]*)\};?\s*$/,
/export\s+(?:const|function)\s+(\w+)\s*(?::\s*React\.FC\s*)?=?\s*\(\s*\)\s*=>\s*\{([\s\S]*)\};?\s*$/
);
if (match) {
@ -137,18 +135,10 @@ export function compileCheck(code: string): CompileResult {
}
}
export function compileToComponent(
code: string,
durationInFrames?: number,
): React.ComponentType {
const staggerFn = durationInFrames
? createStagger(durationInFrames)
: defaultStagger;
export function compileToComponent(code: string, durationInFrames?: number): React.ComponentType {
const staggerFn = durationInFrames ? createStagger(durationInFrames) : defaultStagger;
const jsCode = transpile(code);
const factory = new Function(
...INJECTED_NAMES,
`${jsCode}\nreturn DynamicComponent;`,
);
const factory = new Function(...INJECTED_NAMES, `${jsCode}\nreturn DynamicComponent;`);
return factory(...buildInjectedValues(staggerFn)) as React.ComponentType;
}

View file

@ -13,6 +13,6 @@ declare module "dom-to-pptx" {
export function exportToPptx(
elementOrSelector: string | HTMLElement | Array<string | HTMLElement>,
options?: ExportOptions,
options?: ExportOptions
): Promise<Blob>;
}