mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
feat: enhance podcast generation with duplicate request prevention and improved UI feedback
This commit is contained in:
parent
e79e1187b2
commit
783ee9c154
6 changed files with 269 additions and 7 deletions
|
|
@ -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} />;
|
||||
|
|
|
|||
|
|
@ -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() };
|
||||
}
|
||||
|
|
|
|||
74
surfsense_web/lib/chat/podcast-state.ts
Normal file
74
surfsense_web/lib/chat/podcast-state.ts
Normal 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));
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue