mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-04 22:02:16 +02:00
feat: add public report content retrieval and enhance report handling
This commit is contained in:
parent
59628fdf76
commit
e5626342fc
5 changed files with 192 additions and 43 deletions
|
|
@ -20,6 +20,7 @@ from app.services.public_chat_service import (
|
||||||
clone_from_snapshot,
|
clone_from_snapshot,
|
||||||
get_public_chat,
|
get_public_chat,
|
||||||
get_snapshot_podcast,
|
get_snapshot_podcast,
|
||||||
|
get_snapshot_report,
|
||||||
)
|
)
|
||||||
from app.users import current_active_user
|
from app.users import current_active_user
|
||||||
|
|
||||||
|
|
@ -114,3 +115,28 @@ async def stream_public_podcast(
|
||||||
"Content-Disposition": f"inline; filename={os.path.basename(file_path)}",
|
"Content-Disposition": f"inline; filename={os.path.basename(file_path)}",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{share_token}/reports/{report_id}/content")
|
||||||
|
async def get_public_report_content(
|
||||||
|
share_token: str,
|
||||||
|
report_id: int,
|
||||||
|
session: AsyncSession = Depends(get_async_session),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get report content from a public chat snapshot.
|
||||||
|
|
||||||
|
No authentication required - the share_token provides access.
|
||||||
|
Returns report content including title, markdown body, and metadata.
|
||||||
|
"""
|
||||||
|
report_info = await get_snapshot_report(session, share_token, report_id)
|
||||||
|
|
||||||
|
if not report_info:
|
||||||
|
raise HTTPException(status_code=404, detail="Report not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": report_info.get("original_id"),
|
||||||
|
"title": report_info.get("title"),
|
||||||
|
"content": report_info.get("content"),
|
||||||
|
"report_metadata": report_info.get("report_metadata"),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ from app.db import (
|
||||||
Podcast,
|
Podcast,
|
||||||
PodcastStatus,
|
PodcastStatus,
|
||||||
PublicChatSnapshot,
|
PublicChatSnapshot,
|
||||||
|
Report,
|
||||||
SearchSpaceMembership,
|
SearchSpaceMembership,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
|
@ -38,6 +39,7 @@ UI_TOOLS = {
|
||||||
"display_image",
|
"display_image",
|
||||||
"link_preview",
|
"link_preview",
|
||||||
"generate_podcast",
|
"generate_podcast",
|
||||||
|
"generate_report",
|
||||||
"scrape_webpage",
|
"scrape_webpage",
|
||||||
"multi_link_preview",
|
"multi_link_preview",
|
||||||
}
|
}
|
||||||
|
|
@ -195,19 +197,22 @@ async def create_snapshot(
|
||||||
message_ids = []
|
message_ids = []
|
||||||
podcasts_data = []
|
podcasts_data = []
|
||||||
podcast_ids_seen: set[int] = set()
|
podcast_ids_seen: set[int] = set()
|
||||||
|
reports_data = []
|
||||||
|
report_ids_seen: set[int] = set()
|
||||||
|
|
||||||
for msg in sorted(thread.messages, key=lambda m: m.created_at):
|
for msg in sorted(thread.messages, key=lambda m: m.created_at):
|
||||||
author = await get_author_display(session, msg.author_id, user_cache)
|
author = await get_author_display(session, msg.author_id, user_cache)
|
||||||
sanitized_content = sanitize_content_for_public(msg.content)
|
sanitized_content = sanitize_content_for_public(msg.content)
|
||||||
|
|
||||||
# Extract podcast references and update status to "ready" for completed podcasts
|
# Extract podcast/report references and update status to "ready" for completed ones
|
||||||
if isinstance(sanitized_content, list):
|
if isinstance(sanitized_content, list):
|
||||||
for part in sanitized_content:
|
for part in sanitized_content:
|
||||||
if (
|
if not isinstance(part, dict) or part.get("type") != "tool-call":
|
||||||
isinstance(part, dict)
|
continue
|
||||||
and part.get("type") == "tool-call"
|
|
||||||
and part.get("toolName") == "generate_podcast"
|
tool_name = part.get("toolName")
|
||||||
):
|
|
||||||
|
if tool_name == "generate_podcast":
|
||||||
result_data = part.get("result", {})
|
result_data = part.get("result", {})
|
||||||
podcast_id = result_data.get("podcast_id")
|
podcast_id = result_data.get("podcast_id")
|
||||||
if podcast_id and podcast_id not in podcast_ids_seen:
|
if podcast_id and podcast_id not in podcast_ids_seen:
|
||||||
|
|
@ -220,6 +225,19 @@ async def create_snapshot(
|
||||||
# Update status to "ready" so frontend renders PodcastPlayer
|
# Update status to "ready" so frontend renders PodcastPlayer
|
||||||
part["result"] = {**result_data, "status": "ready"}
|
part["result"] = {**result_data, "status": "ready"}
|
||||||
|
|
||||||
|
elif tool_name == "generate_report":
|
||||||
|
result_data = part.get("result", {})
|
||||||
|
report_id = result_data.get("report_id")
|
||||||
|
if report_id and report_id not in report_ids_seen:
|
||||||
|
report_info = await _get_report_for_snapshot(
|
||||||
|
session, report_id
|
||||||
|
)
|
||||||
|
if report_info:
|
||||||
|
reports_data.append(report_info)
|
||||||
|
report_ids_seen.add(report_id)
|
||||||
|
# Update status to "ready" so frontend renders ReportCard
|
||||||
|
part["result"] = {**result_data, "status": "ready"}
|
||||||
|
|
||||||
messages_data.append(
|
messages_data.append(
|
||||||
{
|
{
|
||||||
"id": msg.id,
|
"id": msg.id,
|
||||||
|
|
@ -266,6 +284,7 @@ async def create_snapshot(
|
||||||
"author": thread_author,
|
"author": thread_author,
|
||||||
"messages": messages_data,
|
"messages": messages_data,
|
||||||
"podcasts": podcasts_data,
|
"podcasts": podcasts_data,
|
||||||
|
"reports": reports_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create new snapshot
|
# Create new snapshot
|
||||||
|
|
@ -309,6 +328,25 @@ async def _get_podcast_for_snapshot(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_report_for_snapshot(
|
||||||
|
session: AsyncSession,
|
||||||
|
report_id: int,
|
||||||
|
) -> dict | None:
|
||||||
|
"""Get report info for embedding in snapshot_data."""
|
||||||
|
result = await session.execute(select(Report).filter(Report.id == report_id))
|
||||||
|
report = result.scalars().first()
|
||||||
|
|
||||||
|
if not report:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"original_id": report.id,
|
||||||
|
"title": report.title,
|
||||||
|
"content": report.content,
|
||||||
|
"report_metadata": report.report_metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Snapshot Retrieval
|
# Snapshot Retrieval
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -578,6 +616,7 @@ async def clone_from_snapshot(
|
||||||
data = snapshot.snapshot_data
|
data = snapshot.snapshot_data
|
||||||
messages_data = data.get("messages", [])
|
messages_data = data.get("messages", [])
|
||||||
podcasts_lookup = {p.get("original_id"): p for p in data.get("podcasts", [])}
|
podcasts_lookup = {p.get("original_id"): p for p in data.get("podcasts", [])}
|
||||||
|
reports_lookup = {r.get("original_id"): r for r in data.get("reports", [])}
|
||||||
|
|
||||||
new_thread = NewChatThread(
|
new_thread = NewChatThread(
|
||||||
title=data.get("title", "Cloned Chat"),
|
title=data.get("title", "Cloned Chat"),
|
||||||
|
|
@ -594,6 +633,7 @@ async def clone_from_snapshot(
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
podcast_id_mapping: dict[int, int] = {}
|
podcast_id_mapping: dict[int, int] = {}
|
||||||
|
report_id_mapping: dict[int, int] = {}
|
||||||
|
|
||||||
# Check which authors from snapshot still exist in DB
|
# Check which authors from snapshot still exist in DB
|
||||||
author_ids_from_snapshot: set[UUID] = set()
|
author_ids_from_snapshot: set[UUID] = set()
|
||||||
|
|
@ -655,6 +695,34 @@ async def clone_from_snapshot(
|
||||||
"podcast_id": podcast_id_mapping[old_podcast_id],
|
"podcast_id": podcast_id_mapping[old_podcast_id],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(part, dict)
|
||||||
|
and part.get("type") == "tool-call"
|
||||||
|
and part.get("toolName") == "generate_report"
|
||||||
|
):
|
||||||
|
result = part.get("result", {})
|
||||||
|
old_report_id = result.get("report_id")
|
||||||
|
|
||||||
|
if old_report_id and old_report_id not in report_id_mapping:
|
||||||
|
report_info = reports_lookup.get(old_report_id)
|
||||||
|
if report_info:
|
||||||
|
new_report = Report(
|
||||||
|
title=report_info.get("title", "Cloned Report"),
|
||||||
|
content=report_info.get("content"),
|
||||||
|
report_metadata=report_info.get("report_metadata"),
|
||||||
|
search_space_id=target_search_space_id,
|
||||||
|
thread_id=new_thread.id,
|
||||||
|
)
|
||||||
|
session.add(new_report)
|
||||||
|
await session.flush()
|
||||||
|
report_id_mapping[old_report_id] = new_report.id
|
||||||
|
|
||||||
|
if old_report_id and old_report_id in report_id_mapping:
|
||||||
|
part["result"] = {
|
||||||
|
**result,
|
||||||
|
"report_id": report_id_mapping[old_report_id],
|
||||||
|
}
|
||||||
|
|
||||||
new_message = NewChatMessage(
|
new_message = NewChatMessage(
|
||||||
thread_id=new_thread.id,
|
thread_id=new_thread.id,
|
||||||
role=role,
|
role=role,
|
||||||
|
|
@ -696,3 +764,29 @@ async def get_snapshot_podcast(
|
||||||
return podcast
|
return podcast
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_snapshot_report(
|
||||||
|
session: AsyncSession,
|
||||||
|
share_token: str,
|
||||||
|
report_id: int,
|
||||||
|
) -> dict | None:
|
||||||
|
"""
|
||||||
|
Get report info from a snapshot by original report ID.
|
||||||
|
|
||||||
|
Used for displaying report content in public view.
|
||||||
|
Looks up the report by its original_id in the snapshot's reports array.
|
||||||
|
"""
|
||||||
|
snapshot = await get_snapshot_by_token(session, share_token)
|
||||||
|
|
||||||
|
if not snapshot:
|
||||||
|
return None
|
||||||
|
|
||||||
|
reports = snapshot.snapshot_data.get("reports", [])
|
||||||
|
|
||||||
|
# Find report by original_id
|
||||||
|
for report in reports:
|
||||||
|
if report.get("original_id") == report_id:
|
||||||
|
return report
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ interface ReportPanelState {
|
||||||
reportId: number | null;
|
reportId: number | null;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
wordCount: number | null;
|
wordCount: number | null;
|
||||||
|
/** When set, uses public endpoints for fetching report data (public shared chat) */
|
||||||
|
shareToken: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ReportPanelState = {
|
const initialState: ReportPanelState = {
|
||||||
|
|
@ -12,6 +14,7 @@ const initialState: ReportPanelState = {
|
||||||
reportId: null,
|
reportId: null,
|
||||||
title: null,
|
title: null,
|
||||||
wordCount: null,
|
wordCount: null,
|
||||||
|
shareToken: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Core atom holding the report panel state */
|
/** Core atom holding the report panel state */
|
||||||
|
|
@ -30,13 +33,15 @@ export const openReportPanelAtom = atom(
|
||||||
reportId,
|
reportId,
|
||||||
title,
|
title,
|
||||||
wordCount,
|
wordCount,
|
||||||
}: { reportId: number; title: string; wordCount?: number }
|
shareToken,
|
||||||
|
}: { reportId: number; title: string; wordCount?: number; shareToken?: string | null }
|
||||||
) => {
|
) => {
|
||||||
set(reportPanelAtom, {
|
set(reportPanelAtom, {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
reportId,
|
reportId,
|
||||||
title,
|
title,
|
||||||
wordCount: wordCount ?? null,
|
wordCount: wordCount ?? null,
|
||||||
|
shareToken: shareToken ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -101,12 +101,15 @@ function ReportPanelContent({
|
||||||
title,
|
title,
|
||||||
onClose,
|
onClose,
|
||||||
insideDrawer = false,
|
insideDrawer = false,
|
||||||
|
shareToken,
|
||||||
}: {
|
}: {
|
||||||
reportId: number;
|
reportId: number;
|
||||||
title: string;
|
title: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
/** When true, adjusts dropdown behavior to work inside a Vaul drawer on mobile */
|
/** When true, adjusts dropdown behavior to work inside a Vaul drawer on mobile */
|
||||||
insideDrawer?: boolean;
|
insideDrawer?: boolean;
|
||||||
|
/** When set, uses public endpoint for fetching report data (public shared chat) */
|
||||||
|
shareToken?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [reportContent, setReportContent] =
|
const [reportContent, setReportContent] =
|
||||||
useState<ReportContentResponse | null>(null);
|
useState<ReportContentResponse | null>(null);
|
||||||
|
|
@ -122,9 +125,10 @@ function ReportPanelContent({
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const rawData = await baseApiService.get<unknown>(
|
const url = shareToken
|
||||||
`/api/v1/reports/${reportId}/content`
|
? `/api/v1/public/${shareToken}/reports/${reportId}/content`
|
||||||
);
|
: `/api/v1/reports/${reportId}/content`;
|
||||||
|
const rawData = await baseApiService.get<unknown>(url);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const parsed = ReportContentResponseSchema.safeParse(rawData);
|
const parsed = ReportContentResponseSchema.safeParse(rawData);
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
|
|
@ -159,7 +163,7 @@ function ReportPanelContent({
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [reportId]);
|
}, [reportId, shareToken]);
|
||||||
|
|
||||||
// Copy markdown content
|
// Copy markdown content
|
||||||
const handleCopy = useCallback(async () => {
|
const handleCopy = useCallback(async () => {
|
||||||
|
|
@ -274,34 +278,40 @@ function ReportPanelContent({
|
||||||
<span className="sr-only">Download options</span>
|
<span className="sr-only">Download options</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className={`min-w-[180px]${insideDrawer ? " z-[100]" : ""}`}>
|
<DropdownMenuContent align="start" className={`min-w-[180px]${insideDrawer ? " z-[100]" : ""}`}>
|
||||||
<DropdownMenuItem onClick={() => handleExport("md")}>
|
<DropdownMenuItem onClick={() => handleExport("md")}>
|
||||||
<DownloadIcon className="size-4" />
|
<DownloadIcon className="size-4" />
|
||||||
Download Markdown
|
Download Markdown
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
{/* PDF/DOCX export requires server-side conversion via authenticated endpoint.
|
||||||
onClick={() => handleExport("pdf")}
|
Hide for public viewers who have no auth token. */}
|
||||||
disabled={exporting !== null}
|
{!shareToken && (
|
||||||
>
|
<>
|
||||||
{exporting === "pdf" ? (
|
<DropdownMenuItem
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
onClick={() => handleExport("pdf")}
|
||||||
) : (
|
disabled={exporting !== null}
|
||||||
<DownloadIcon className="size-4" />
|
>
|
||||||
)}
|
{exporting === "pdf" ? (
|
||||||
Download PDF
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
</DropdownMenuItem>
|
) : (
|
||||||
<DropdownMenuItem
|
<DownloadIcon className="size-4" />
|
||||||
onClick={() => handleExport("docx")}
|
)}
|
||||||
disabled={exporting !== null}
|
Download PDF
|
||||||
>
|
</DropdownMenuItem>
|
||||||
{exporting === "docx" ? (
|
<DropdownMenuItem
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
onClick={() => handleExport("docx")}
|
||||||
) : (
|
disabled={exporting !== null}
|
||||||
<DownloadIcon className="size-4" />
|
>
|
||||||
)}
|
{exporting === "docx" ? (
|
||||||
Download DOCX
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
</DropdownMenuItem>
|
) : (
|
||||||
</DropdownMenuContent>
|
<DownloadIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
Download DOCX
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
|
|
@ -363,6 +373,7 @@ function DesktopReportPanel() {
|
||||||
reportId={panelState.reportId}
|
reportId={panelState.reportId}
|
||||||
title={panelState.title || "Report"}
|
title={panelState.title || "Report"}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
|
shareToken={panelState.shareToken}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -395,6 +406,7 @@ function MobileReportDrawer() {
|
||||||
reportId={panelState.reportId}
|
reportId={panelState.reportId}
|
||||||
title={panelState.title || "Report"}
|
title={panelState.title || "Report"}
|
||||||
insideDrawer
|
insideDrawer
|
||||||
|
shareToken={panelState.shareToken}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { FileTextIcon } from "lucide-react";
|
import { FileTextIcon } from "lucide-react";
|
||||||
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
|
|
@ -103,10 +104,13 @@ function ReportCard({
|
||||||
reportId,
|
reportId,
|
||||||
title,
|
title,
|
||||||
wordCount,
|
wordCount,
|
||||||
|
shareToken,
|
||||||
}: {
|
}: {
|
||||||
reportId: number;
|
reportId: number;
|
||||||
title: string;
|
title: string;
|
||||||
wordCount?: number;
|
wordCount?: number;
|
||||||
|
/** When set, uses public endpoint for fetching report data */
|
||||||
|
shareToken?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const openPanel = useSetAtom(openReportPanelAtom);
|
const openPanel = useSetAtom(openReportPanelAtom);
|
||||||
const panelState = useAtomValue(reportPanelAtom);
|
const panelState = useAtomValue(reportPanelAtom);
|
||||||
|
|
@ -124,9 +128,10 @@ function ReportCard({
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const rawData = await baseApiService.get<unknown>(
|
const url = shareToken
|
||||||
`/api/v1/reports/${reportId}/content`
|
? `/api/v1/public/${shareToken}/reports/${reportId}/content`
|
||||||
);
|
: `/api/v1/reports/${reportId}/content`;
|
||||||
|
const rawData = await baseApiService.get<unknown>(url);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const parsed = ReportMetadataResponseSchema.safeParse(rawData);
|
const parsed = ReportMetadataResponseSchema.safeParse(rawData);
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
|
|
@ -154,7 +159,7 @@ function ReportCard({
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [reportId, title, wordCount]);
|
}, [reportId, title, wordCount, shareToken]);
|
||||||
|
|
||||||
// Show non-clickable error card for any error (failed status, not found, etc.)
|
// Show non-clickable error card for any error (failed status, not found, etc.)
|
||||||
if (!isLoading && error) {
|
if (!isLoading && error) {
|
||||||
|
|
@ -168,6 +173,7 @@ function ReportCard({
|
||||||
reportId,
|
reportId,
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
wordCount: metadata.wordCount ?? undefined,
|
wordCount: metadata.wordCount ?? undefined,
|
||||||
|
shareToken,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -218,6 +224,11 @@ export const GenerateReportToolUI = makeAssistantToolUI<
|
||||||
>({
|
>({
|
||||||
toolName: "generate_report",
|
toolName: "generate_report",
|
||||||
render: function GenerateReportUI({ args, result, status }) {
|
render: function GenerateReportUI({ args, result, status }) {
|
||||||
|
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";
|
||||||
|
|
||||||
// Loading state - tool is still running (LLM generating report)
|
// Loading state - tool is still running (LLM generating report)
|
||||||
|
|
@ -264,6 +275,7 @@ export const GenerateReportToolUI = makeAssistantToolUI<
|
||||||
reportId={result.report_id}
|
reportId={result.report_id}
|
||||||
title={result.title || topic}
|
title={result.title || topic}
|
||||||
wordCount={result.word_count ?? undefined}
|
wordCount={result.word_count ?? undefined}
|
||||||
|
shareToken={shareToken}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue