mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-04 22:02:16 +02:00
chore: ran linting
This commit is contained in:
parent
207b9e0ed3
commit
a2dd5fb671
14 changed files with 124 additions and 162 deletions
|
|
@ -67,4 +67,3 @@ def downgrade() -> None:
|
||||||
END $$;
|
END $$;
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,4 +64,3 @@ def downgrade() -> None:
|
||||||
op.execute("DROP INDEX IF EXISTS ix_reports_thread_id")
|
op.execute("DROP INDEX IF EXISTS ix_reports_thread_id")
|
||||||
op.execute("DROP INDEX IF EXISTS ix_reports_search_space_id")
|
op.execute("DROP INDEX IF EXISTS ix_reports_search_space_id")
|
||||||
op.execute("DROP TABLE IF EXISTS reports")
|
op.execute("DROP TABLE IF EXISTS reports")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,14 +212,18 @@ def create_generate_report_tool(
|
||||||
)
|
)
|
||||||
return failed_report.id
|
return failed_report.id
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("[generate_report] Could not persist failed report row")
|
logger.exception(
|
||||||
|
"[generate_report] Could not persist failed report row"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get the LLM instance for this search space
|
# Get the LLM instance for this search space
|
||||||
llm = await get_document_summary_llm(db_session, search_space_id)
|
llm = await get_document_summary_llm(db_session, search_space_id)
|
||||||
if not llm:
|
if not llm:
|
||||||
error_msg = "No LLM configured. Please configure a language model in Settings."
|
error_msg = (
|
||||||
|
"No LLM configured. Please configure a language model in Settings."
|
||||||
|
)
|
||||||
report_id = await _save_failed_report(error_msg)
|
report_id = await _save_failed_report(error_msg)
|
||||||
return {
|
return {
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
|
|
|
||||||
|
|
@ -1039,7 +1039,9 @@ class Report(BaseModel, TimestampMixin):
|
||||||
title = Column(String(500), nullable=False)
|
title = Column(String(500), nullable=False)
|
||||||
content = Column(Text, nullable=True) # Markdown body
|
content = Column(Text, nullable=True) # Markdown body
|
||||||
report_metadata = Column(JSONB, nullable=True) # section headings, word count, etc.
|
report_metadata = Column(JSONB, nullable=True) # section headings, word count, etc.
|
||||||
report_style = Column(String(100), nullable=True) # e.g. "executive_summary", "deep_research"
|
report_style = Column(
|
||||||
|
String(100), nullable=True
|
||||||
|
) # e.g. "executive_summary", "deep_research"
|
||||||
|
|
||||||
search_space_id = Column(
|
search_space_id = Column(
|
||||||
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
|
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ from .notes_routes import router as notes_router
|
||||||
from .notifications_routes import router as notifications_router
|
from .notifications_routes import router as notifications_router
|
||||||
from .notion_add_connector_route import router as notion_add_connector_router
|
from .notion_add_connector_route import router as notion_add_connector_router
|
||||||
from .podcasts_routes import router as podcasts_router
|
from .podcasts_routes import router as podcasts_router
|
||||||
from .reports_routes import router as reports_router
|
|
||||||
from .public_chat_routes import router as public_chat_router
|
from .public_chat_routes import router as public_chat_router
|
||||||
from .rbac_routes import router as rbac_router
|
from .rbac_routes import router as rbac_router
|
||||||
|
from .reports_routes import router as reports_router
|
||||||
from .search_source_connectors_routes import router as search_source_connectors_router
|
from .search_source_connectors_routes import router as search_source_connectors_router
|
||||||
from .search_spaces_routes import router as search_spaces_router
|
from .search_spaces_routes import router as search_spaces_router
|
||||||
from .slack_add_connector_route import router as slack_add_connector_router
|
from .slack_add_connector_route import router as slack_add_connector_router
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ class ExportFormat(str, Enum):
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
async def _get_report_with_access(
|
async def _get_report_with_access(
|
||||||
report_id: int,
|
report_id: int,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|
@ -66,7 +67,7 @@ async def _get_report_with_access(
|
||||||
if not report:
|
if not report:
|
||||||
raise HTTPException(status_code=404, detail="Report not found")
|
raise HTTPException(status_code=404, detail="Report not found")
|
||||||
|
|
||||||
# Lightweight membership check – no granular RBAC, just "is the user a
|
# Lightweight membership check - no granular RBAC, just "is the user a
|
||||||
# member of the search space this report belongs to?"
|
# member of the search space this report belongs to?"
|
||||||
await check_search_space_access(session, user, report.search_space_id)
|
await check_search_space_access(session, user, report.search_space_id)
|
||||||
|
|
||||||
|
|
@ -191,7 +192,9 @@ async def read_report_content(
|
||||||
@router.get("/reports/{report_id}/export")
|
@router.get("/reports/{report_id}/export")
|
||||||
async def export_report(
|
async def export_report(
|
||||||
report_id: int,
|
report_id: int,
|
||||||
format: ExportFormat = Query(ExportFormat.PDF, description="Export format: pdf or docx"),
|
format: ExportFormat = Query(
|
||||||
|
ExportFormat.PDF, description="Export format: pdf or docx"
|
||||||
|
),
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user),
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
|
|
@ -243,8 +246,9 @@ async def export_report(
|
||||||
|
|
||||||
# Sanitize filename
|
# Sanitize filename
|
||||||
safe_title = (
|
safe_title = (
|
||||||
"".join(c if c.isalnum() or c in " -_" else "_" for c in report.title)
|
"".join(
|
||||||
.strip()[:80]
|
c if c.isalnum() or c in " -_" else "_" for c in report.title
|
||||||
|
).strip()[:80]
|
||||||
or "report"
|
or "report"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -265,9 +269,7 @@ async def export_report(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Report export failed")
|
logger.exception("Report export failed")
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=500, detail=f"Export failed: {e!s}") from e
|
||||||
status_code=500, detail=f"Export failed: {e!s}"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/reports/{report_id}", response_model=dict)
|
@router.delete("/reports/{report_id}", response_model=dict)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ from .new_llm_config import (
|
||||||
NewLLMConfigUpdate,
|
NewLLMConfigUpdate,
|
||||||
)
|
)
|
||||||
from .podcasts import PodcastBase, PodcastCreate, PodcastRead, PodcastUpdate
|
from .podcasts import PodcastBase, PodcastCreate, PodcastRead, PodcastUpdate
|
||||||
from .reports import ReportBase, ReportContentRead, ReportRead, ReportVersionInfo
|
|
||||||
from .rbac_schemas import (
|
from .rbac_schemas import (
|
||||||
InviteAcceptRequest,
|
InviteAcceptRequest,
|
||||||
InviteAcceptResponse,
|
InviteAcceptResponse,
|
||||||
|
|
@ -77,6 +76,7 @@ from .rbac_schemas import (
|
||||||
RoleUpdate,
|
RoleUpdate,
|
||||||
UserSearchSpaceAccess,
|
UserSearchSpaceAccess,
|
||||||
)
|
)
|
||||||
|
from .reports import ReportBase, ReportContentRead, ReportRead, ReportVersionInfo
|
||||||
from .search_source_connector import (
|
from .search_source_connector import (
|
||||||
MCPConnectorCreate,
|
MCPConnectorCreate,
|
||||||
MCPConnectorRead,
|
MCPConnectorRead,
|
||||||
|
|
|
||||||
|
|
@ -229,9 +229,7 @@ async def create_snapshot(
|
||||||
result_data = part.get("result", {})
|
result_data = part.get("result", {})
|
||||||
report_id = result_data.get("report_id")
|
report_id = result_data.get("report_id")
|
||||||
if report_id and report_id not in report_ids_seen:
|
if report_id and report_id not in report_ids_seen:
|
||||||
report_info = await _get_report_for_snapshot(
|
report_info = await _get_report_for_snapshot(session, report_id)
|
||||||
session, report_id
|
|
||||||
)
|
|
||||||
if report_info:
|
if report_info:
|
||||||
reports_data.append(report_info)
|
reports_data.append(report_info)
|
||||||
report_ids_seen.add(report_id)
|
report_ids_seen.add(report_id)
|
||||||
|
|
@ -816,9 +814,7 @@ async def get_snapshot_report_versions(
|
||||||
return []
|
return []
|
||||||
|
|
||||||
reports = snapshot.snapshot_data.get("reports", [])
|
reports = snapshot.snapshot_data.get("reports", [])
|
||||||
siblings = [
|
siblings = [r for r in reports if r.get("report_group_id") == report_group_id]
|
||||||
r for r in reports if r.get("report_group_id") == report_group_id
|
|
||||||
]
|
|
||||||
|
|
||||||
# Sort by original_id (ascending = insertion order ≈ created_at order)
|
# Sort by original_id (ascending = insertion order ≈ created_at order)
|
||||||
siblings.sort(key=lambda r: r.get("original_id", 0))
|
siblings.sort(key=lambda r: r.get("original_id", 0))
|
||||||
|
|
|
||||||
|
|
@ -50,4 +50,3 @@ export const openReportPanelAtom = atom(
|
||||||
export const closeReportPanelAtom = atom(null, (_, set) => {
|
export const closeReportPanelAtom = atom(null, (_, set) => {
|
||||||
set(reportPanelAtom, initialState);
|
set(reportPanelAtom, initialState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import "katex/dist/katex.min.css";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const code = createCodePlugin({
|
const code = createCodePlugin({
|
||||||
themes: ["nord", "nord"]
|
themes: ["nord", "nord"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const math = createMathPlugin({
|
const math = createMathPlugin({
|
||||||
|
|
@ -24,9 +24,7 @@ interface MarkdownViewerProps {
|
||||||
*/
|
*/
|
||||||
function stripOuterMarkdownFence(content: string): string {
|
function stripOuterMarkdownFence(content: string): string {
|
||||||
const trimmed = content.trim();
|
const trimmed = content.trim();
|
||||||
const match = trimmed.match(
|
const match = trimmed.match(/^```(?:markdown|md)?\s*\n([\s\S]+?)\n```\s*$/);
|
||||||
/^```(?:markdown|md)?\s*\n([\s\S]+?)\n```\s*$/
|
|
||||||
);
|
|
||||||
return match ? match[1] : content;
|
return match ? match[1] : content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,8 +119,18 @@ export function MarkdownViewer({ content, className }: MarkdownViewerProps) {
|
||||||
<table className="w-full divide-y divide-border" {...props} />
|
<table className="w-full divide-y divide-border" {...props} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
th: ({ ...props }) => <th className="px-4 py-2.5 text-left text-sm font-semibold text-muted-foreground/80 bg-muted/30 border-r border-border/40 last:border-r-0" {...props} />,
|
th: ({ ...props }) => (
|
||||||
td: ({ ...props }) => <td className="px-4 py-2.5 text-sm border-t border-r border-border/40 last:border-r-0" {...props} />,
|
<th
|
||||||
|
className="px-4 py-2.5 text-left text-sm font-semibold text-muted-foreground/80 bg-muted/30 border-r border-border/40 last:border-r-0"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
td: ({ ...props }) => (
|
||||||
|
<td
|
||||||
|
className="px-4 py-2.5 text-sm border-t border-r border-border/40 last:border-r-0"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import {
|
import { ChevronDownIcon, XIcon } from "lucide-react";
|
||||||
ChevronDownIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||||
closeReportPanelAtom,
|
|
||||||
reportPanelAtom,
|
|
||||||
} from "@/atoms/chat/report-panel.atom";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import {
|
import { Drawer, DrawerContent, DrawerHandle } from "@/components/ui/drawer";
|
||||||
Drawer,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHandle,
|
|
||||||
} from "@/components/ui/drawer";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -118,8 +108,7 @@ function ReportPanelContent({
|
||||||
/** When set, uses public endpoint for fetching report data (public shared chat) */
|
/** When set, uses public endpoint for fetching report data (public shared chat) */
|
||||||
shareToken?: string | null;
|
shareToken?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [reportContent, setReportContent] =
|
const [reportContent, setReportContent] = useState<ReportContentResponse | null>(null);
|
||||||
useState<ReportContentResponse | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
@ -150,10 +139,7 @@ function ReportPanelContent({
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
// Check if the report was marked as failed in metadata
|
// Check if the report was marked as failed in metadata
|
||||||
if (parsed.data.report_metadata?.status === "failed") {
|
if (parsed.data.report_metadata?.status === "failed") {
|
||||||
setError(
|
setError(parsed.data.report_metadata?.error_message || "Report generation failed");
|
||||||
parsed.data.report_metadata?.error_message ||
|
|
||||||
"Report generation failed"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setReportContent(parsed.data);
|
setReportContent(parsed.data);
|
||||||
// Update versions from the response
|
// Update versions from the response
|
||||||
|
|
@ -162,18 +148,13 @@ function ReportPanelContent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn("Invalid report content response:", parsed.error.issues);
|
||||||
"Invalid report content response:",
|
|
||||||
parsed.error.issues
|
|
||||||
);
|
|
||||||
setError("Invalid response format");
|
setError("Invalid response format");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
console.error("Error fetching report content:", err);
|
console.error("Error fetching report content:", err);
|
||||||
setError(
|
setError(err instanceof Error ? err.message : "Failed to load report");
|
||||||
err instanceof Error ? err.message : "Failed to load report"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setIsLoading(false);
|
if (!cancelled) setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -202,8 +183,10 @@ function ReportPanelContent({
|
||||||
async (format: "pdf" | "docx" | "md") => {
|
async (format: "pdf" | "docx" | "md") => {
|
||||||
setExporting(format);
|
setExporting(format);
|
||||||
const safeTitle =
|
const safeTitle =
|
||||||
title.replace(/[^a-zA-Z0-9 _-]/g, "_").trim().slice(0, 80) ||
|
title
|
||||||
"report";
|
.replace(/[^a-zA-Z0-9 _-]/g, "_")
|
||||||
|
.trim()
|
||||||
|
.slice(0, 80) || "report";
|
||||||
try {
|
try {
|
||||||
if (format === "md") {
|
if (format === "md") {
|
||||||
// Download markdown content directly as a .md file
|
// Download markdown content directly as a .md file
|
||||||
|
|
@ -248,7 +231,6 @@ function ReportPanelContent({
|
||||||
[activeReportId, title, reportContent?.content]
|
[activeReportId, title, reportContent?.content]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Show full-page skeleton only on initial load (no data loaded yet).
|
// Show full-page skeleton only on initial load (no data loaded yet).
|
||||||
// Once we have versions/content from a prior fetch, keep the action bar visible.
|
// Once we have versions/content from a prior fetch, keep the action bar visible.
|
||||||
const hasLoadedBefore = versions.length > 0 || reportContent !== null;
|
const hasLoadedBefore = versions.length > 0 || reportContent !== null;
|
||||||
|
|
@ -259,12 +241,7 @@ function ReportPanelContent({
|
||||||
{/* Minimal top bar with close button even during initial load */}
|
{/* Minimal top bar with close button even during initial load */}
|
||||||
<div className="flex items-center justify-end px-4 py-2 shrink-0">
|
<div className="flex items-center justify-end px-4 py-2 shrink-0">
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<Button
|
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onClose}
|
|
||||||
className="size-7 shrink-0"
|
|
||||||
>
|
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
<span className="sr-only">Close report panel</span>
|
<span className="sr-only">Close report panel</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -282,64 +259,63 @@ function ReportPanelContent({
|
||||||
{/* Action bar — always visible after initial load */}
|
{/* Action bar — always visible after initial load */}
|
||||||
<div className="flex items-center justify-between px-4 py-2 shrink-0">
|
<div className="flex items-center justify-between px-4 py-2 shrink-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Copy button */}
|
{/* Copy button */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
disabled={isLoading || !reportContent?.content}
|
disabled={isLoading || !reportContent?.content}
|
||||||
className="h-8 min-w-[80px] px-3.5 py-4 text-[15px]"
|
className="h-8 min-w-[80px] px-3.5 py-4 text-[15px]"
|
||||||
>
|
>
|
||||||
{copied ? "Copied" : "Copy"}
|
{copied ? "Copied" : "Copy"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Export dropdown */}
|
{/* Export dropdown */}
|
||||||
<DropdownMenu modal={insideDrawer ? false : undefined}>
|
<DropdownMenu modal={insideDrawer ? false : undefined}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={isLoading || !reportContent?.content}
|
disabled={isLoading || !reportContent?.content}
|
||||||
className="h-8 px-3.5 py-4 text-[15px] gap-1.5"
|
className="h-8 px-3.5 py-4 text-[15px] gap-1.5"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
<ChevronDownIcon className="size-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className={`min-w-[180px]${insideDrawer ? " z-[100]" : ""}`}
|
||||||
>
|
>
|
||||||
Export
|
<DropdownMenuItem onClick={() => handleExport("md")}>
|
||||||
<ChevronDownIcon className="size-3" />
|
Download Markdown
|
||||||
</Button>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuTrigger>
|
{/* PDF/DOCX export requires server-side conversion via authenticated endpoint.
|
||||||
<DropdownMenuContent align="start" className={`min-w-[180px]${insideDrawer ? " z-[100]" : ""}`}>
|
|
||||||
<DropdownMenuItem onClick={() => handleExport("md")}>
|
|
||||||
Download Markdown
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{/* PDF/DOCX export requires server-side conversion via authenticated endpoint.
|
|
||||||
Hide for public viewers who have no auth token. */}
|
Hide for public viewers who have no auth token. */}
|
||||||
{!shareToken && (
|
{!shareToken && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleExport("pdf")}
|
onClick={() => handleExport("pdf")}
|
||||||
disabled={exporting !== null}
|
disabled={exporting !== null}
|
||||||
>
|
>
|
||||||
{exporting === "pdf" && (
|
{exporting === "pdf" && <Spinner size="xs" />}
|
||||||
<Spinner size="xs" />
|
Download PDF
|
||||||
)}
|
</DropdownMenuItem>
|
||||||
Download PDF
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
onClick={() => handleExport("docx")}
|
||||||
<DropdownMenuItem
|
disabled={exporting !== null}
|
||||||
onClick={() => handleExport("docx")}
|
>
|
||||||
disabled={exporting !== null}
|
{exporting === "docx" && <Spinner size="xs" />}
|
||||||
>
|
Download DOCX
|
||||||
{exporting === "docx" && (
|
</DropdownMenuItem>
|
||||||
<Spinner size="xs" />
|
</>
|
||||||
)}
|
)}
|
||||||
Download DOCX
|
</DropdownMenuContent>
|
||||||
</DropdownMenuItem>
|
</DropdownMenu>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
{/* Version switcher — only shown when multiple versions exist */}
|
{/* Version switcher — only shown when multiple versions exist */}
|
||||||
{versions.length > 1 && (
|
{versions.length > 1 &&
|
||||||
insideDrawer ? (
|
(insideDrawer ? (
|
||||||
/* Mobile: compact dropdown */
|
/* Mobile: compact dropdown */
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|
@ -387,16 +363,10 @@ function ReportPanelContent({
|
||||||
{activeVersionIndex + 1} of {versions.length}
|
{activeVersionIndex + 1} of {versions.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<Button
|
<Button variant="ghost" size="icon" onClick={onClose} className="size-7 shrink-0">
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onClose}
|
|
||||||
className="size-7 shrink-0"
|
|
||||||
>
|
|
||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
<span className="sr-only">Close report panel</span>
|
<span className="sr-only">Close report panel</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -411,9 +381,7 @@ function ReportPanelContent({
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-3 p-6 text-center">
|
<div className="flex flex-1 flex-col items-center justify-center gap-3 p-6 text-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-foreground">Failed to load report</p>
|
<p className="font-medium text-foreground">Failed to load report</p>
|
||||||
<p className="text-sm text-red-500 mt-1">
|
<p className="text-sm text-red-500 mt-1">{error || "An unknown error occurred"}</p>
|
||||||
{error || "An unknown error occurred"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -421,9 +389,7 @@ function ReportPanelContent({
|
||||||
{reportContent.content ? (
|
{reportContent.content ? (
|
||||||
<MarkdownViewer content={reportContent.content} />
|
<MarkdownViewer content={reportContent.content} />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground italic">
|
<p className="text-muted-foreground italic">No content available.</p>
|
||||||
No content available.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -485,10 +451,7 @@ function MobileReportDrawer() {
|
||||||
}}
|
}}
|
||||||
shouldScaleBackground={false}
|
shouldScaleBackground={false}
|
||||||
>
|
>
|
||||||
<DrawerContent
|
<DrawerContent className="h-[90vh] max-h-[90vh] z-80" overlayClassName="z-80">
|
||||||
className="h-[90vh] max-h-[90vh] z-80"
|
|
||||||
overlayClassName="z-80"
|
|
||||||
>
|
|
||||||
<DrawerHandle />
|
<DrawerHandle />
|
||||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||||
<ReportPanelContent
|
<ReportPanelContent
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,9 @@ export function Article({
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-muted-foreground text-[10px] sm:text-xs mt-1 line-clamp-2">{description}</p>
|
<p className="text-muted-foreground text-[10px] sm:text-xs mt-1 line-clamp-2">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Metadata row */}
|
{/* Metadata row */}
|
||||||
|
|
@ -305,7 +307,6 @@ export function Article({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Response actions */}
|
{/* Response actions */}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ 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";
|
||||||
import {
|
import { openReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
|
||||||
openReportPanelAtom,
|
|
||||||
reportPanelAtom,
|
|
||||||
} from "@/atoms/chat/report-panel.atom";
|
|
||||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,9 +94,7 @@ function ReportErrorState({ title, error }: { title: string; error: string }) {
|
||||||
<h3 className="font-semibold text-muted-foreground text-sm sm:text-base leading-tight line-clamp-2">
|
<h3 className="font-semibold text-muted-foreground text-sm sm:text-base leading-tight line-clamp-2">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground/60 text-[11px] sm:text-xs mt-0.5 truncate">
|
<p className="text-muted-foreground/60 text-[11px] sm:text-xs mt-0.5 truncate">{error}</p>
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -148,10 +143,7 @@ function ReportCard({
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
// Check if report was marked as failed in metadata
|
// Check if report was marked as failed in metadata
|
||||||
if (parsed.data.report_metadata?.status === "failed") {
|
if (parsed.data.report_metadata?.status === "failed") {
|
||||||
setError(
|
setError(parsed.data.report_metadata?.error_message || "Report generation failed");
|
||||||
parsed.data.report_metadata?.error_message ||
|
|
||||||
"Report generation failed"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Determine version label from versions array
|
// Determine version label from versions array
|
||||||
let versionLabel: string | null = null;
|
let versionLabel: string | null = null;
|
||||||
|
|
@ -164,8 +156,7 @@ function ReportCard({
|
||||||
}
|
}
|
||||||
setMetadata({
|
setMetadata({
|
||||||
title: parsed.data.title || title,
|
title: parsed.data.title || title,
|
||||||
wordCount:
|
wordCount: parsed.data.report_metadata?.word_count ?? wordCount ?? null,
|
||||||
parsed.data.report_metadata?.word_count ?? wordCount ?? null,
|
|
||||||
versionLabel,
|
versionLabel,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -219,9 +210,10 @@ function ReportCard({
|
||||||
<span className="inline-block h-3 w-24 rounded bg-muted/60 animate-pulse" />
|
<span className="inline-block h-3 w-24 rounded bg-muted/60 animate-pulse" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{metadata.wordCount != null &&
|
{metadata.wordCount != null && `${metadata.wordCount.toLocaleString()} words`}
|
||||||
`${metadata.wordCount.toLocaleString()} words`}
|
{metadata.wordCount != null && metadata.versionLabel && (
|
||||||
{metadata.wordCount != null && metadata.versionLabel && <Dot className="inline size-4" />}
|
<Dot className="inline size-4" />
|
||||||
|
)}
|
||||||
{metadata.versionLabel}
|
{metadata.versionLabel}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -241,10 +233,7 @@ function ReportCard({
|
||||||
* Unlike podcast (which uses polling), the report is generated inline
|
* Unlike podcast (which uses polling), the report is generated inline
|
||||||
* and the result contains status: "ready" immediately.
|
* and the result contains status: "ready" immediately.
|
||||||
*/
|
*/
|
||||||
export const GenerateReportToolUI = makeAssistantToolUI<
|
export const GenerateReportToolUI = makeAssistantToolUI<GenerateReportArgs, GenerateReportResult>({
|
||||||
GenerateReportArgs,
|
|
||||||
GenerateReportResult
|
|
||||||
>({
|
|
||||||
toolName: "generate_report",
|
toolName: "generate_report",
|
||||||
render: function GenerateReportUI({ args, result, status }) {
|
render: function GenerateReportUI({ args, result, status }) {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -288,7 +277,12 @@ export const GenerateReportToolUI = makeAssistantToolUI<
|
||||||
|
|
||||||
// Failed result
|
// Failed result
|
||||||
if (result.status === "failed") {
|
if (result.status === "failed") {
|
||||||
return <ReportErrorState title={result.title || topic} error={result.error || "Generation failed"} />;
|
return (
|
||||||
|
<ReportErrorState
|
||||||
|
title={result.title || topic}
|
||||||
|
error={result.error || "Generation failed"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ready with report_id
|
// Ready with report_id
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,7 @@ function ScrapeCancelledState({ url }: { url: string }) {
|
||||||
function ParsedArticle({ result }: { result: unknown }) {
|
function ParsedArticle({ result }: { result: unknown }) {
|
||||||
const { description, ...article } = parseSerializableArticle(result);
|
const { description, ...article } = parseSerializableArticle(result);
|
||||||
|
|
||||||
return (
|
return <Article {...article} maxWidth="480px" />;
|
||||||
<Article
|
|
||||||
{...article}
|
|
||||||
maxWidth="480px"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue