fix: resolve runtime crashes in tool-ui components and backend import errors

Backend:
- Remove dead imports (display_image, link_preview, knowledge_base) from tools registry and __init__
- Delete orphaned elif chain (lines 784-1046) in knowledge_base.py left after asyncio.gather refactor
- Add missing NotionAPIError class to notion_history.py

Frontend:
- Add display-image.tsx, link-preview.tsx, scrape-webpage.tsx tool UI components
- Fix GeneratePodcastToolUI: add null guard for no-props render + optional chaining on status/args
- Fix SaveMemoryToolUI/RecallMemoryToolUI: add null guard when rendered without props in provider
- Update launch.json to use pnpm on port 3999

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vonic 2026-04-14 13:42:31 +07:00
parent 67429c287f
commit dc545f8028
10 changed files with 236 additions and 273 deletions

View file

@ -0,0 +1,54 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { z } from "zod";
import { ImageIcon } from "lucide-react";
const DisplayImageArgsSchema = z.object({
url: z.string().optional(),
alt: z.string().optional(),
caption: z.string().optional(),
});
const DisplayImageResultSchema = z.object({
url: z.string().optional(),
alt: z.string().optional(),
caption: z.string().optional(),
}).passthrough();
type DisplayImageArgs = z.infer<typeof DisplayImageArgsSchema>;
type DisplayImageResult = z.infer<typeof DisplayImageResultSchema>;
export const DisplayImageToolUI = makeAssistantToolUI<DisplayImageArgs, DisplayImageResult>({
toolName: "display_image",
render: ({ args, result, status }) => {
const isLoading = status.type === "running";
const imageUrl = result?.url ?? args?.url;
const altText = result?.alt ?? args?.alt ?? "Image";
const caption = result?.caption ?? args?.caption;
if (isLoading) {
return (
<div className="my-3 flex items-center gap-2 rounded-lg border bg-card/60 px-4 py-3">
<ImageIcon className="size-4 animate-pulse text-muted-foreground" />
<span className="text-sm text-muted-foreground">Loading image...</span>
</div>
);
}
if (!imageUrl) return null;
return (
<div className="my-3 rounded-lg border bg-card/60 overflow-hidden">
<img
src={imageUrl}
alt={altText}
className="w-full max-h-96 object-contain"
/>
{caption && (
<p className="px-4 py-2 text-sm text-muted-foreground">{caption}</p>
)}
</div>
);
},
});

View file

@ -377,15 +377,19 @@ export const GeneratePodcastToolUI = ({
result,
status,
}: ToolCallMessagePartProps<GeneratePodcastArgs, GeneratePodcastResult>) => {
const title = args.podcast_title || "SurfSense Podcast";
// Guard: when rendered without props (e.g. as <GeneratePodcastToolUI /> in provider),
// render nothing — actual rendering happens via assistant-message.tsx by_name map.
if (!status && !result && !args) return null;
const title = args?.podcast_title || "SurfSense Podcast";
// Loading state - tool is still running (agent processing)
if (status.type === "running" || status.type === "requires-action") {
if (status?.type === "running" || status?.type === "requires-action") {
return <PodcastGeneratingState title={title} />;
}
// Incomplete/cancelled state
if (status.type === "incomplete") {
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">

View file

@ -0,0 +1,61 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { z } from "zod";
import { ExternalLinkIcon, Loader2Icon } from "lucide-react";
const LinkPreviewArgsSchema = z.object({
url: z.string(),
}).passthrough();
const LinkPreviewResultSchema = z.object({
url: z.string().optional(),
title: z.string().optional(),
description: z.string().optional(),
image: z.string().optional(),
favicon: z.string().optional(),
error: z.string().optional(),
}).passthrough();
type LinkPreviewArgs = z.infer<typeof LinkPreviewArgsSchema>;
type LinkPreviewResult = z.infer<typeof LinkPreviewResultSchema>;
export const LinkPreviewToolUI = makeAssistantToolUI<LinkPreviewArgs, LinkPreviewResult>({
toolName: "link_preview",
render: ({ args, result, status }) => {
const isLoading = status.type === "running";
const url = result?.url ?? args?.url;
if (isLoading) {
return (
<div className="my-2 flex items-center gap-2 rounded-lg border bg-card/60 px-4 py-3">
<Loader2Icon className="size-4 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">Loading preview...</span>
</div>
);
}
if (result?.error || !url) return null;
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="my-2 flex items-start gap-3 rounded-lg border bg-card/60 p-3 hover:bg-card transition-colors no-underline"
>
{result?.favicon && (
<img src={result.favicon} alt="" className="size-4 mt-0.5 shrink-0" />
)}
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{result?.title ?? url}</p>
{result?.description && (
<p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">{result.description}</p>
)}
<p className="text-xs text-muted-foreground truncate mt-1">{url}</p>
</div>
<ExternalLinkIcon className="size-3.5 shrink-0 text-muted-foreground mt-0.5" />
</a>
);
},
});

View file

@ -0,0 +1,56 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { z } from "zod";
import { GlobeIcon, Loader2Icon, CheckCircle2Icon, XCircleIcon } from "lucide-react";
const ScrapeWebpageArgsSchema = z.object({
url: z.string(),
}).passthrough();
const ScrapeWebpageResultSchema = z.object({
url: z.string().optional(),
title: z.string().optional(),
content: z.string().optional(),
error: z.string().optional(),
success: z.boolean().optional(),
}).passthrough();
type ScrapeWebpageArgs = z.infer<typeof ScrapeWebpageArgsSchema>;
type ScrapeWebpageResult = z.infer<typeof ScrapeWebpageResultSchema>;
export const ScrapeWebpageToolUI = makeAssistantToolUI<ScrapeWebpageArgs, ScrapeWebpageResult>({
toolName: "scrape_webpage",
render: ({ args, result, status }) => {
const isLoading = status.type === "running";
const url = result?.url ?? args?.url;
const hasError = result?.error || result?.success === false;
const isSuccess = result?.success !== false && !result?.error && status.type === "complete";
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 shrink-0">
{isLoading ? (
<Loader2Icon className="size-4 animate-spin text-primary" />
) : hasError ? (
<XCircleIcon className="size-4 text-destructive" />
) : (
<GlobeIcon className="size-4 text-primary" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">
{isLoading ? "Scraping webpage..." : hasError ? "Failed to scrape" : "Scraped webpage"}
</p>
{url && (
<p className="text-xs text-muted-foreground truncate">{url}</p>
)}
{hasError && result?.error && (
<p className="text-xs text-destructive mt-0.5">{result.error}</p>
)}
</div>
{isSuccess && <CheckCircle2Icon className="size-4 text-green-500 shrink-0" />}
</div>
);
},
});

View file

@ -85,6 +85,57 @@ export const UpdateMemoryToolUI = ({
return null;
};
// ============================================================================
// Save Memory Tool UI (stub tool not yet in backend)
// ============================================================================
export const SaveMemoryToolUI = ({
status,
}: ToolCallMessagePartProps<{ content: string }, { status: string }>) => {
if (!status) return null;
const isRunning = status.type === "running" || status.type === "requires-action";
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">
{isRunning ? (
<Loader2Icon className="size-4 animate-spin text-primary" />
) : (
<BrainIcon className="size-4 text-primary" />
)}
</div>
<p className="text-sm font-medium">
{isRunning ? "Saving to memory..." : "Memory saved"}
</p>
{!isRunning && <CheckIcon className="ml-auto size-4 text-green-500" />}
</div>
);
};
// ============================================================================
// Recall Memory Tool UI (stub tool not yet in backend)
// ============================================================================
export const RecallMemoryToolUI = ({
status,
}: ToolCallMessagePartProps<{ query: string }, { memories: string[] }>) => {
if (!status) return null;
const isRunning = status.type === "running" || status.type === "requires-action";
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">
{isRunning ? (
<Loader2Icon className="size-4 animate-spin text-primary" />
) : (
<BrainIcon className="size-4 text-primary" />
)}
</div>
<p className="text-sm font-medium">
{isRunning ? "Recalling from memory..." : "Memory recalled"}
</p>
</div>
);
};
// ============================================================================
// Exports
// ============================================================================