diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 41962b769..5a74cddeb 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -753,7 +753,12 @@ class Podcast(BaseModel, TimestampMixin): podcast_transcript = Column(JSONB, nullable=True) file_location = Column(Text, nullable=True) status = Column( - SQLAlchemyEnum(PodcastStatus, name="podcast_status", create_type=False), + SQLAlchemyEnum( + PodcastStatus, + name="podcast_status", + create_type=False, + values_callable=lambda x: [e.value for e in x], + ), nullable=False, default=PodcastStatus.READY, server_default="ready", diff --git a/surfsense_backend/app/routes/podcasts_routes.py b/surfsense_backend/app/routes/podcasts_routes.py index 041dd80ee..fa8326096 100644 --- a/surfsense_backend/app/routes/podcasts_routes.py +++ b/surfsense_backend/app/routes/podcasts_routes.py @@ -116,7 +116,7 @@ async def read_podcast( "You don't have permission to read podcasts in this search space", ) - return podcast + return PodcastRead.from_orm_with_entries(podcast) except HTTPException as he: raise he except SQLAlchemyError: diff --git a/surfsense_backend/app/schemas/podcasts.py b/surfsense_backend/app/schemas/podcasts.py index ad77c27f8..9e5cb0262 100644 --- a/surfsense_backend/app/schemas/podcasts.py +++ b/surfsense_backend/app/schemas/podcasts.py @@ -43,6 +43,22 @@ class PodcastRead(PodcastBase): id: int status: PodcastStatusEnum = PodcastStatusEnum.READY created_at: datetime + transcript_entries: int | None = None class Config: from_attributes = True + + @classmethod + def from_orm_with_entries(cls, obj): + """Create PodcastRead with transcript_entries computed.""" + data = { + "id": obj.id, + "title": obj.title, + "podcast_transcript": obj.podcast_transcript, + "file_location": obj.file_location, + "search_space_id": obj.search_space_id, + "status": obj.status, + "created_at": obj.created_at, + "transcript_entries": len(obj.podcast_transcript) if obj.podcast_transcript else None, + } + return cls(**data) diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index d025bceab..33ec64696 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -790,13 +790,13 @@ export default function NewChatPage() { // Update the tool call with its result updateToolCall(parsed.toolCallId, { result: parsed.output }); // Handle podcast-specific logic - if (parsed.output?.status === "processing" && parsed.output?.task_id) { + if (parsed.output?.status === "pending" && parsed.output?.podcast_id) { // Check if this is a podcast tool by looking at the content part const idx = toolCallIndices.get(parsed.toolCallId); if (idx !== undefined) { const part = contentParts[idx]; if (part?.type === "tool-call" && part.toolName === "generate_podcast") { - setActivePodcastTaskId(parsed.output.task_id); + setActivePodcastTaskId(String(parsed.output.podcast_id)); } } } @@ -1210,12 +1210,12 @@ export default function NewChatPage() { case "tool-output-available": updateToolCall(parsed.toolCallId, { result: parsed.output }); - if (parsed.output?.status === "processing" && parsed.output?.task_id) { + if (parsed.output?.status === "pending" && parsed.output?.podcast_id) { const idx = toolCallIndices.get(parsed.toolCallId); if (idx !== undefined) { const part = contentParts[idx]; if (part?.type === "tool-call" && part.toolName === "generate_podcast") { - setActivePodcastTaskId(parsed.output.task_id); + setActivePodcastTaskId(String(parsed.output.podcast_id)); } } } diff --git a/surfsense_web/components/tool-ui/generate-podcast.tsx b/surfsense_web/components/tool-ui/generate-podcast.tsx index c76d7ce5a..67eabbc90 100644 --- a/surfsense_web/components/tool-ui/generate-podcast.tsx +++ b/surfsense_web/components/tool-ui/generate-podcast.tsx @@ -20,21 +20,31 @@ const GeneratePodcastArgsSchema = z.object({ }); const GeneratePodcastResultSchema = z.object({ - status: z.enum(["processing", "already_generating", "success", "error"]), - task_id: z.string().nullish(), + // Support both old and new status values for backwards compatibility + status: z.enum([ + "pending", + "generating", + "ready", + "failed", + // Legacy values from old saved chats + "processing", + "already_generating", + "success", + "error", + ]), podcast_id: z.number().nullish(), + task_id: z.string().nullish(), // Legacy field for old saved chats title: z.string().nullish(), transcript_entries: z.number().nullish(), message: z.string().nullish(), error: z.string().nullish(), }); -const TaskStatusResponseSchema = z.object({ - status: z.enum(["processing", "success", "error"]), - podcast_id: z.number().nullish(), - title: z.string().nullish(), +const PodcastStatusResponseSchema = z.object({ + status: z.enum(["pending", "generating", "ready", "failed"]), + id: z.number(), + title: z.string(), transcript_entries: z.number().nullish(), - state: z.string().nullish(), error: z.string().nullish(), }); @@ -52,17 +62,17 @@ const PodcastDetailsSchema = z.object({ */ type GeneratePodcastArgs = z.infer; type GeneratePodcastResult = z.infer; -type TaskStatusResponse = z.infer; +type PodcastStatusResponse = z.infer; type PodcastTranscriptEntry = z.infer; /** - * Parse and validate task status response + * Parse and validate podcast status response */ -function parseTaskStatusResponse(data: unknown): TaskStatusResponse { - const result = TaskStatusResponseSchema.safeParse(data); +function parsePodcastStatusResponse(data: unknown): PodcastStatusResponse | null { + const result = PodcastStatusResponseSchema.safeParse(data); if (!result.success) { - console.warn("Invalid task status response:", result.error.issues); - return { status: "error", error: "Invalid response from server" }; + console.warn("Invalid podcast status response:", result.error.issues); + return null; } return result.data; } @@ -283,44 +293,42 @@ function PodcastPlayer({ } /** - * Polling component that checks task status and shows player when complete + * Polling component that checks podcast status and shows player when ready */ -function PodcastTaskPoller({ taskId, title }: { taskId: string; title: string }) { - const [taskStatus, setTaskStatus] = useState({ status: "processing" }); +function PodcastStatusPoller({ podcastId, title }: { podcastId: number; title: string }) { + const [podcastStatus, setPodcastStatus] = useState(null); const pollingRef = useRef(null); // Set active podcast state when this component mounts useEffect(() => { - setActivePodcastTaskId(taskId); + setActivePodcastTaskId(String(podcastId)); // Clear when component unmounts return () => { - // Only clear if this task is still the active one clearActivePodcastTaskId(); }; - }, [taskId]); + }, [podcastId]); - // Poll for task status + // Poll for podcast status useEffect(() => { const pollStatus = async () => { try { - const rawResponse = await baseApiService.get( - `/api/v1/podcasts/task/${taskId}/status` - ); - const response = parseTaskStatusResponse(rawResponse); - setTaskStatus(response); + const rawResponse = await baseApiService.get(`/api/v1/podcasts/${podcastId}`); + const response = parsePodcastStatusResponse(rawResponse); + if (response) { + setPodcastStatus(response); - // Stop polling if task is complete or errored - if (response.status !== "processing") { - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; + // Stop polling if podcast is ready or failed + if (response.status === "ready" || response.status === "failed") { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + clearActivePodcastTaskId(); } - // Clear the active podcast state when task completes - clearActivePodcastTaskId(); } } catch (err) { - console.error("Error polling task status:", err); + console.error("Error polling podcast status:", err); // Don't stop polling on network errors, continue polling } }; @@ -336,27 +344,31 @@ function PodcastTaskPoller({ taskId, title }: { taskId: string; title: string }) clearInterval(pollingRef.current); } }; - }, [taskId]); + }, [podcastId]); - // Show loading state while processing - if (taskStatus.status === "processing") { + // Show loading state while pending or generating + if ( + !podcastStatus || + podcastStatus.status === "pending" || + podcastStatus.status === "generating" + ) { return ; } // Show error state - if (taskStatus.status === "error") { - return ; + if (podcastStatus.status === "failed") { + return ; } - // Show player when complete - if (taskStatus.status === "success" && taskStatus.podcast_id) { + // Show player when ready + if (podcastStatus.status === "ready") { return ( @@ -415,14 +427,15 @@ export const GeneratePodcastToolUI = makeAssistantToolUI< return ; } - // Error result - if (result.status === "error") { - return ; + // Failed result (new: "failed", legacy: "error") + if (result.status === "failed" || result.status === "error") { + return ; } // Already generating - show simple warning, don't create another poller // The FIRST tool call will display the podcast when ready - if (result.status === "already_generating") { + // (new: "generating", legacy: "already_generating") + if (result.status === "generating" || result.status === "already_generating") { return (
@@ -442,13 +455,13 @@ export const GeneratePodcastToolUI = makeAssistantToolUI< ); } - // Processing - poll for completion - if (result.status === "processing" && result.task_id) { - return ; + // Pending - poll for completion (new: "pending" with podcast_id) + if (result.status === "pending" && result.podcast_id) { + return ; } - // Success with podcast_id (direct result, not via polling) - if (result.status === "success" && result.podcast_id) { + // Ready with podcast_id (new: "ready", legacy: "success") + if ((result.status === "ready" || result.status === "success") && result.podcast_id) { return ( +
+
+ +
+
+

+ This podcast was generated with an older version and cannot be displayed. +

+

+ Please generate a new podcast to listen. +

+
+
+
+ ); + } + // Fallback - missing required data - return ; + return ; }, });