From 6298d631e4d3050f0ac12c849ba250b47130acc4 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 11 Nov 2025 23:51:26 -0800 Subject: [PATCH] refactor(chats): streamline podcast generation and enhance chat selection UI - Removed podcast generation state and related functions from the ChatsPageClient component. - Simplified chat selection UI by removing unnecessary elements and improving layout. - Updated the AnnouncementBanner component to use Jotai for state management, allowing for persistent dismissal of the announcement. --- .../[search_space_id]/chats/chats-client.tsx | 485 ++---------------- .../components/announcement-banner.tsx | 13 +- surfsense_web/stores/announcement.atom.ts | 5 + 3 files changed, 71 insertions(+), 432 deletions(-) create mode 100644 surfsense_web/stores/announcement.atom.ts diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx index b1f3aaf04..9517431b4 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx @@ -3,12 +3,9 @@ import { format } from "date-fns"; import { Calendar, - CheckCircle, - Circle, ExternalLink, MessageCircleMore, MoreHorizontal, - Podcast, Search, Tag, Trash2, @@ -35,9 +32,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -// UI Components import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Pagination, PaginationContent, @@ -109,18 +104,6 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) const [chatToDelete, setChatToDelete] = useState<{ id: number; title: string } | null>(null); const [isDeleting, setIsDeleting] = useState(false); - // New state for podcast generation - const [selectedChats, setSelectedChats] = useState([]); - const [selectionMode, setSelectionMode] = useState(false); - const [podcastDialogOpen, setPodcastDialogOpen] = useState(false); - const [podcastTitle, setPodcastTitle] = useState(""); - const [isGeneratingPodcast, setIsGeneratingPodcast] = useState(false); - - // New state for individual podcast generation - const [currentChatIndex, setCurrentChatIndex] = useState(0); - const [podcastTitles, setPodcastTitles] = useState<{ [key: number]: string }>({}); - const [processingChat, setProcessingChat] = useState(null); - const chatsPerPage = 9; const searchParams = useSearchParams(); @@ -264,178 +247,6 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) // Get unique chat types for filter dropdown const chatTypes = ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))]; - // Generate individual podcasts from selected chats - const handleGeneratePodcast = async () => { - if (selectedChats.length === 0) { - toast.error("Please select at least one chat"); - return; - } - - const currentChatId = selectedChats[currentChatIndex]; - const currentTitle = podcastTitles[currentChatId] || podcastTitle; - - if (!currentTitle.trim()) { - toast.error("Please enter a podcast title"); - return; - } - - setIsGeneratingPodcast(true); - try { - const token = localStorage.getItem("surfsense_bearer_token"); - if (!token) { - toast.error("Authentication error. Please log in again."); - setIsGeneratingPodcast(false); - return; - } - - // Create payload for single chat - const payload = { - type: "CHAT", - ids: [currentChatId], // Single chat ID - search_space_id: parseInt(searchSpaceId), - podcast_title: currentTitle, - }; - - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate`, - { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - } - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.detail || "Failed to generate podcast"); - } - - const _data = await response.json(); - toast.success(`Podcast "${currentTitle}" generation started!`); - - // Move to the next chat or finish - if (currentChatIndex < selectedChats.length - 1) { - // Set up for next chat - setCurrentChatIndex(currentChatIndex + 1); - - // Find the next chat from the chats array - const nextChatId = selectedChats[currentChatIndex + 1]; - const nextChat = chats.find((chat) => chat.id === nextChatId) || null; - setProcessingChat(nextChat); - - // Default title for the next chat - if (!podcastTitles[nextChatId]) { - setPodcastTitle(nextChat?.title || `Podcast from Chat ${nextChatId}`); - } else { - setPodcastTitle(podcastTitles[nextChatId]); - } - - setIsGeneratingPodcast(false); - } else { - // All done - finishPodcastGeneration(); - } - } catch (error) { - console.error("Error generating podcast:", error); - toast.error(error instanceof Error ? error.message : "Failed to generate podcast"); - setIsGeneratingPodcast(false); - } - }; - - // Helper to finish the podcast generation process - const finishPodcastGeneration = () => { - toast.success("All podcasts are being generated! Check the logs tab to see their status."); - setPodcastDialogOpen(false); - setSelectedChats([]); - setSelectionMode(false); - setCurrentChatIndex(0); - setPodcastTitles({}); - setProcessingChat(null); - setPodcastTitle(""); - setIsGeneratingPodcast(false); - }; - - // Start podcast generation flow - const startPodcastGeneration = () => { - if (selectedChats.length === 0) { - toast.error("Please select at least one chat"); - return; - } - - // Reset the state for podcast generation - setCurrentChatIndex(0); - setPodcastTitles({}); - - // Set up for the first chat - const firstChatId = selectedChats[0]; - const firstChat = chats.find((chat) => chat.id === firstChatId) || null; - setProcessingChat(firstChat); - - // Set default title for the first chat - setPodcastTitle(firstChat?.title || `Podcast from Chat ${firstChatId}`); - setPodcastDialogOpen(true); - }; - - // Update the title for the current chat - const updateCurrentChatTitle = (title: string) => { - const currentChatId = selectedChats[currentChatIndex]; - setPodcastTitle(title); - setPodcastTitles((prev) => ({ - ...prev, - [currentChatId]: title, - })); - }; - - // Skip generating a podcast for the current chat - const skipCurrentChat = () => { - if (currentChatIndex < selectedChats.length - 1) { - // Move to the next chat - setCurrentChatIndex(currentChatIndex + 1); - - // Find the next chat - const nextChatId = selectedChats[currentChatIndex + 1]; - const nextChat = chats.find((chat) => chat.id === nextChatId) || null; - setProcessingChat(nextChat); - - // Set default title for the next chat - if (!podcastTitles[nextChatId]) { - setPodcastTitle(nextChat?.title || `Podcast from Chat ${nextChatId}`); - } else { - setPodcastTitle(podcastTitles[nextChatId]); - } - } else { - // All done (all skipped) - finishPodcastGeneration(); - } - }; - - // Toggle chat selection - const toggleChatSelection = (chatId: number) => { - setSelectedChats((prev) => - prev.includes(chatId) ? prev.filter((id) => id !== chatId) : [...prev, chatId] - ); - }; - - // Select all visible chats - const selectAllVisibleChats = () => { - const visibleChatIds = currentChats.map((chat) => chat.id); - setSelectedChats((prev) => { - const allSelected = visibleChatIds.every((id) => prev.includes(id)); - return allSelected - ? prev.filter((id) => !visibleChatIds.includes(id)) // Deselect all visible if all are selected - : [...new Set([...prev, ...visibleChatIds])]; // Add all visible, ensuring no duplicates - }); - }; - - // Cancel selection mode - const cancelSelectionMode = () => { - setSelectionMode(false); - setSelectedChats([]); - }; - return (
- {selectionMode ? ( - <> - - - - - ) : ( - <> - - - - )} +
@@ -577,96 +347,56 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) animate="animate" exit="exit" transition={{ duration: 0.2, delay: index * 0.05 }} - className={cn( - "overflow-hidden hover:shadow-md transition-shadow", - selectionMode && selectedChats.includes(chat.id) - ? "ring-2 ring-primary ring-offset-2" - : "" - )} - onClick={(e) => { - if (!selectionMode) return; - // Ignore clicks coming from interactive elements - if ((e.target as HTMLElement).closest("button, a, [data-stop-selection]")) - return; - toggleChatSelection(chat.id); - }} + className="overflow-hidden hover:shadow-md transition-shadow" >
-
- {selectionMode && ( -
- {selectedChats.includes(chat.id) ? ( - - ) : ( - - )} -
- )} -
- - {chat.title || `Chat ${chat.id}`} - - - - - {format(new Date(chat.created_at), "MMM d, yyyy")} - - -
+
+ + {chat.title || `Chat ${chat.id}`} + + + + + {format(new Date(chat.created_at), "MMM d, yyyy")} + +
- {!selectionMode && ( - - - - - - - router.push( - `/dashboard/${chat.search_space_id}/researcher/${chat.id}` - ) - } - > - - View Chat - - { - setSelectedChats([chat.id]); - setPodcastTitle(chat.title || `Chat ${chat.id}`); - setPodcastDialogOpen(true); - }} - > - - Generate Podcast - - - { - e.stopPropagation(); - setChatToDelete({ - id: chat.id, - title: chat.title || `Chat ${chat.id}`, - }); - setDeleteDialogOpen(true); - }} - > - - Delete Chat - - - - )} + + + + + + + router.push( + `/dashboard/${chat.search_space_id}/researcher/${chat.id}` + ) + } + > + + View Chat + + + { + e.stopPropagation(); + setChatToDelete({ + id: chat.id, + title: chat.title || `Chat ${chat.id}`, + }); + setDeleteDialogOpen(true); + }} + > + + Delete Chat + + +
@@ -799,107 +529,6 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) - - {/* Podcast Generation Dialog */} - { - if (!isOpen) { - // Cancel the process if dialog is closed - setPodcastDialogOpen(false); - setSelectedChats([]); - setSelectionMode(false); - setCurrentChatIndex(0); - setPodcastTitles({}); - setProcessingChat(null); - setPodcastTitle(""); - } else { - setPodcastDialogOpen(true); - } - }} - > - - - - - - Generate Podcast {currentChatIndex + 1} of {selectedChats.length} - - - - {selectedChats.length > 1 ? ( - <> - Creating individual podcasts for each selected chat. Currently processing:{" "} - - {processingChat?.title || `Chat ${selectedChats[currentChatIndex]}`} - - - ) : ( - "Create a podcast from this chat. The podcast will be available in the podcasts section once generated." - )} - - - -
-
- - updateCurrentChatTitle(e.target.value)} - /> -
- - {selectedChats.length > 1 && ( -
-
-
- )} -
- - - {selectedChats.length > 1 && !isGeneratingPodcast && ( - - )} - - - -
-
); } diff --git a/surfsense_web/components/announcement-banner.tsx b/surfsense_web/components/announcement-banner.tsx index 673bda51d..c8ac05def 100644 --- a/surfsense_web/components/announcement-banner.tsx +++ b/surfsense_web/components/announcement-banner.tsx @@ -1,13 +1,18 @@ "use client"; +import { useAtom } from "jotai"; import { ExternalLink, Info, X } from "lucide-react"; -import { useState } from "react"; import { Button } from "@/components/ui/button"; +import { announcementDismissedAtom } from "@/stores/announcement.atom"; export function AnnouncementBanner() { - const [isVisible, setIsVisible] = useState(true); + const [isDismissed, setIsDismissed] = useAtom(announcementDismissedAtom); - if (!isVisible) return null; + const handleDismiss = () => { + setIsDismissed(true); + }; + + if (isDismissed) return null; return (
@@ -30,7 +35,7 @@ export function AnnouncementBanner() { variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-blue-100 hover:text-white hover:bg-blue-700/50 dark:hover:bg-blue-800/50 absolute right-4" - onClick={() => setIsVisible(false)} + onClick={handleDismiss} > Dismiss diff --git a/surfsense_web/stores/announcement.atom.ts b/surfsense_web/stores/announcement.atom.ts new file mode 100644 index 000000000..31e032978 --- /dev/null +++ b/surfsense_web/stores/announcement.atom.ts @@ -0,0 +1,5 @@ +import { atomWithStorage } from "jotai/utils"; + +// Atom to track whether the announcement banner has been dismissed +// Persists to localStorage automatically +export const announcementDismissedAtom = atomWithStorage("surfsense_announcement_dismissed", false);