mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-02 19:55:18 +02:00
feat: clone podcasts when cloning public chat
Creates new podcast records for cloned user with thread_id, updates podcast_id references in message content.
This commit is contained in:
parent
e3d6b1d789
commit
070bb42506
2 changed files with 53 additions and 29 deletions
|
|
@ -194,7 +194,7 @@ async def create_snapshot(
|
|||
author = await get_author_display(session, msg.author_id, user_cache)
|
||||
sanitized_content = sanitize_content_for_public(msg.content)
|
||||
|
||||
# Extract podcast references (keep original podcast_id unchanged)
|
||||
# Extract podcast references and update status to "ready" for completed podcasts
|
||||
if isinstance(sanitized_content, list):
|
||||
for part in sanitized_content:
|
||||
if (
|
||||
|
|
@ -205,13 +205,14 @@ async def create_snapshot(
|
|||
result_data = part.get("result", {})
|
||||
podcast_id = result_data.get("podcast_id")
|
||||
if podcast_id and podcast_id not in podcast_ids_seen:
|
||||
|
||||
podcast_info = await _get_podcast_for_snapshot(
|
||||
session, podcast_id
|
||||
)
|
||||
if podcast_info:
|
||||
podcasts_data.append(podcast_info)
|
||||
podcast_ids_seen.add(podcast_id)
|
||||
# Update status to "ready" so frontend renders PodcastPlayer
|
||||
part["result"] = {**result_data, "status": "ready"}
|
||||
|
||||
|
||||
messages_data.append(
|
||||
|
|
@ -494,9 +495,12 @@ async def clone_from_snapshot(
|
|||
Copy messages and podcasts from source thread to target thread.
|
||||
|
||||
Creates thread and copies messages from snapshot_data.
|
||||
When encountering generate_podcast tool-calls, creates cloned podcast records
|
||||
and updates the podcast_id references inline.
|
||||
Returns the new thread info.
|
||||
"""
|
||||
# Get snapshot
|
||||
import copy
|
||||
|
||||
snapshot = await get_snapshot_by_token(session, share_token)
|
||||
|
||||
if not snapshot:
|
||||
|
|
@ -504,17 +508,15 @@ async def clone_from_snapshot(
|
|||
status_code=404, detail="Chat not found or no longer public"
|
||||
)
|
||||
|
||||
# Get user's default search space
|
||||
target_search_space_id = await get_user_default_search_space(session, user.id)
|
||||
|
||||
if target_search_space_id is None:
|
||||
raise HTTPException(status_code=400, detail="No search space found for user")
|
||||
|
||||
# Get snapshot data
|
||||
data = snapshot.snapshot_data
|
||||
messages_data = data.get("messages", [])
|
||||
podcasts_lookup = {p.get("original_id"): p for p in data.get("podcasts", [])}
|
||||
|
||||
# Create new thread
|
||||
new_thread = NewChatThread(
|
||||
title=data.get("title", "Cloned Chat"),
|
||||
archived=False,
|
||||
|
|
@ -526,22 +528,55 @@ async def clone_from_snapshot(
|
|||
needs_history_bootstrap=True,
|
||||
)
|
||||
session.add(new_thread)
|
||||
await session.flush() # Get thread ID
|
||||
await session.flush()
|
||||
|
||||
podcast_id_mapping: dict[int, int] = {}
|
||||
|
||||
# Copy messages from snapshot_data (preserve original authors)
|
||||
for msg_data in messages_data:
|
||||
# Parse original author_id if present
|
||||
original_author_id = None
|
||||
author_id_str = msg_data.get("author_id")
|
||||
if author_id_str:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
original_author_id = UUID(author_id_str)
|
||||
|
||||
content = copy.deepcopy(msg_data.get("content", []))
|
||||
|
||||
if isinstance(content, list):
|
||||
for part in content:
|
||||
if (
|
||||
isinstance(part, dict)
|
||||
and part.get("type") == "tool-call"
|
||||
and part.get("toolName") == "generate_podcast"
|
||||
):
|
||||
result = part.get("result", {})
|
||||
old_podcast_id = result.get("podcast_id")
|
||||
|
||||
if old_podcast_id and old_podcast_id not in podcast_id_mapping:
|
||||
podcast_info = podcasts_lookup.get(old_podcast_id)
|
||||
if podcast_info:
|
||||
new_podcast = Podcast(
|
||||
title=podcast_info.get("title", "Cloned Podcast"),
|
||||
podcast_transcript=podcast_info.get("transcript"),
|
||||
file_location=podcast_info.get("file_path"),
|
||||
status=PodcastStatus.READY,
|
||||
search_space_id=target_search_space_id,
|
||||
thread_id=new_thread.id,
|
||||
)
|
||||
session.add(new_podcast)
|
||||
await session.flush()
|
||||
podcast_id_mapping[old_podcast_id] = new_podcast.id
|
||||
|
||||
if old_podcast_id and old_podcast_id in podcast_id_mapping:
|
||||
part["result"] = {
|
||||
**result,
|
||||
"podcast_id": podcast_id_mapping[old_podcast_id],
|
||||
}
|
||||
|
||||
new_message = NewChatMessage(
|
||||
thread_id=new_thread.id,
|
||||
role=msg_data.get("role", "user"),
|
||||
content=msg_data.get("content", []),
|
||||
author_id=original_author_id,
|
||||
content=content,
|
||||
author_id=original_author_id,
|
||||
)
|
||||
session.add(new_message)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { AlertCircleIcon, MicIcon } from "lucide-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { Audio } from "@/components/tool-ui/audio";
|
||||
|
|
@ -172,20 +173,6 @@ function AudioLoadingState({ title }: { title: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public share token from URL if in public view.
|
||||
* Returns null if not in a public view.
|
||||
*/
|
||||
function getPublicShareToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const match = window.location.pathname.match(/^\/public\/([^/]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Podcast Player Component - Fetches audio and transcript
|
||||
* Automatically uses public endpoint when viewing a public chat snapshot.
|
||||
*/
|
||||
function PodcastPlayer({
|
||||
podcastId,
|
||||
title,
|
||||
|
|
@ -197,6 +184,11 @@ function PodcastPlayer({
|
|||
description: string;
|
||||
durationMs?: number;
|
||||
}) {
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const isPublicRoute = pathname?.startsWith("/public/");
|
||||
const shareToken = isPublicRoute && typeof params?.token === "string" ? params.token : null;
|
||||
|
||||
const [audioSrc, setAudioSrc] = useState<string | null>(null);
|
||||
const [transcript, setTranscript] = useState<PodcastTranscriptEntry[] | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
|
@ -228,9 +220,6 @@ function PodcastPlayer({
|
|||
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
|
||||
|
||||
try {
|
||||
// Check if we're in a public view
|
||||
const shareToken = getPublicShareToken();
|
||||
|
||||
let audioBlob: Blob;
|
||||
let rawPodcastDetails: unknown = null;
|
||||
|
||||
|
|
@ -285,7 +274,7 @@ function PodcastPlayer({
|
|||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [podcastId]);
|
||||
}, [podcastId, shareToken]);
|
||||
|
||||
// Load podcast when component mounts
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue