mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-14 22:52:15 +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)
|
author = await get_author_display(session, msg.author_id, user_cache)
|
||||||
sanitized_content = sanitize_content_for_public(msg.content)
|
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):
|
if isinstance(sanitized_content, list):
|
||||||
for part in sanitized_content:
|
for part in sanitized_content:
|
||||||
if (
|
if (
|
||||||
|
|
@ -205,13 +205,14 @@ async def create_snapshot(
|
||||||
result_data = part.get("result", {})
|
result_data = part.get("result", {})
|
||||||
podcast_id = result_data.get("podcast_id")
|
podcast_id = result_data.get("podcast_id")
|
||||||
if podcast_id and podcast_id not in podcast_ids_seen:
|
if podcast_id and podcast_id not in podcast_ids_seen:
|
||||||
|
|
||||||
podcast_info = await _get_podcast_for_snapshot(
|
podcast_info = await _get_podcast_for_snapshot(
|
||||||
session, podcast_id
|
session, podcast_id
|
||||||
)
|
)
|
||||||
if podcast_info:
|
if podcast_info:
|
||||||
podcasts_data.append(podcast_info)
|
podcasts_data.append(podcast_info)
|
||||||
podcast_ids_seen.add(podcast_id)
|
podcast_ids_seen.add(podcast_id)
|
||||||
|
# Update status to "ready" so frontend renders PodcastPlayer
|
||||||
|
part["result"] = {**result_data, "status": "ready"}
|
||||||
|
|
||||||
|
|
||||||
messages_data.append(
|
messages_data.append(
|
||||||
|
|
@ -494,9 +495,12 @@ async def clone_from_snapshot(
|
||||||
Copy messages and podcasts from source thread to target thread.
|
Copy messages and podcasts from source thread to target thread.
|
||||||
|
|
||||||
Creates thread and copies messages from snapshot_data.
|
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.
|
Returns the new thread info.
|
||||||
"""
|
"""
|
||||||
# Get snapshot
|
import copy
|
||||||
|
|
||||||
snapshot = await get_snapshot_by_token(session, share_token)
|
snapshot = await get_snapshot_by_token(session, share_token)
|
||||||
|
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
|
|
@ -504,17 +508,15 @@ async def clone_from_snapshot(
|
||||||
status_code=404, detail="Chat not found or no longer public"
|
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)
|
target_search_space_id = await get_user_default_search_space(session, user.id)
|
||||||
|
|
||||||
if target_search_space_id is None:
|
if target_search_space_id is None:
|
||||||
raise HTTPException(status_code=400, detail="No search space found for user")
|
raise HTTPException(status_code=400, detail="No search space found for user")
|
||||||
|
|
||||||
# Get snapshot data
|
|
||||||
data = snapshot.snapshot_data
|
data = snapshot.snapshot_data
|
||||||
messages_data = data.get("messages", [])
|
messages_data = data.get("messages", [])
|
||||||
|
podcasts_lookup = {p.get("original_id"): p for p in data.get("podcasts", [])}
|
||||||
|
|
||||||
# Create new thread
|
|
||||||
new_thread = NewChatThread(
|
new_thread = NewChatThread(
|
||||||
title=data.get("title", "Cloned Chat"),
|
title=data.get("title", "Cloned Chat"),
|
||||||
archived=False,
|
archived=False,
|
||||||
|
|
@ -526,21 +528,54 @@ async def clone_from_snapshot(
|
||||||
needs_history_bootstrap=True,
|
needs_history_bootstrap=True,
|
||||||
)
|
)
|
||||||
session.add(new_thread)
|
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:
|
for msg_data in messages_data:
|
||||||
# Parse original author_id if present
|
|
||||||
original_author_id = None
|
original_author_id = None
|
||||||
author_id_str = msg_data.get("author_id")
|
author_id_str = msg_data.get("author_id")
|
||||||
if author_id_str:
|
if author_id_str:
|
||||||
with contextlib.suppress(ValueError, TypeError):
|
with contextlib.suppress(ValueError, TypeError):
|
||||||
original_author_id = UUID(author_id_str)
|
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(
|
new_message = NewChatMessage(
|
||||||
thread_id=new_thread.id,
|
thread_id=new_thread.id,
|
||||||
role=msg_data.get("role", "user"),
|
role=msg_data.get("role", "user"),
|
||||||
content=msg_data.get("content", []),
|
content=content,
|
||||||
author_id=original_author_id,
|
author_id=original_author_id,
|
||||||
)
|
)
|
||||||
session.add(new_message)
|
session.add(new_message)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { AlertCircleIcon, MicIcon } from "lucide-react";
|
import { AlertCircleIcon, MicIcon } from "lucide-react";
|
||||||
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Audio } from "@/components/tool-ui/audio";
|
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({
|
function PodcastPlayer({
|
||||||
podcastId,
|
podcastId,
|
||||||
title,
|
title,
|
||||||
|
|
@ -197,6 +184,11 @@ function PodcastPlayer({
|
||||||
description: string;
|
description: string;
|
||||||
durationMs?: number;
|
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 [audioSrc, setAudioSrc] = useState<string | null>(null);
|
||||||
const [transcript, setTranscript] = useState<PodcastTranscriptEntry[] | null>(null);
|
const [transcript, setTranscript] = useState<PodcastTranscriptEntry[] | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
@ -228,9 +220,6 @@ function PodcastPlayer({
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
|
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if we're in a public view
|
|
||||||
const shareToken = getPublicShareToken();
|
|
||||||
|
|
||||||
let audioBlob: Blob;
|
let audioBlob: Blob;
|
||||||
let rawPodcastDetails: unknown = null;
|
let rawPodcastDetails: unknown = null;
|
||||||
|
|
||||||
|
|
@ -285,7 +274,7 @@ function PodcastPlayer({
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [podcastId]);
|
}, [podcastId, shareToken]);
|
||||||
|
|
||||||
// Load podcast when component mounts
|
// Load podcast when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue