mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-14 20:55:15 +02:00
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:
parent
67429c287f
commit
dc545f8028
10 changed files with 236 additions and 273 deletions
|
|
@ -3,10 +3,11 @@
|
|||
"configurations": [
|
||||
{
|
||||
"name": "SurfSense Frontend",
|
||||
"runtimeExecutable": "bun",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": ["dev"],
|
||||
"cwd": "surfsense_web",
|
||||
"port": 3000
|
||||
"port": 3999,
|
||||
"autoPort": false
|
||||
},
|
||||
{
|
||||
"name": "SurfSense Backend",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ from .crypto_realtime import (
|
|||
create_get_live_token_data_tool,
|
||||
create_get_live_token_price_tool,
|
||||
)
|
||||
from .display_image import create_display_image_tool
|
||||
from .generate_image import create_generate_image_tool
|
||||
from .knowledge_base import (
|
||||
CONNECTOR_DESCRIPTIONS,
|
||||
|
|
@ -57,7 +56,6 @@ __all__ = [
|
|||
"create_generate_video_presentation_tool",
|
||||
"create_get_live_token_data_tool",
|
||||
"create_get_live_token_price_tool",
|
||||
"create_link_preview_tool",
|
||||
"create_recall_memory_tool",
|
||||
"create_save_memory_tool",
|
||||
"create_scrape_webpage_tool",
|
||||
|
|
|
|||
|
|
@ -781,269 +781,6 @@ async def search_knowledge_base_raw_async(
|
|||
for docs in connector_results:
|
||||
all_documents.extend(docs)
|
||||
|
||||
elif connector == "TEAMS_CONNECTOR":
|
||||
_, chunks = await connector_service.search_teams(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "NOTION_CONNECTOR":
|
||||
_, chunks = await connector_service.search_notion(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "GITHUB_CONNECTOR":
|
||||
_, chunks = await connector_service.search_github(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "LINEAR_CONNECTOR":
|
||||
_, chunks = await connector_service.search_linear(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "TAVILY_API":
|
||||
_, chunks = await connector_service.search_tavily(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "SEARXNG_API":
|
||||
_, chunks = await connector_service.search_searxng(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "LINKUP_API":
|
||||
# Keep behavior aligned with researcher: default "standard"
|
||||
_, chunks = await connector_service.search_linkup(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
mode="standard",
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "BAIDU_SEARCH_API":
|
||||
_, chunks = await connector_service.search_baidu(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "DISCORD_CONNECTOR":
|
||||
_, chunks = await connector_service.search_discord(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "JIRA_CONNECTOR":
|
||||
_, chunks = await connector_service.search_jira(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "GOOGLE_CALENDAR_CONNECTOR":
|
||||
_, chunks = await connector_service.search_google_calendar(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "AIRTABLE_CONNECTOR":
|
||||
_, chunks = await connector_service.search_airtable(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "GOOGLE_GMAIL_CONNECTOR":
|
||||
_, chunks = await connector_service.search_google_gmail(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "GOOGLE_DRIVE_FILE":
|
||||
_, chunks = await connector_service.search_google_drive(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "CONFLUENCE_CONNECTOR":
|
||||
_, chunks = await connector_service.search_confluence(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "CLICKUP_CONNECTOR":
|
||||
_, chunks = await connector_service.search_clickup(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "LUMA_CONNECTOR":
|
||||
_, chunks = await connector_service.search_luma(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "ELASTICSEARCH_CONNECTOR":
|
||||
_, chunks = await connector_service.search_elasticsearch(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "NOTE":
|
||||
_, chunks = await connector_service.search_notes(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "BOOKSTACK_CONNECTOR":
|
||||
_, chunks = await connector_service.search_bookstack(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "CIRCLEBACK":
|
||||
_, chunks = await connector_service.search_circleback(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "OBSIDIAN_CONNECTOR":
|
||||
_, chunks = await connector_service.search_obsidian(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "DEXSCREENER_CONNECTOR":
|
||||
_, chunks = await connector_service.search_dexscreener(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
print(f"[DEBUG] DexScreener search returned {len(chunks)} chunks")
|
||||
if chunks:
|
||||
print(f"[DEBUG] First chunk metadata: {chunks[0].get('document', {}).get('metadata', {})}")
|
||||
all_documents.extend(chunks)
|
||||
|
||||
# =========================================================
|
||||
# Composio Connectors
|
||||
# =========================================================
|
||||
elif connector == "COMPOSIO_GOOGLE_DRIVE_CONNECTOR":
|
||||
_, chunks = await connector_service.search_composio_google_drive(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "COMPOSIO_GMAIL_CONNECTOR":
|
||||
_, chunks = await connector_service.search_composio_gmail(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
elif connector == "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR":
|
||||
_, chunks = await connector_service.search_composio_google_calendar(
|
||||
user_query=query,
|
||||
search_space_id=search_space_id,
|
||||
top_k=top_k,
|
||||
start_date=resolved_start_date,
|
||||
end_date=resolved_end_date,
|
||||
)
|
||||
all_documents.extend(chunks)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error searching connector {connector}: {e}")
|
||||
continue
|
||||
|
||||
# Deduplicate by content hash
|
||||
seen_doc_ids: set[Any] = set()
|
||||
seen_content_hashes: set[int] = set()
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ from .crypto_realtime import (
|
|||
create_get_live_token_data_tool,
|
||||
create_get_live_token_price_tool,
|
||||
)
|
||||
from .display_image import create_display_image_tool
|
||||
from .dropbox import (
|
||||
create_create_dropbox_file_tool,
|
||||
create_delete_dropbox_file_tool,
|
||||
|
|
@ -85,8 +84,6 @@ from .linear import (
|
|||
create_delete_linear_issue_tool,
|
||||
create_update_linear_issue_tool,
|
||||
)
|
||||
from .knowledge_base import create_search_knowledge_base_tool
|
||||
from .link_preview import create_link_preview_tool
|
||||
from .mcp_tool import load_mcp_tools
|
||||
from .notion import (
|
||||
create_create_notion_page_tool,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ from notion_client.errors import APIResponseError
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
|
||||
class NotionAPIError(Exception):
|
||||
"""Raised when Notion API returns an error response."""
|
||||
|
||||
from app.config import config
|
||||
from app.db import SearchSourceConnector
|
||||
from app.schemas.notion_auth_credentials import NotionAuthCredentialsBase
|
||||
|
|
|
|||
54
surfsense_web/components/tool-ui/display-image.tsx
Normal file
54
surfsense_web/components/tool-ui/display-image.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
61
surfsense_web/components/tool-ui/link-preview.tsx
Normal file
61
surfsense_web/components/tool-ui/link-preview.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
||||
56
surfsense_web/components/tool-ui/scrape-webpage.tsx
Normal file
56
surfsense_web/components/tool-ui/scrape-webpage.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue