feat: enhance podcast generation with duplicate request prevention and improved UI feedback

This commit is contained in:
Anish Sarkar 2025-12-21 20:07:04 +05:30
parent e79e1187b2
commit 783ee9c154
6 changed files with 269 additions and 7 deletions

View file

@ -6,6 +6,10 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { Audio } from "@/components/tool-ui/audio";
import { baseApiService } from "@/lib/apis/base-api.service";
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
import {
clearActivePodcastTaskId,
setActivePodcastTaskId,
} from "@/lib/chat/podcast-state";
/**
* Type definitions for the generate_podcast tool
@ -17,7 +21,7 @@ interface GeneratePodcastArgs {
}
interface GeneratePodcastResult {
status: "processing" | "success" | "error";
status: "processing" | "already_generating" | "success" | "error";
task_id?: string;
podcast_id?: number;
title?: string;
@ -218,6 +222,17 @@ function PodcastTaskPoller({
const [pollCount, setPollCount] = useState(0);
const pollingRef = useRef<NodeJS.Timeout | null>(null);
// Set active podcast state when this component mounts
useEffect(() => {
setActivePodcastTaskId(taskId);
// Clear when component unmounts
return () => {
// Only clear if this task is still the active one
clearActivePodcastTaskId();
};
}, [taskId]);
// Poll for task status
useEffect(() => {
const pollStatus = async () => {
@ -233,6 +248,8 @@ function PodcastTaskPoller({
clearInterval(pollingRef.current);
pollingRef.current = null;
}
// Clear the active podcast state when task completes
clearActivePodcastTaskId();
}
} catch (err) {
console.error("Error polling task status:", err);
@ -336,6 +353,28 @@ export const GeneratePodcastToolUI = makeAssistantToolUI<
return <PodcastErrorState title={title} error={result.error || "Unknown error"} />;
}
// 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") {
return (
<div className="my-4 overflow-hidden rounded-xl border border-amber-500/20 bg-amber-500/5 p-4">
<div className="flex items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
<MicIcon className="size-5 text-amber-500" />
</div>
<div>
<p className="text-amber-600 dark:text-amber-400 text-sm font-medium">
Podcast already in progress
</p>
<p className="text-muted-foreground text-xs mt-0.5">
Please wait for the current podcast to complete.
</p>
</div>
</div>
</div>
);
}
// Processing - poll for completion
if (result.status === "processing" && result.task_id) {
return <PodcastTaskPoller taskId={result.task_id} title={result.title || title} />;

View file

@ -4,7 +4,13 @@
*/
import type { ChatModelAdapter, ChatModelRunOptions } from "@assistant-ui/react";
import { toast } from "sonner";
import { getBearerToken } from "@/lib/auth-utils";
import {
isPodcastGenerating,
looksLikePodcastRequest,
setActivePodcastTaskId,
} from "@/lib/chat/podcast-state";
interface NewChatAdapterConfig {
searchSpaceId: number;
@ -59,6 +65,21 @@ export function createNewChatAdapter(config: NewChatAdapterConfig): ChatModelAda
throw new Error("User query cannot be empty");
}
// Check if user is requesting a podcast while one is already generating
if (isPodcastGenerating() && looksLikePodcastRequest(userQuery)) {
toast.warning("A podcast is already being generated. Please wait for it to complete.");
// Return a message telling the user to wait
yield {
content: [
{
type: "text",
text: "A podcast is already being generated. Please wait for it to complete before requesting another one.",
},
],
};
return;
}
const token = getBearerToken();
if (!token) {
throw new Error("Not authenticated. Please log in again.");
@ -204,6 +225,20 @@ export function createNewChatAdapter(config: NewChatAdapterConfig): ChatModelAda
const existing = toolCalls.get(toolCallId);
if (existing) {
existing.result = output;
// If this is a podcast tool with status="processing", set the state immediately
// This ensures subsequent podcast requests are intercepted
if (
existing.toolName === "generate_podcast" &&
output &&
typeof output === "object" &&
"status" in output &&
output.status === "processing" &&
"task_id" in output &&
typeof output.task_id === "string"
) {
setActivePodcastTaskId(output.task_id);
}
}
yield { content: buildContent() };
break;
@ -245,6 +280,19 @@ export function createNewChatAdapter(config: NewChatAdapterConfig): ChatModelAda
const existing = toolCalls.get(toolCallId);
if (existing) {
existing.result = output;
// Set podcast state if processing
if (
existing.toolName === "generate_podcast" &&
output &&
typeof output === "object" &&
"status" in output &&
output.status === "processing" &&
"task_id" in output &&
typeof output.task_id === "string"
) {
setActivePodcastTaskId(output.task_id);
}
}
yield { content: buildContent() };
}

View file

@ -0,0 +1,74 @@
/**
* Module-level state for tracking active podcast generation.
* Used by the new-chat adapter to prevent duplicate podcast requests.
*/
type PodcastStateListener = (isGenerating: boolean) => void;
let _activePodcastTaskId: string | null = null;
const _listeners: Set<PodcastStateListener> = new Set();
/**
* Check if a podcast is currently being generated
*/
export function isPodcastGenerating(): boolean {
return _activePodcastTaskId !== null;
}
/**
* Get the active podcast task ID
*/
export function getActivePodcastTaskId(): string | null {
return _activePodcastTaskId;
}
/**
* Set the active podcast task ID (when podcast generation starts)
*/
export function setActivePodcastTaskId(taskId: string): void {
_activePodcastTaskId = taskId;
notifyListeners();
}
/**
* Clear the active podcast task ID (when podcast generation completes or errors)
*/
export function clearActivePodcastTaskId(): void {
_activePodcastTaskId = null;
notifyListeners();
}
/**
* Subscribe to podcast state changes
*/
export function subscribeToPodcastState(listener: PodcastStateListener): () => void {
_listeners.add(listener);
return () => {
_listeners.delete(listener);
};
}
function notifyListeners(): void {
const isGenerating = _activePodcastTaskId !== null;
for (const listener of _listeners) {
listener(isGenerating);
}
}
/**
* Check if a message looks like a podcast request
*/
export function looksLikePodcastRequest(message: string): boolean {
const podcastPatterns = [
/\bpodcast\b/i,
/\bcreate.*podcast\b/i,
/\bgenerate.*podcast\b/i,
/\bmake.*podcast\b/i,
/\bturn.*into.*podcast\b/i,
/\bpodcast.*about\b/i,
/\bgive.*podcast\b/i,
];
return podcastPatterns.some((pattern) => pattern.test(message));
}