mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: ran linting
This commit is contained in:
parent
323f7f2b4a
commit
c674fb3054
37 changed files with 972 additions and 920 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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" />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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" />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
2
surfsense_web/lib/remotion/dom-to-pptx.d.ts
vendored
2
surfsense_web/lib/remotion/dom-to-pptx.d.ts
vendored
|
|
@ -13,6 +13,6 @@ declare module "dom-to-pptx" {
|
|||
|
||||
export function exportToPptx(
|
||||
elementOrSelector: string | HTMLElement | Array<string | HTMLElement>,
|
||||
options?: ExportOptions,
|
||||
options?: ExportOptions
|
||||
): Promise<Blob>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue