From bc512ec08980da8195f814fde7acbd05c38fae82 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 22 Oct 2025 14:41:47 +0200 Subject: [PATCH 01/35] Add chat pannel --- .../components/chat/ChatInterface.tsx | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index acd22ad9e..f51b37b57 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -1,9 +1,13 @@ "use client"; import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui"; +import { PanelRight } from "lucide-react"; +import { useState } from "react"; +import type { ResearchMode } from "@/components/chat"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; import type { Document } from "@/hooks/use-documents"; +import { cn } from "@/lib/utils"; interface ChatInterfaceProps { handler: ChatHandler; @@ -24,19 +28,46 @@ export default function ChatInterface({ searchMode, onSearchModeChange, }: ChatInterfaceProps) { + const [isChatPannelOpen, setIsChatPannelOpen] = useState(false); + return ( -
- -
- +
+
+ +
+ +
+
+
+
+ +
+ +
Chat pannel
From 9085cb2d27312e092157b9bad50f1eaa47ba18b3 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 22 Oct 2025 14:43:51 +0200 Subject: [PATCH 02/35] add transition on interaction --- surfsense_web/components/chat/ChatInterface.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index f51b37b57..082e1ea44 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -48,13 +48,13 @@ export default function ChatInterface({
From 70dd7f1cd7714f4f371c773ba9e62df64a8f4cce Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 22 Oct 2025 14:51:13 +0200 Subject: [PATCH 03/35] add chat pannel container --- .../components/chat/ChatPannel/ChatPannelContainer.tsx | 3 +++ surfsense_web/package.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx diff --git a/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx b/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx new file mode 100644 index 000000000..f3f2ae9f1 --- /dev/null +++ b/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx @@ -0,0 +1,3 @@ +export function ChatPanelContainer() { + return
Chat Panel container
; +} diff --git a/surfsense_web/package.json b/surfsense_web/package.json index 61e5bfd80..678ca5db2 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -16,7 +16,8 @@ "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "format:fix": "npx @biomejs/biome check --fix" }, "dependencies": { "@ai-sdk/react": "^1.2.12", From 37bf3445b052a6bf593b17071cfc148e6c6f7140 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 22 Oct 2025 14:52:17 +0200 Subject: [PATCH 04/35] add chat pannel view --- surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx new file mode 100644 index 000000000..f15fce818 --- /dev/null +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx @@ -0,0 +1,3 @@ +export default function ChatPanelView() { + return
Chat Panel View
; +} From 24e366d3266fc5c1be7047a5833ff3eebb2e16f1 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 22 Oct 2025 15:14:38 +0200 Subject: [PATCH 05/35] add 'generate podcast component' --- .../components/chat/ChatInterface.tsx | 5 ++++- .../chat/ChatPannel/ChatPanelView.tsx | 21 ++++++++++++++++++- .../chat/ChatPannel/ChatPannelContainer.tsx | 4 +++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 082e1ea44..4aaeb2847 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -8,6 +8,7 @@ import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; import type { Document } from "@/hooks/use-documents"; import { cn } from "@/lib/utils"; +import { ChatPanelContainer } from "./ChatPannel/ChatPannelContainer"; interface ChatInterfaceProps { handler: ChatHandler; @@ -67,7 +68,9 @@ export default function ChatInterface({
-
Chat pannel
+
+ +
diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx index f15fce818..5f4eb0405 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx @@ -1,3 +1,22 @@ +import { Pencil, Podcast } from "lucide-react"; + export default function ChatPanelView() { - return
Chat Panel View
; + return ( +
+
+
+
+ + +
+

Generate Podcast

+
+
+
+ ); } diff --git a/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx b/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx index f3f2ae9f1..7df13a1d2 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx @@ -1,3 +1,5 @@ +import ChatPanelView from "./ChatPanelView"; + export function ChatPanelContainer() { - return
Chat Panel container
; + return ; } From 34c07d369983bd14d70c9986225f0180d1253726 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 22 Oct 2025 15:35:44 +0200 Subject: [PATCH 06/35] add accessibilty --- .../components/chat/ChatInterface.tsx | 82 +++++++++++-------- .../chat/ChatPannel/ChatPanelView.tsx | 52 +++++++++--- 2 files changed, 88 insertions(+), 46 deletions(-) diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 4aaeb2847..f94c2b3ee 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -2,7 +2,7 @@ import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui"; import { PanelRight } from "lucide-react"; -import { useState } from "react"; +import { createContext, useState } from "react"; import type { ResearchMode } from "@/components/chat"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; @@ -20,6 +20,13 @@ interface ChatInterfaceProps { onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; } +interface ChatInterfaceContext { + isChatPannelOpen: boolean; + setIsChatPannelOpen: (value: boolean) => void; +} + +export const chatInterfaceContext = createContext(null); + export default function ChatInterface({ handler, onDocumentSelectionChange, @@ -30,49 +37,56 @@ export default function ChatInterface({ onSearchModeChange, }: ChatInterfaceProps) { const [isChatPannelOpen, setIsChatPannelOpen] = useState(false); + const contextValue = { + isChatPannelOpen, + setIsChatPannelOpen, + }; return ( - -
-
- -
- + + +
+
+ +
+ +
-
-
- -
+ +
-
- +
+ +
-
- + + ); } diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx index 5f4eb0405..995c4d3a7 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx @@ -1,21 +1,49 @@ +"use client"; + import { Pencil, Podcast } from "lucide-react"; +import { useContext } from "react"; +import { cn } from "@/lib/utils"; +import { chatInterfaceContext } from "../ChatInterface"; export default function ChatPanelView() { + const context = useContext(chatInterfaceContext); + if (!context) { + throw new Error("chatInterfaceContext must be used within a ChatProvider"); + } + + const { isChatPannelOpen, setIsChatPannelOpen } = context; + return (
-
-
-
- - +
+ {isChatPannelOpen ? ( +
+
+ + +
+

Generate Podcast

-

Generate Podcast

-
+ ) : ( + + )}
); From 6d34007a2622d36a11b9129c76fb6d436bc54002 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 22 Oct 2025 15:39:29 +0200 Subject: [PATCH 07/35] add white mode --- surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx index 995c4d3a7..c03f6fb7d 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx @@ -22,7 +22,7 @@ export default function ChatPanelView() { )} > {isChatPannelOpen ? ( -
+
-
+ {chatId ? (
- -
+ +
-
- +
+ +
-
+ ) : null}
diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx index c03f6fb7d..1835a5677 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx @@ -4,8 +4,13 @@ import { Pencil, Podcast } from "lucide-react"; import { useContext } from "react"; import { cn } from "@/lib/utils"; import { chatInterfaceContext } from "../ChatInterface"; +import { ConfigModal } from "./ConfigModal"; -export default function ChatPanelView() { +interface ChatPanelViewProps { + chatId: string; +} + +export default function ChatPanelView({ chatId }: ChatPanelViewProps) { const context = useContext(chatInterfaceContext); if (!context) { throw new Error("chatInterfaceContext must be used within a ChatProvider"); @@ -22,16 +27,10 @@ export default function ChatPanelView() { )} > {isChatPannelOpen ? ( -
+
- +

Generate Podcast

diff --git a/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx b/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx index 7df13a1d2..881000a67 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx @@ -1,5 +1,9 @@ import ChatPanelView from "./ChatPanelView"; -export function ChatPanelContainer() { - return ; +interface ChatPanelContainerProps { + chatId: string; +} + +export function ChatPanelContainer({ chatId }: ChatPanelContainerProps) { + return ; } diff --git a/surfsense_web/components/chat/ChatPannel/ConfigModal.tsx b/surfsense_web/components/chat/ChatPannel/ConfigModal.tsx new file mode 100644 index 000000000..4c005e52e --- /dev/null +++ b/surfsense_web/components/chat/ChatPannel/ConfigModal.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Pencil } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; + +export function ConfigModal() { + return ( + + + + + +
+ + + + + +
+
+
+ ); +} From ce658b91ea555e1c4a4a309c98900a0bc38c13b9 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 01:28:07 +0200 Subject: [PATCH 09/35] Add sserver actions --- .../components/chat/ChatInterface.tsx | 36 ++------------ .../chat/ChatPannel/ChatPanelContainer.tsx | 49 +++++++++++++++++++ .../chat/ChatPannel/ChatPanelView.tsx | 4 +- .../chat/ChatPannel/ChatPannelContainer.tsx | 9 ---- .../components/chat/ChatPannel/actions.ts | 15 ++++++ 5 files changed, 71 insertions(+), 42 deletions(-) create mode 100644 surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx delete mode 100644 surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx create mode 100644 surfsense_web/components/chat/ChatPannel/actions.ts diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 72fddfff9..79874ad67 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -8,8 +8,7 @@ import type { ResearchMode } from "@/components/chat"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; import type { Document } from "@/hooks/use-documents"; -import { cn } from "@/lib/utils"; -import { ChatPanelContainer } from "./ChatPannel/ChatPannelContainer"; +import { ChatPanelContainer } from "./ChatPannel/ChatPanelContainer"; interface ChatInterfaceProps { handler: ChatHandler; @@ -24,6 +23,7 @@ interface ChatInterfaceProps { interface ChatInterfaceContext { isChatPannelOpen: boolean; setIsChatPannelOpen: (value: boolean) => void; + chat_id: string; } export const chatInterfaceContext = createContext(null); @@ -37,14 +37,14 @@ export default function ChatInterface({ searchMode, onSearchModeChange, }: ChatInterfaceProps) { + const { chat_id } = useParams(); const [isChatPannelOpen, setIsChatPannelOpen] = useState(false); const contextValue = { isChatPannelOpen, setIsChatPannelOpen, + chat_id: typeof chat_id === "string" ? chat_id : chat_id ? chat_id[0] : "", }; - const { chat_id: chatId } = useParams(); - return ( @@ -63,33 +63,7 @@ export default function ChatInterface({ />
- {chatId ? ( -
-
- -
- -
- -
-
- ) : null} +
diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx new file mode 100644 index 000000000..38be9c060 --- /dev/null +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx @@ -0,0 +1,49 @@ +import { PanelRight } from "lucide-react"; +import { useContext } from "react"; +import { cn } from "@/lib/utils"; +import { chatInterfaceContext } from "../ChatInterface"; +import { generatePodCastAction, getChatPodcastPromise } from "./actions"; +import { ChatPanelView } from "./ChatPanelView"; + +export interface PodCastInterface { + title: string; + podcast_transcript: string; + search_space_id: string; +} + +export function ChatPanelContainer() { + const context = useContext(chatInterfaceContext); + + if (!context) { + throw new Error("chatInterfaceContext must be used within a ChatProvider"); + } + + const { isChatPannelOpen, setIsChatPannelOpen, chat_id: chatId } = context; + return chatId && chatId !== "" ? ( +
+
+ +
+ +
+ +
+
+ ) : null; +} diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx index 1835a5677..575d2bf65 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx @@ -7,10 +7,10 @@ import { chatInterfaceContext } from "../ChatInterface"; import { ConfigModal } from "./ConfigModal"; interface ChatPanelViewProps { - chatId: string; + chat_id: string; } -export default function ChatPanelView({ chatId }: ChatPanelViewProps) { +export function ChatPanelView({ chat_id: chatId }: ChatPanelViewProps) { const context = useContext(chatInterfaceContext); if (!context) { throw new Error("chatInterfaceContext must be used within a ChatProvider"); diff --git a/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx b/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx deleted file mode 100644 index 881000a67..000000000 --- a/surfsense_web/components/chat/ChatPannel/ChatPannelContainer.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ChatPanelView from "./ChatPanelView"; - -interface ChatPanelContainerProps { - chatId: string; -} - -export function ChatPanelContainer({ chatId }: ChatPanelContainerProps) { - return ; -} diff --git a/surfsense_web/components/chat/ChatPannel/actions.ts b/surfsense_web/components/chat/ChatPannel/actions.ts new file mode 100644 index 000000000..af84f2806 --- /dev/null +++ b/surfsense_web/components/chat/ChatPannel/actions.ts @@ -0,0 +1,15 @@ +"use server"; + +import type { PodCastInterface } from "./ChatPanelContainer"; + +export const generatePodCastAction = async (formData: { prompt: string; chatId: string }) => { + console.log("Generating podcast"); +}; + +export const getChatPodcastPromise = async (chatId: string): Promise => { + return Promise.resolve({ + title: "Test", + podcast_transcript: "Test", + search_space_id: "Test", + }); +}; From 05a8bd1be340ac93cacc2c4e95261588b54d6128 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 16:43:28 +0200 Subject: [PATCH 10/35] add generate podcast action --- .../chat/ChatPannel/ChatPanelContainer.tsx | 18 +++++++++++++++++- .../components/chat/ChatPannel/actions.ts | 12 +++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx index 38be9c060..aebdca29e 100644 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx +++ b/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx @@ -1,5 +1,5 @@ import { PanelRight } from "lucide-react"; -import { useContext } from "react"; +import { useActionState, useContext } from "react"; import { cn } from "@/lib/utils"; import { chatInterfaceContext } from "../ChatInterface"; import { generatePodCastAction, getChatPodcastPromise } from "./actions"; @@ -11,6 +11,15 @@ export interface PodCastInterface { search_space_id: string; } +export type PodcastGenerationState = Partial<{ + title: string; + podcast_transcript: string; + search_space_id: string; + chat_id: string; + prompt: string; + error: unknown; +}>; + export function ChatPanelContainer() { const context = useContext(chatInterfaceContext); @@ -19,6 +28,13 @@ export function ChatPanelContainer() { } const { isChatPannelOpen, setIsChatPannelOpen, chat_id: chatId } = context; + + const [state, generatePodcastAction, isGeneratingPodcast] = + useActionState(generatePodCastAction, { + chat_id: chatId, + prompt: "Test", + }); + return chatId && chatId !== "" ? (
{ - console.log("Generating podcast"); +export const generatePodCastAction = async ( + formData: PodcastGenerationState +): Promise => { + return Promise.resolve({ + title: "Test", + podcast_transcript: "Test", + search_space_id: "Test", + }); }; export const getChatPodcastPromise = async (chatId: string): Promise => { From d2f00fd328fd9ce9edadcb2b19866a8c41ec5abc Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 19:26:41 +0200 Subject: [PATCH 11/35] fix fetchChatDetails return type --- surfsense_web/hooks/use-chat.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts index 95ec3a374..77c33b386 100644 --- a/surfsense_web/hooks/use-chat.ts +++ b/surfsense_web/hooks/use-chat.ts @@ -1,5 +1,6 @@ import type { Message } from "@ai-sdk/react"; import { useCallback, useEffect, useState } from "react"; +import { Chat, type ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { ResearchMode } from "@/components/chat"; import type { Document } from "@/hooks/use-documents"; @@ -49,7 +50,7 @@ interface UseChatAPIProps { export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { const fetchChatDetails = useCallback( - async (chatId: string) => { + async (chatId: string): Promise => { if (!token) return null; try { From 25dfcaee27993c1afee7d8f93abf40e268d97b2c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 19:28:51 +0200 Subject: [PATCH 12/35] fix fetchChatDetails return type --- surfsense_web/hooks/use-chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts index 77c33b386..fdd447a10 100644 --- a/surfsense_web/hooks/use-chat.ts +++ b/surfsense_web/hooks/use-chat.ts @@ -1,6 +1,6 @@ import type { Message } from "@ai-sdk/react"; import { useCallback, useEffect, useState } from "react"; -import { Chat, type ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; +import { type ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { ResearchMode } from "@/components/chat"; import type { Document } from "@/hooks/use-documents"; From 4f36b230bdb9d4a403de950524a3f733e4372975 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 19:30:17 +0200 Subject: [PATCH 13/35] get chat details on chat interface mount --- .../components/chat/ChatInterface.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 79874ad67..b5bbd49d7 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -3,10 +3,13 @@ import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui"; import { PanelRight } from "lucide-react"; import { useParams } from "next/navigation"; -import { createContext, useState } from "react"; +import { createContext, useCallback, useEffect, useState } from "react"; +import { Chat, type ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; +import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/podcasts-client"; import type { ResearchMode } from "@/components/chat"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; +import { useChatAPI } from "@/hooks/use-chat"; import type { Document } from "@/hooks/use-documents"; import { ChatPanelContainer } from "./ChatPannel/ChatPanelContainer"; @@ -24,6 +27,9 @@ interface ChatInterfaceContext { isChatPannelOpen: boolean; setIsChatPannelOpen: (value: boolean) => void; chat_id: string; + podcast: PodcastItem | null; + setPodcast: (value: PodcastItem) => void; + chatDetails: ChatDetails | null; } export const chatInterfaceContext = createContext(null); @@ -37,14 +43,38 @@ export default function ChatInterface({ searchMode, onSearchModeChange, }: ChatInterfaceProps) { - const { chat_id } = useParams(); + const { chat_id, search_space_id } = useParams(); + const [chatDetails, setChatDetails] = useState(null); const [isChatPannelOpen, setIsChatPannelOpen] = useState(false); + const [podcast, setPodcast] = useState(null); const contextValue = { isChatPannelOpen, setIsChatPannelOpen, chat_id: typeof chat_id === "string" ? chat_id : chat_id ? chat_id[0] : "", + podcast, + setPodcast, + chatDetails, }; + const { fetchChatDetails } = useChatAPI({ + token: localStorage.getItem("surfsense_bearer_token"), + search_space_id: search_space_id as string, + }); + + const getChat = useCallback( + async (id: string) => { + const chat = await fetchChatDetails(id); + setChatDetails(chat); + }, + [fetchChatDetails] + ); + + useEffect(() => { + const id = typeof chat_id === "string" ? chat_id : chat_id ? chat_id[0] : ""; + if (!id) return; + getChat(id); + }, [chat_id, search_space_id]); + return ( From caae2bcfc06611616e6c983cced60811e25e2305 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 19:32:41 +0200 Subject: [PATCH 14/35] implement generate podcast handler --- .../chat/ChatPanel/ChatPanelContainer.tsx | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx new file mode 100644 index 000000000..75481dc3a --- /dev/null +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx @@ -0,0 +1,78 @@ +import { PanelRight } from "lucide-react"; +import { useActionState, useContext, useTransition } from "react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { chatInterfaceContext } from "../ChatInterface"; +import type { GeneratePodcastRequest } from "./actions"; +import { ChatPanelView } from "./ChatPanelView"; + +export function ChatPanelContainer() { + const context = useContext(chatInterfaceContext); + + if (!context) { + throw new Error("chatInterfaceContext must be used within a ChatProvider"); + } + + const { isChatPannelOpen, setIsChatPannelOpen, chat_id: chatId, setPodcast } = context; + + const generatePodcast = async (request: GeneratePodcastRequest) => { + try { + const { podcast_title = "SurfSense Podcast" } = request; + + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`, + { + method: "POST", + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...request, podcast_title }), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to generate podcast"); + } + + const result = await response.json(); + + setPodcast(result); + + toast.success(`Podcast generation started!`); + } catch (error) { + console.error("Error generating podcast:", error); + console.log(error); + } finally { + } + }; + + return chatId && chatId !== "" ? ( +
+
+ +
+ +
+ +
+
+ ) : null; +} From f16562685e628d48662f8c3bcab6984b0d5750d2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 19:34:03 +0200 Subject: [PATCH 15/35] update chat panel view --- .../chat/ChatPanel/ChatPanelView.tsx | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx new file mode 100644 index 000000000..0d2b86ab8 --- /dev/null +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Pencil, Podcast } from "lucide-react"; +import { useContext, useTransition } from "react"; +import { cn } from "@/lib/utils"; +import { chatInterfaceContext } from "../ChatInterface"; +import type { GeneratePodcastRequest } from "./actions"; +import { ConfigModal } from "./ConfigModal"; + +interface ChatPanelViewProps { + generatePodcast: (request: GeneratePodcastRequest) => Promise; +} + +export function ChatPanelView(props: ChatPanelViewProps) { + const context = useContext(chatInterfaceContext); + if (!context) { + throw new Error("chatInterfaceContext must be used within a ChatProvider"); + } + + const { isChatPannelOpen, setIsChatPannelOpen } = context; + const { generatePodcast } = props; + + const [isGeneratingPodcast, startGeneratingPodcast] = useTransition(); + + const handleGeneratePodcast = () => { + startGeneratingPodcast(() => { + generatePodcast({ + type: "CHAT", + ids: [1], + search_space_id: 1, + }); + }); + }; + + return ( +
+
+ {isChatPannelOpen ? ( +
+
+ + +
+

Generate Podcast

+
+ ) : ( + + )} +
+
+ ); +} From 58fed4656754938c73c0354a5d6e42acf7c44044 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 19:35:52 +0200 Subject: [PATCH 16/35] updae chat config madal --- .../components/chat/ChatPanel/ConfigModal.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 surfsense_web/components/chat/ChatPanel/ConfigModal.tsx diff --git a/surfsense_web/components/chat/ChatPanel/ConfigModal.tsx b/surfsense_web/components/chat/ChatPanel/ConfigModal.tsx new file mode 100644 index 000000000..77b617f5f --- /dev/null +++ b/surfsense_web/components/chat/ChatPanel/ConfigModal.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { Pencil } from "lucide-react"; +import { useCallback, useContext } from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { chatInterfaceContext } from "../ChatInterface"; +import type { GeneratePodcastRequest } from "./actions"; + +interface ConfigModalProps { + generatePodcast: (request: GeneratePodcastRequest) => Promise; +} + +export function ConfigModal(props: ConfigModalProps) { + const context = useContext(chatInterfaceContext); + + if (!context) { + throw new Error("chatInterfaceContext must be used within a ChatProvider"); + } + const { chatDetails } = context; + const { generatePodcast } = props; + + const handleGeneratePost = useCallback(async () => { + if (!chatDetails) return; + await generatePodcast({ + type: "CHAT", + ids: [chatDetails.id], + search_space_id: chatDetails.search_space_id, + podcast_title: chatDetails.title, + }); + }, [chatDetails]); + return ( + + + + + +
+ + + + + +
+
+
+ ); +} From 9007436ff67496d7a6be0f9b70ee0861e698f417 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 19:48:10 +0200 Subject: [PATCH 17/35] fix typo --- .../[search_space_id]/chats/chats-client.tsx | 14 +++- .../podcasts/podcasts-client.tsx | 2 +- .../components/chat/ChatInterface.tsx | 5 +- .../chat/ChatPanel/ChatPanelContainer.tsx | 8 ++- .../chat/ChatPannel/ChatPanelContainer.tsx | 65 ------------------- .../chat/ChatPannel/ChatPanelView.tsx | 49 -------------- .../chat/ChatPannel/ConfigModal.tsx | 40 ------------ .../components/chat/ChatPannel/actions.ts | 21 ------ surfsense_web/hooks/use-chat.ts | 2 +- 9 files changed, 23 insertions(+), 183 deletions(-) delete mode 100644 surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx delete mode 100644 surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx delete mode 100644 surfsense_web/components/chat/ChatPannel/ConfigModal.tsx delete mode 100644 surfsense_web/components/chat/ChatPannel/actions.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 cb58acf27..4a9b0777d 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 @@ -56,14 +56,24 @@ import { } from "@/components/ui/select"; import { cn } from "@/lib/utils"; -interface Chat { +export interface Chat { created_at: string; id: number; - type: string; + type: "DOCUMENT" | "CHAT"; title: string; search_space_id: number; } +export interface ChatDetails { + type: "DOCUMENT" | "CHAT"; + title: string; + initial_connectors: string[]; + messages: any[]; + created_at: string; + id: number; + search_space_id: number; +} + interface ChatsPageClientProps { searchSpaceId: string; } diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx index 3fa6db3ff..b8d867218 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx @@ -47,7 +47,7 @@ import { } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; -interface PodcastItem { +export interface PodcastItem { id: number; title: string; created_at: string; diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index b5bbd49d7..0c2d7c5a7 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -1,17 +1,16 @@ "use client"; import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui"; -import { PanelRight } from "lucide-react"; import { useParams } from "next/navigation"; import { createContext, useCallback, useEffect, useState } from "react"; -import { Chat, type ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; +import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/podcasts-client"; import type { ResearchMode } from "@/components/chat"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; import { useChatAPI } from "@/hooks/use-chat"; import type { Document } from "@/hooks/use-documents"; -import { ChatPanelContainer } from "./ChatPannel/ChatPanelContainer"; +import { ChatPanelContainer } from "./ChatPanel/ChatPanelContainer"; interface ChatInterfaceProps { handler: ChatHandler; diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx index 75481dc3a..2f460ee31 100644 --- a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx @@ -3,9 +3,15 @@ import { useActionState, useContext, useTransition } from "react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { chatInterfaceContext } from "../ChatInterface"; -import type { GeneratePodcastRequest } from "./actions"; import { ChatPanelView } from "./ChatPanelView"; +export interface GeneratePodcastRequest { + type: "CHAT" | "DOCUMENT"; + ids: number[]; + search_space_id: number; + podcast_title?: string; +} + export function ChatPanelContainer() { const context = useContext(chatInterfaceContext); diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx deleted file mode 100644 index aebdca29e..000000000 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelContainer.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { PanelRight } from "lucide-react"; -import { useActionState, useContext } from "react"; -import { cn } from "@/lib/utils"; -import { chatInterfaceContext } from "../ChatInterface"; -import { generatePodCastAction, getChatPodcastPromise } from "./actions"; -import { ChatPanelView } from "./ChatPanelView"; - -export interface PodCastInterface { - title: string; - podcast_transcript: string; - search_space_id: string; -} - -export type PodcastGenerationState = Partial<{ - title: string; - podcast_transcript: string; - search_space_id: string; - chat_id: string; - prompt: string; - error: unknown; -}>; - -export function ChatPanelContainer() { - const context = useContext(chatInterfaceContext); - - if (!context) { - throw new Error("chatInterfaceContext must be used within a ChatProvider"); - } - - const { isChatPannelOpen, setIsChatPannelOpen, chat_id: chatId } = context; - - const [state, generatePodcastAction, isGeneratingPodcast] = - useActionState(generatePodCastAction, { - chat_id: chatId, - prompt: "Test", - }); - - return chatId && chatId !== "" ? ( -
-
- -
- -
- -
-
- ) : null; -} diff --git a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx deleted file mode 100644 index 575d2bf65..000000000 --- a/surfsense_web/components/chat/ChatPannel/ChatPanelView.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { Pencil, Podcast } from "lucide-react"; -import { useContext } from "react"; -import { cn } from "@/lib/utils"; -import { chatInterfaceContext } from "../ChatInterface"; -import { ConfigModal } from "./ConfigModal"; - -interface ChatPanelViewProps { - chat_id: string; -} - -export function ChatPanelView({ chat_id: chatId }: ChatPanelViewProps) { - const context = useContext(chatInterfaceContext); - if (!context) { - throw new Error("chatInterfaceContext must be used within a ChatProvider"); - } - - const { isChatPannelOpen, setIsChatPannelOpen } = context; - - return ( -
-
- {isChatPannelOpen ? ( -
-
- - -
-

Generate Podcast

-
- ) : ( - - )} -
-
- ); -} diff --git a/surfsense_web/components/chat/ChatPannel/ConfigModal.tsx b/surfsense_web/components/chat/ChatPannel/ConfigModal.tsx deleted file mode 100644 index 4c005e52e..000000000 --- a/surfsense_web/components/chat/ChatPannel/ConfigModal.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import { Pencil } from "lucide-react"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; - -export function ConfigModal() { - return ( - - - - - -
- - - - - -
-
-
- ); -} diff --git a/surfsense_web/components/chat/ChatPannel/actions.ts b/surfsense_web/components/chat/ChatPannel/actions.ts deleted file mode 100644 index 71bd23a95..000000000 --- a/surfsense_web/components/chat/ChatPannel/actions.ts +++ /dev/null @@ -1,21 +0,0 @@ -"use server"; - -import type { PodCastInterface, PodcastGenerationState } from "./ChatPanelContainer"; - -export const generatePodCastAction = async ( - formData: PodcastGenerationState -): Promise => { - return Promise.resolve({ - title: "Test", - podcast_transcript: "Test", - search_space_id: "Test", - }); -}; - -export const getChatPodcastPromise = async (chatId: string): Promise => { - return Promise.resolve({ - title: "Test", - podcast_transcript: "Test", - search_space_id: "Test", - }); -}; diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts index fdd447a10..4e094cb31 100644 --- a/surfsense_web/hooks/use-chat.ts +++ b/surfsense_web/hooks/use-chat.ts @@ -1,6 +1,6 @@ import type { Message } from "@ai-sdk/react"; import { useCallback, useEffect, useState } from "react"; -import { type ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; +import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { ResearchMode } from "@/components/chat"; import type { Document } from "@/hooks/use-documents"; From e47b1cb4c53caa5074eabc359cf8404d14fc2850 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 21:42:39 +0200 Subject: [PATCH 18/35] stop propagation in the chat panelcl --- .../components/chat/ChatInterface.tsx | 2 -- .../chat/ChatPanel/ChatPanelContainer.tsx | 6 +--- .../chat/ChatPanel/ChatPanelView.tsx | 35 ++++++++++--------- .../components/chat/ChatPanel/ConfigModal.tsx | 27 +++++++------- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 0c2d7c5a7..d0d37fa04 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -26,8 +26,6 @@ interface ChatInterfaceContext { isChatPannelOpen: boolean; setIsChatPannelOpen: (value: boolean) => void; chat_id: string; - podcast: PodcastItem | null; - setPodcast: (value: PodcastItem) => void; chatDetails: ChatDetails | null; } diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx index 2f460ee31..66dbc088a 100644 --- a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx @@ -19,7 +19,7 @@ export function ChatPanelContainer() { throw new Error("chatInterfaceContext must be used within a ChatProvider"); } - const { isChatPannelOpen, setIsChatPannelOpen, chat_id: chatId, setPodcast } = context; + const { isChatPannelOpen, setIsChatPannelOpen, chat_id: chatId } = context; const generatePodcast = async (request: GeneratePodcastRequest) => { try { @@ -42,10 +42,6 @@ export function ChatPanelContainer() { throw new Error(errorData.detail || "Failed to generate podcast"); } - const result = await response.json(); - - setPodcast(result); - toast.success(`Podcast generation started!`); } catch (error) { console.error("Error generating podcast:", error); diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx index 0d2b86ab8..0521e070a 100644 --- a/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx @@ -1,10 +1,10 @@ "use client"; import { Pencil, Podcast } from "lucide-react"; -import { useContext, useTransition } from "react"; +import { useCallback, useContext, useTransition } from "react"; import { cn } from "@/lib/utils"; import { chatInterfaceContext } from "../ChatInterface"; -import type { GeneratePodcastRequest } from "./actions"; +import type { GeneratePodcastRequest } from "./ChatPanelContainer"; import { ConfigModal } from "./ConfigModal"; interface ChatPanelViewProps { @@ -17,37 +17,40 @@ export function ChatPanelView(props: ChatPanelViewProps) { throw new Error("chatInterfaceContext must be used within a ChatProvider"); } - const { isChatPannelOpen, setIsChatPannelOpen } = context; + const { isChatPannelOpen, setIsChatPannelOpen, chatDetails } = context; const { generatePodcast } = props; - const [isGeneratingPodcast, startGeneratingPodcast] = useTransition(); - - const handleGeneratePodcast = () => { - startGeneratingPodcast(() => { - generatePodcast({ - type: "CHAT", - ids: [1], - search_space_id: 1, - }); + const handleGeneratePost = useCallback(async () => { + if (!chatDetails) return; + await generatePodcast({ + type: "CHAT", + ids: [chatDetails.id], + search_space_id: chatDetails.search_space_id, + podcast_title: chatDetails.title, }); - }; + }, [chatDetails]); return (
{isChatPannelOpen ? ( -
+
+ ) : ( + e.stopPropagation()} + > +
@@ -48,8 +48,9 @@ export function ConfigModal(props: ConfigModalProps) { +
+ {/* Show stale podcast warning if applicable */} + {podcastIsStale && ( +
+
+ +
+

Podcast is outdated

+

+ {getPodcastStalenessMessage( + chatDetails?.state_version || 0, + podcast?.chat_state_version + )} +

+
+
+
+ )} + + {/* Generate/Regenerate button */} + +
) : ( )}
diff --git a/surfsense_web/components/chat/PodcastUtils.ts b/surfsense_web/components/chat/PodcastUtils.ts new file mode 100644 index 000000000..5662f96b5 --- /dev/null +++ b/surfsense_web/components/chat/PodcastUtils.ts @@ -0,0 +1,43 @@ +/** + * Determines if a podcast is stale compared to the current chat state. + * A podcast is considered stale if: + * - The chat's current state_version is greater than the podcast's chat_state_version + * + * @param chatVersion - The current state_version of the chat + * @param podcastVersion - The chat_state_version stored when the podcast was generated (nullable) + * @returns true if the podcast is stale, false otherwise + */ +export function isPodcastStale( + chatVersion: number, + podcastVersion: number | null | undefined +): boolean { + // If podcast has no version, it's stale (generated before this feature) + if (!podcastVersion) { + return true; + } + // If chat version is greater than podcast version, it's stale : We can change this condition to consider staleness after a huge number of updates + return chatVersion > podcastVersion; +} + +/** + * Gets a human-readable message about podcast staleness + * + * @param chatVersion - The current state_version of the chat + * @param podcastVersion - The chat_state_version stored when the podcast was generated + * @returns A descriptive message about the podcast's staleness status + */ +export function getPodcastStalenessMessage( + chatVersion: number, + podcastVersion: number | null | undefined +): string { + if (!podcastVersion) { + return "This podcast was generated before chat updates were tracked. Consider regenerating it."; + } + + if (chatVersion > podcastVersion) { + const versionDiff = chatVersion - podcastVersion; + return `This podcast is outdated. The chat has been updated ${versionDiff} time${versionDiff > 1 ? "s" : ""} since this podcast was generated.`; + } + + return "This podcast is up to date with the current chat."; +} From aaa6ee22ed25675f2d6690b829d6c3ab8f1daef3 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 22:29:31 +0200 Subject: [PATCH 20/35] implement chat/podcast staleness --- .../32_add_podcast_staleness_detection.py | 42 +++++++++++++++++++ surfsense_backend/app/db.py | 3 ++ surfsense_backend/app/routes/chats_routes.py | 3 ++ surfsense_backend/app/schemas/chats.py | 2 + surfsense_backend/app/schemas/podcasts.py | 1 + surfsense_backend/app/tasks/podcast_tasks.py | 1 + 6 files changed, 52 insertions(+) create mode 100644 surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py diff --git a/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py b/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py new file mode 100644 index 000000000..79eab4097 --- /dev/null +++ b/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py @@ -0,0 +1,42 @@ +"""Add podcast staleness detection columns + +Revision ID: 32 +Revises: 31 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers +revision: str = "32" +down_revision: str | None = "31" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Add state_version to chats table and chat_state_version to podcasts table.""" + + # Add state_version column to chats table with default value of 1 + op.add_column( + "chats", + sa.Column("state_version", sa.BigInteger(), nullable=False, server_default="1"), + ) + + # Add chat_state_version column to podcasts table (nullable, set when podcast is generated) + op.add_column( + "podcasts", sa.Column("chat_state_version", sa.BigInteger(), nullable=True) + ) + + +def downgrade() -> None: + """Remove state_version and chat_state_version columns.""" + + # Remove chat_state_version from podcasts table + op.drop_column("podcasts", "chat_state_version") + + # Remove state_version from chats table + op.drop_column("chats", "state_version") diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 12454c652..d17a0b797 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -9,6 +9,7 @@ from sqlalchemy import ( ARRAY, JSON, TIMESTAMP, + BigInteger, Boolean, Column, Enum as SQLAlchemyEnum, @@ -157,6 +158,7 @@ class Chat(BaseModel, TimestampMixin): title = Column(String, nullable=False, index=True) initial_connectors = Column(ARRAY(String), nullable=True) messages = Column(JSON, nullable=False) + state_version = Column(BigInteger, nullable=False, default=1) search_space_id = Column( Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False @@ -203,6 +205,7 @@ class Podcast(BaseModel, TimestampMixin): title = Column(String, nullable=False, index=True) podcast_transcript = Column(JSON, nullable=False, default={}) file_location = Column(String(500), nullable=False, default="") + chat_state_version = Column(BigInteger, nullable=True) search_space_id = Column( Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False diff --git a/surfsense_backend/app/routes/chats_routes.py b/surfsense_backend/app/routes/chats_routes.py index f77171167..eec5228f3 100644 --- a/surfsense_backend/app/routes/chats_routes.py +++ b/surfsense_backend/app/routes/chats_routes.py @@ -196,6 +196,7 @@ async def read_chats( Chat.initial_connectors, Chat.search_space_id, Chat.created_at, + Chat.state_version, ) .join(SearchSpace) .filter(SearchSpace.user_id == user.id) @@ -259,6 +260,8 @@ async def update_chat( update_data = chat_update.model_dump(exclude_unset=True) for key, value in update_data.items(): setattr(db_chat, key, value) + # Increment state_version when chat is modified + db_chat.state_version = (db_chat.state_version or 0) + 1 await session.commit() await session.refresh(db_chat) return db_chat diff --git a/surfsense_backend/app/schemas/chats.py b/surfsense_backend/app/schemas/chats.py index 59c274dc5..64ce73e9e 100644 --- a/surfsense_backend/app/schemas/chats.py +++ b/surfsense_backend/app/schemas/chats.py @@ -13,12 +13,14 @@ class ChatBase(BaseModel): initial_connectors: list[str] | None = None messages: list[Any] search_space_id: int + state_version: int = 1 class ChatBaseWithoutMessages(BaseModel): type: ChatType title: str search_space_id: int + state_version: int = 1 class ClientAttachment(BaseModel): diff --git a/surfsense_backend/app/schemas/podcasts.py b/surfsense_backend/app/schemas/podcasts.py index d86b3151c..66ad9df7b 100644 --- a/surfsense_backend/app/schemas/podcasts.py +++ b/surfsense_backend/app/schemas/podcasts.py @@ -10,6 +10,7 @@ class PodcastBase(BaseModel): podcast_transcript: list[Any] file_location: str = "" search_space_id: int + chat_state_version: int | None = None class PodcastCreate(PodcastBase): diff --git a/surfsense_backend/app/tasks/podcast_tasks.py b/surfsense_backend/app/tasks/podcast_tasks.py index e5f828ef2..3548eb043 100644 --- a/surfsense_backend/app/tasks/podcast_tasks.py +++ b/surfsense_backend/app/tasks/podcast_tasks.py @@ -144,6 +144,7 @@ async def generate_chat_podcast( podcast_transcript=serializable_transcript, file_location=result["final_podcast_file_path"], search_space_id=search_space_id, + chat_state_version=chat.state_version, ) # Add to session and commit From 666ea87a9dbae38114cb62c204eb0befd78eff9d Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 23:49:49 +0200 Subject: [PATCH 21/35] reference the chat in the podcast --- .../32_add_podcast_staleness_detection.py | 14 +++++++-- surfsense_backend/app/db.py | 3 ++ .../app/routes/podcasts_routes.py | 30 +++++++++++++++++++ surfsense_backend/app/tasks/podcast_tasks.py | 1 + 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py b/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py index 79eab4097..d63e6cba2 100644 --- a/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py +++ b/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py @@ -1,4 +1,6 @@ -"""Add podcast staleness detection columns +"""Add podcast staleness detection columns to chats and podcasts tables + +This feature allows the system to detect when a podcast is outdated compared to the current state of the chat it was generated from, enabling users to regenerate podcasts when needed. Revision ID: 32 Revises: 31 @@ -18,7 +20,7 @@ depends_on: str | Sequence[str] | None = None def upgrade() -> None: - """Add state_version to chats table and chat_state_version to podcasts table.""" + """Add state_version, chat_state_version, and chat_id to chats and podcasts tables.""" # Add state_version column to chats table with default value of 1 op.add_column( @@ -31,12 +33,18 @@ def upgrade() -> None: "podcasts", sa.Column("chat_state_version", sa.BigInteger(), nullable=True) ) + # Add chat_id column to podcasts table (nullable, set when podcast is generated from a chat) + op.add_column("podcasts", sa.Column("chat_id", sa.Integer(), nullable=True)) + def downgrade() -> None: - """Remove state_version and chat_state_version columns.""" + """Remove state_version, chat_state_version, and chat_id columns.""" # Remove chat_state_version from podcasts table op.drop_column("podcasts", "chat_state_version") + # Remove chat_id from podcasts table + op.drop_column("podcasts", "chat_id") + # Remove state_version from chats table op.drop_column("chats", "state_version") diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index d17a0b797..48154a417 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -205,6 +205,9 @@ class Podcast(BaseModel, TimestampMixin): title = Column(String, nullable=False, index=True) podcast_transcript = Column(JSON, nullable=False, default={}) file_location = Column(String(500), nullable=False, default="") + chat_id = Column( + Integer, ForeignKey("chats.id", ondelete="CASCADE"), nullable=True + ) # If generated from a chat, this will be the chat id, else null ( can be from a document or a chat ) chat_state_version = Column(BigInteger, nullable=True) search_space_id = Column( diff --git a/surfsense_backend/app/routes/podcasts_routes.py b/surfsense_backend/app/routes/podcasts_routes.py index e37bdd190..1772917eb 100644 --- a/surfsense_backend/app/routes/podcasts_routes.py +++ b/surfsense_backend/app/routes/podcasts_routes.py @@ -287,3 +287,33 @@ async def stream_podcast( raise HTTPException( status_code=500, detail=f"Error streaming podcast: {e!s}" ) from e + + +@router.get("/podcasts/by-chat/{chat_id}", response_model=PodcastRead) +async def get_podcast_by_chat_id( + chat_id: int, + session: AsyncSession = Depends(get_async_session), + user: User = Depends(current_active_user), +): + try: + # Get the podcast and check if user has access + result = await session.execute( + select(Podcast) + .join(SearchSpace) + .filter(Podcast.chat_id == chat_id, SearchSpace.user_id == user.id) + ) + podcast = result.scalars().first() + + if not podcast: + raise HTTPException( + status_code=404, + detail="Podcast not found or you don't have permission to access it", + ) + + return podcast + except HTTPException as he: + raise he + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error fetching podcast: {e!s}" + ) from e diff --git a/surfsense_backend/app/tasks/podcast_tasks.py b/surfsense_backend/app/tasks/podcast_tasks.py index 3548eb043..e333b4d1b 100644 --- a/surfsense_backend/app/tasks/podcast_tasks.py +++ b/surfsense_backend/app/tasks/podcast_tasks.py @@ -145,6 +145,7 @@ async def generate_chat_podcast( file_location=result["final_podcast_file_path"], search_space_id=search_space_id, chat_state_version=chat.state_version, + chat_id=chat.id, ) # Add to session and commit From 13342eb823a7007fdb58e6e4d54c77cea6d76b61 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 23 Oct 2025 23:50:45 +0200 Subject: [PATCH 22/35] Add use podcasts --- .../components/chat/ChatInterface.tsx | 12 ++++++++ .../components/chat/ChatPanel/ConfigModal.tsx | 2 +- surfsense_web/hooks/use-podcast.ts | 30 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/hooks/use-podcast.ts diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index b865c36e7..377d141b4 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -10,6 +10,7 @@ import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; import { useChatAPI } from "@/hooks/use-chat"; import type { Document } from "@/hooks/use-documents"; +import { usePodcast } from "@/hooks/use-podcast"; import { ChatPanelContainer } from "./ChatPanel/ChatPanelContainer"; interface ChatInterfaceProps { @@ -55,11 +56,21 @@ export default function ChatInterface({ chatDetails, }; + const { getPodcastByChatId } = usePodcast(); + const { fetchChatDetails } = useChatAPI({ token: localStorage.getItem("surfsense_bearer_token"), search_space_id: search_space_id as string, }); + const getPodcast = useCallback( + async (id: string) => { + const podcast = await getPodcastByChatId(Number(id)); + setPodcast(podcast); + }, + [getPodcastByChatId] + ); + const getChat = useCallback( async (id: string) => { const chat = await fetchChatDetails(id); @@ -72,6 +83,7 @@ export default function ChatInterface({ const id = typeof chat_id === "string" ? chat_id : chat_id ? chat_id[0] : ""; if (!id) return; getChat(id); + getPodcast(id); }, [chat_id, search_space_id]); return ( diff --git a/surfsense_web/components/chat/ChatPanel/ConfigModal.tsx b/surfsense_web/components/chat/ChatPanel/ConfigModal.tsx index 98b163212..d0caeab5e 100644 --- a/surfsense_web/components/chat/ChatPanel/ConfigModal.tsx +++ b/surfsense_web/components/chat/ChatPanel/ConfigModal.tsx @@ -39,7 +39,7 @@ export function ConfigModal(props: ConfigModalProps) { > - + e.stopPropagation()} align="end" className="bg-sidebar w-96 ">
) : ( )}
+
+ {isChatPannelOpen ? ( + + ) : podcast ? ( + + ) : null} +
); } diff --git a/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayer.tsx b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayer.tsx new file mode 100644 index 000000000..e17306f66 --- /dev/null +++ b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayer.tsx @@ -0,0 +1,321 @@ +"use client"; + +import { Pause, Play, Podcast, SkipBack, SkipForward, Volume2, VolumeX, X } from "lucide-react"; +import { motion } from "motion/react"; +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/podcasts-client"; +import { Button } from "@/components/ui/button"; +import { Slider } from "@/components/ui/slider"; +import { PodcastPlayerCompactSkeleton } from "./PodcastPlayerCompactSkeleton"; + +interface PodcastPlayerProps { + podcast: PodcastItem | null; + isLoading?: boolean; + onClose?: () => void; + compact?: boolean; +} + +export function PodcastPlayer({ + podcast, + isLoading = false, + onClose, + compact = false, +}: PodcastPlayerProps) { + const [audioSrc, setAudioSrc] = useState(undefined); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [volume, setVolume] = useState(0.7); + const [isMuted, setIsMuted] = useState(false); + const [isFetching, setIsFetching] = useState(false); + const audioRef = useRef(null); + const currentObjectUrlRef = useRef(null); + + // Cleanup object URL on unmount + useEffect(() => { + return () => { + if (currentObjectUrlRef.current) { + URL.revokeObjectURL(currentObjectUrlRef.current); + currentObjectUrlRef.current = null; + } + }; + }, []); + + // Load podcast audio when podcast changes + useEffect(() => { + if (!podcast) { + setAudioSrc(undefined); + setCurrentTime(0); + setDuration(0); + setIsPlaying(false); + setIsFetching(false); + return; + } + + const loadPodcast = async () => { + setIsFetching(true); + try { + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + throw new Error("Authentication token not found."); + } + + // Revoke previous object URL if exists + if (currentObjectUrlRef.current) { + URL.revokeObjectURL(currentObjectUrlRef.current); + currentObjectUrlRef.current = null; + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + signal: controller.signal, + } + ); + + if (!response.ok) { + throw new Error(`Failed to fetch audio stream: ${response.statusText}`); + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + currentObjectUrlRef.current = objectUrl; + setAudioSrc(objectUrl); + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + throw new Error("Request timed out. Please try again."); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } catch (error) { + console.error("Error fetching podcast:", error); + toast.error(error instanceof Error ? error.message : "Failed to load podcast audio."); + setAudioSrc(undefined); + } finally { + setIsFetching(false); + } + }; + + loadPodcast(); + }, [podcast]); + + const handleTimeUpdate = () => { + if (audioRef.current) { + setCurrentTime(audioRef.current.currentTime); + } + }; + + const handleMetadataLoaded = () => { + if (audioRef.current) { + setDuration(audioRef.current.duration); + } + }; + + const togglePlayPause = () => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + const handleSeek = (value: number[]) => { + if (audioRef.current) { + audioRef.current.currentTime = value[0]; + setCurrentTime(value[0]); + } + }; + + const handleVolumeChange = (value: number[]) => { + if (audioRef.current) { + const newVolume = value[0]; + audioRef.current.volume = newVolume; + setVolume(newVolume); + + if (newVolume === 0) { + audioRef.current.muted = true; + setIsMuted(true); + } else { + audioRef.current.muted = false; + setIsMuted(false); + } + } + }; + + const toggleMute = () => { + if (audioRef.current) { + const newMutedState = !isMuted; + audioRef.current.muted = newMutedState; + setIsMuted(newMutedState); + + if (!newMutedState && volume === 0) { + const restoredVolume = 0.5; + audioRef.current.volume = restoredVolume; + setVolume(restoredVolume); + } + } + }; + + const skipForward = () => { + if (audioRef.current) { + audioRef.current.currentTime = Math.min( + audioRef.current.duration, + audioRef.current.currentTime + 10 + ); + } + }; + + const skipBackward = () => { + if (audioRef.current) { + audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10); + } + }; + + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`; + }; + + // Show skeleton while fetching + if (isFetching && compact) { + return ; + } + + if (!podcast || !audioSrc) { + return null; + } + + if (compact) { + return ( + <> +
+
+ + + +

{podcast.title}

+ {onClose && ( + + + + )} +
+ +
+ +
+ {formatTime(currentTime)} / {formatTime(duration)} +
+
+ +
+ + + + + + + + + + + + + + + +
+
+ + + + ); + } + + return null; +} diff --git a/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayerCompactSkeleton.tsx b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayerCompactSkeleton.tsx new file mode 100644 index 000000000..d7007dadd --- /dev/null +++ b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/PodcastPlayerCompactSkeleton.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Podcast } from "lucide-react"; +import { motion } from "motion/react"; + +export function PodcastPlayerCompactSkeleton() { + return ( +
+ {/* Header with icon and title */} +
+ + + + {/* Title skeleton */} +
+
+ + {/* Progress bar skeleton */} +
+
+
+
+ + {/* Controls skeleton */} +
+
+
+
+
+
+
+ ); +} diff --git a/surfsense_web/components/chat/ChatPanel/PodcastPlayer/index.ts b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/index.ts new file mode 100644 index 000000000..55c19f934 --- /dev/null +++ b/surfsense_web/components/chat/ChatPanel/PodcastPlayer/index.ts @@ -0,0 +1,2 @@ +export { PodcastPlayer } from "./PodcastPlayer"; +export { PodcastPlayerCompactSkeleton } from "./PodcastPlayerCompactSkeleton"; From 678d8fbbcdce94cd0817011d35899438538c0c81 Mon Sep 17 00:00:00 2001 From: thierryverse Date: Wed, 29 Oct 2025 22:44:48 +0200 Subject: [PATCH 29/35] fix podcast re-generation --- surfsense_backend/app/tasks/podcast_tasks.py | 4 +- .../chat/ChatPanel/ChatPanelView.tsx | 40 ++++++++++--------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/surfsense_backend/app/tasks/podcast_tasks.py b/surfsense_backend/app/tasks/podcast_tasks.py index de65ca5e2..51e750f4d 100644 --- a/surfsense_backend/app/tasks/podcast_tasks.py +++ b/surfsense_backend/app/tasks/podcast_tasks.py @@ -141,9 +141,7 @@ async def generate_chat_podcast( # check if podcast already exists for this chat with the same title (re-generation) existing_podcast = await session.execute( - select(Podcast).filter( - Podcast.chat_id == chat_id, Podcast.title == podcast_title - ) + select(Podcast).filter(Podcast.chat_id == chat_id) ) existing_podcast = existing_podcast.scalars().first() diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx index 0e3485464..dc18afd0d 100644 --- a/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx @@ -113,25 +113,27 @@ export function ChatPanelView(props: ChatPanelViewProps) { )}
-
- {isChatPannelOpen ? ( - - ) : podcast ? ( - - ) : null} -
+ {podcast ? ( +
+ {isChatPannelOpen ? ( + + ) : podcast ? ( + + ) : null} +
+ ) : null}
); } From 55e5b45a42cd6e725ea260354554b6851398d146 Mon Sep 17 00:00:00 2001 From: thierryverse Date: Tue, 11 Nov 2025 04:02:04 +0200 Subject: [PATCH 30/35] fix podcast generation --- surfsense_backend/.env.example | 7 ++ .../32_add_podcast_staleness_detection.py | 50 ---------- .../34_add_podcast_staleness_detection.py | 60 ++++++++++++ .../app/agents/podcaster/nodes.py | 5 +- .../app/agents/podcaster/prompts.py | 12 ++- .../app/tasks/celery_tasks/podcast_tasks.py | 2 +- surfsense_backend/app/tasks/podcast_tasks.py | 2 +- .../[search_space_id]/client-layout.tsx | 61 +++++++++--- surfsense_web/app/dashboard/layout.tsx | 6 +- surfsense_web/app/layout.tsx | 7 +- .../components/announcement-banner.tsx | 2 +- .../components/chat/ChatInterface.tsx | 92 ++++------------- .../chat/ChatPanel/ChatPanelContainer.tsx | 89 +++++++---------- .../chat/ChatPanel/ChatPanelView.tsx | 28 ++++-- .../components/chat/ChatPanel/ConfigModal.tsx | 22 +++-- surfsense_web/components/ui/sidebar.tsx | 2 +- surfsense_web/lib/apis/chat-apis.ts | 28 ++++++ surfsense_web/lib/apis/podcast-apis.ts | 52 ++++++++++ surfsense_web/lib/query-client/cache-keys.ts | 3 + surfsense_web/lib/query-client/client.ts | 3 + .../query-client/query-client.provider.tsx | 13 +++ surfsense_web/package.json | 5 + surfsense_web/pnpm-lock.yaml | 98 ++++++++++++++++++- surfsense_web/stores/chat/active-chat.atom.ts | 39 ++++++++ surfsense_web/stores/chat/chat-ui.atom.ts | 9 ++ .../seach-space/active-seach-space.atom.ts | 3 + 26 files changed, 477 insertions(+), 223 deletions(-) delete mode 100644 surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py create mode 100644 surfsense_backend/alembic/versions/34_add_podcast_staleness_detection.py create mode 100644 surfsense_web/lib/apis/chat-apis.ts create mode 100644 surfsense_web/lib/apis/podcast-apis.ts create mode 100644 surfsense_web/lib/query-client/cache-keys.ts create mode 100644 surfsense_web/lib/query-client/client.ts create mode 100644 surfsense_web/lib/query-client/query-client.provider.tsx create mode 100644 surfsense_web/stores/chat/active-chat.atom.ts create mode 100644 surfsense_web/stores/chat/chat-ui.atom.ts create mode 100644 surfsense_web/stores/seach-space/active-seach-space.atom.ts diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index 349cb0307..2a0351d7b 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -1,5 +1,12 @@ DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense +SECRET_KEY=SECRET +NEXT_FRONTEND_URL=http://localhost:3000 + +#Celery Config +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 + #Celery Config CELERY_BROKER_URL=redis://localhost:6379/0 CELERY_RESULT_BACKEND=redis://localhost:6379/0 diff --git a/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py b/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py deleted file mode 100644 index d63e6cba2..000000000 --- a/surfsense_backend/alembic/versions/32_add_podcast_staleness_detection.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Add podcast staleness detection columns to chats and podcasts tables - -This feature allows the system to detect when a podcast is outdated compared to the current state of the chat it was generated from, enabling users to regenerate podcasts when needed. - -Revision ID: 32 -Revises: 31 -""" - -from collections.abc import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers -revision: str = "32" -down_revision: str | None = "31" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - """Add state_version, chat_state_version, and chat_id to chats and podcasts tables.""" - - # Add state_version column to chats table with default value of 1 - op.add_column( - "chats", - sa.Column("state_version", sa.BigInteger(), nullable=False, server_default="1"), - ) - - # Add chat_state_version column to podcasts table (nullable, set when podcast is generated) - op.add_column( - "podcasts", sa.Column("chat_state_version", sa.BigInteger(), nullable=True) - ) - - # Add chat_id column to podcasts table (nullable, set when podcast is generated from a chat) - op.add_column("podcasts", sa.Column("chat_id", sa.Integer(), nullable=True)) - - -def downgrade() -> None: - """Remove state_version, chat_state_version, and chat_id columns.""" - - # Remove chat_state_version from podcasts table - op.drop_column("podcasts", "chat_state_version") - - # Remove chat_id from podcasts table - op.drop_column("podcasts", "chat_id") - - # Remove state_version from chats table - op.drop_column("chats", "state_version") diff --git a/surfsense_backend/alembic/versions/34_add_podcast_staleness_detection.py b/surfsense_backend/alembic/versions/34_add_podcast_staleness_detection.py new file mode 100644 index 000000000..4991cd58e --- /dev/null +++ b/surfsense_backend/alembic/versions/34_add_podcast_staleness_detection.py @@ -0,0 +1,60 @@ +"""Add podcast staleness detection columns to chats and podcasts tables + +This feature allows the system to detect when a podcast is outdated compared to +the current state of the chat it was generated from, enabling users to regenerate +podcasts when needed. + +Revision ID: 34 +Revises: 33 +""" + +from collections.abc import Sequence + +from alembic import op + +# revision identifiers +revision: str = "34" +down_revision: str | None = "33" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Add columns only if they don't already exist (safe for re-runs).""" + + # Add 'state_version' column to chats table (default 1) + op.execute(""" + ALTER TABLE chats + ADD COLUMN IF NOT EXISTS state_version BIGINT DEFAULT 1 NOT NULL + """) + + # Add 'chat_state_version' column to podcasts table + op.execute(""" + ALTER TABLE podcasts + ADD COLUMN IF NOT EXISTS chat_state_version BIGINT + """) + + # Add 'chat_id' column to podcasts table + op.execute(""" + ALTER TABLE podcasts + ADD COLUMN IF NOT EXISTS chat_id INTEGER + """) + + +def downgrade() -> None: + """Remove columns only if they exist.""" + + op.execute(""" + ALTER TABLE podcasts + DROP COLUMN IF EXISTS chat_state_version + """) + + op.execute(""" + ALTER TABLE podcasts + DROP COLUMN IF EXISTS chat_id + """) + + op.execute(""" + ALTER TABLE chats + DROP COLUMN IF EXISTS state_version + """) diff --git a/surfsense_backend/app/agents/podcaster/nodes.py b/surfsense_backend/app/agents/podcaster/nodes.py index bce9882d6..51d748a3e 100644 --- a/surfsense_backend/app/agents/podcaster/nodes.py +++ b/surfsense_backend/app/agents/podcaster/nodes.py @@ -29,6 +29,7 @@ async def create_podcast_transcript( configuration = Configuration.from_runnable_config(config) user_id = configuration.user_id search_space_id = configuration.search_space_id + podcast_title = configuration.podcast_title # Get user's long context LLM llm = await get_user_long_context_llm(state.db_session, user_id, search_space_id) @@ -37,8 +38,8 @@ async def create_podcast_transcript( print(error_message) raise RuntimeError(error_message) - # Get the prompt - prompt = get_podcast_generation_prompt() + # Get the prompt with podcast_title as user_prompt + prompt = get_podcast_generation_prompt(user_prompt=podcast_title) # Create the messages messages = [ diff --git a/surfsense_backend/app/agents/podcaster/prompts.py b/surfsense_backend/app/agents/podcaster/prompts.py index a3d6c3147..3e0981f32 100644 --- a/surfsense_backend/app/agents/podcaster/prompts.py +++ b/surfsense_backend/app/agents/podcaster/prompts.py @@ -1,11 +1,19 @@ import datetime -def get_podcast_generation_prompt(): +def get_podcast_generation_prompt(user_prompt: str | None = None): + user_prompt_section = "" + if user_prompt: + user_prompt_section = f""" + +{user_prompt} + +""" + return f""" Today's date: {datetime.datetime.now().strftime("%Y-%m-%d")} -You are a master podcast scriptwriter, adept at transforming diverse input content into a lively, engaging, and natural-sounding conversation between two distinct podcast hosts. Your primary objective is to craft authentic, flowing dialogue that captures the spontaneity and chemistry of a real podcast discussion, completely avoiding any hint of robotic scripting or stiff formality. Think dynamic interplay, not just information delivery. +You are a master podcast scriptwriter, adept at transforming diverse input content into a lively, engaging, and natural-sounding conversation between two distinct podcast hosts. Your primary objective is to craft authentic, flowing dialogue that captures the spontaneity and chemistry of a real podcast discussion, completely avoiding any hint of robotic scripting or stiff formality. Think dynamic interplay, not just information delivery.{user_prompt_section} - '': A block of text containing the information to be discussed in the podcast. This could be research findings, an article summary, a detailed outline, user chat history related to the topic, or any other relevant raw information. The content might be unstructured but serves as the factual basis for the podcast dialogue. diff --git a/surfsense_backend/app/tasks/celery_tasks/podcast_tasks.py b/surfsense_backend/app/tasks/celery_tasks/podcast_tasks.py index 59a3bb2b1..1e938d829 100644 --- a/surfsense_backend/app/tasks/celery_tasks/podcast_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/podcast_tasks.py @@ -46,7 +46,7 @@ def generate_chat_podcast_task( Args: chat_id: ID of the chat to generate podcast from search_space_id: ID of the search space - podcast_title: Title for the podcast + podcast_title: Title for the podcast (used as user prompt for generation) user_id: ID of the user """ loop = asyncio.new_event_loop() diff --git a/surfsense_backend/app/tasks/podcast_tasks.py b/surfsense_backend/app/tasks/podcast_tasks.py index 51e750f4d..ea125fa11 100644 --- a/surfsense_backend/app/tasks/podcast_tasks.py +++ b/surfsense_backend/app/tasks/podcast_tasks.py @@ -96,7 +96,7 @@ async def generate_chat_podcast( config = { "configurable": { - "podcast_title": "SurfSense", + "podcast_title": podcast_title, "user_id": str(user_id), "search_space_id": search_space_id, } diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index 55da177a4..e1e51c601 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -1,10 +1,12 @@ "use client"; -import { Loader2 } from "lucide-react"; +import { useAtom } from "jotai"; +import { Loader2, PanelRight } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import type React from "react"; import { useEffect, useMemo, useState } from "react"; +import { ChatPanelContainer } from "@/components/chat/ChatPanel/ChatPanelContainer"; import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider"; @@ -13,6 +15,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { useLLMPreferences } from "@/hooks/use-llm-configs"; +import { cn } from "@/lib/utils"; +import { chatUIAtom } from "@/stores/chat/chat-ui.atom"; export function DashboardClientLayout({ children, @@ -30,6 +34,10 @@ export function DashboardClientLayout({ const pathname = usePathname(); const searchSpaceIdNum = Number(searchSpaceId); + const [chatUIState, setChatUIState] = useAtom(chatUIAtom); + + const { isChatPannelOpen } = chatUIState; + const { loading, error, isOnboardingComplete } = useLLMPreferences(searchSpaceIdNum); const [hasCheckedOnboarding, setHasCheckedOnboarding] = useState(false); @@ -129,28 +137,49 @@ export function DashboardClientLayout({ } return ( - + {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} - -
-
-
- - - -
-
- - -
+ +
+
+
+
+
+ + + +
+
+ + + +
+
+
+
{children}
-
- {children} + +
); diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index 9b396e990..01436aff9 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -42,9 +42,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { } return ( - <> +
- {children} - +
{children}
+
); } diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 06f894094..c7bfb6058 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -6,6 +6,7 @@ import { I18nProvider } from "@/components/providers/I18nProvider"; import { ThemeProvider } from "@/components/theme/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { LocaleProvider } from "@/contexts/LocaleContext"; +import { ReactQueryClientProvider } from "@/lib/query-client/query-client.provider"; import { cn } from "@/lib/utils"; const roboto = Roboto({ @@ -89,7 +90,7 @@ export default function RootLayout({ // Locale state is managed by LocaleContext and persisted in localStorage return ( - + - {children} + +
{children}
+
diff --git a/surfsense_web/components/announcement-banner.tsx b/surfsense_web/components/announcement-banner.tsx index e9647085f..673bda51d 100644 --- a/surfsense_web/components/announcement-banner.tsx +++ b/surfsense_web/components/announcement-banner.tsx @@ -10,7 +10,7 @@ export function AnnouncementBanner() { if (!isVisible) return null; return ( -
+
diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index c36725133..2b167ddeb 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -1,16 +1,13 @@ "use client"; import { type ChatHandler, ChatSection as LlamaIndexChatSection } from "@llamaindex/chat-ui"; +import { useSetAtom } from "jotai"; import { useParams } from "next/navigation"; -import { createContext, useCallback, useEffect, useState } from "react"; -import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; -import type { PodcastItem } from "@/app/dashboard/[search_space_id]/podcasts/podcasts-client"; -import type { ResearchMode } from "@/components/chat"; +import { useEffect } from "react"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; -import { useChatAPI } from "@/hooks/use-chat"; import type { Document } from "@/hooks/use-documents"; -import { usePodcast } from "@/hooks/use-podcast"; +import { activeChatIdAtom } from "@/stores/chat/active-chat.atom"; import { ChatPanelContainer } from "./ChatPanel/ChatPanelContainer"; interface ChatInterfaceProps { @@ -23,17 +20,6 @@ interface ChatInterfaceProps { onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; } -interface ChatInterfaceContext { - isChatPannelOpen: boolean; - setIsChatPannelOpen: (value: boolean) => void; - chat_id: string; - chatDetails: ChatDetails | null; - podcast: PodcastItem | null; - setPodcast: (podcast: PodcastItem | null) => void; -} - -export const chatInterfaceContext = createContext(null); - export default function ChatInterface({ handler, onDocumentSelectionChange, @@ -44,69 +30,29 @@ export default function ChatInterface({ onSearchModeChange, }: ChatInterfaceProps) { const { chat_id, search_space_id } = useParams(); - const [chatDetails, setChatDetails] = useState(null); - const [isChatPannelOpen, setIsChatPannelOpen] = useState(false); - const [podcast, setPodcast] = useState(null); - const contextValue = { - isChatPannelOpen, - setIsChatPannelOpen, - chat_id: typeof chat_id === "string" ? chat_id : chat_id ? chat_id[0] : "", - podcast, - setPodcast, - chatDetails, - }; - - const { getPodcastByChatId } = usePodcast(); - - const { fetchChatDetails } = useChatAPI({ - token: localStorage?.getItem("surfsense_bearer_token"), - search_space_id: search_space_id as string, - }); - - const getPodcast = useCallback( - async (id: string) => { - const podcast = await getPodcastByChatId(Number(id)); - setPodcast(podcast); - }, - [getPodcastByChatId] - ); - - const getChat = useCallback( - async (id: string) => { - const chat = await fetchChatDetails(id); - setChatDetails(chat); - }, - [fetchChatDetails] - ); + const setActiveChatIdState = useSetAtom(activeChatIdAtom); useEffect(() => { const id = typeof chat_id === "string" ? chat_id : chat_id ? chat_id[0] : ""; if (!id) return; - getChat(id); - getPodcast(id); + setActiveChatIdState(id); }, [chat_id, search_space_id]); return ( - - -
-
- -
- -
-
- + +
+ +
+
- - +
+
); } diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx index 66dbc088a..4648ae9c9 100644 --- a/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelContainer.tsx @@ -1,8 +1,11 @@ -import { PanelRight } from "lucide-react"; -import { useActionState, useContext, useTransition } from "react"; +"use client"; +import { useAtom, useAtomValue } from "jotai"; +import { LoaderIcon, PanelRight, TriangleAlert } from "lucide-react"; import { toast } from "sonner"; +import { generatePodcast } from "@/lib/apis/podcast-apis"; import { cn } from "@/lib/utils"; -import { chatInterfaceContext } from "../ChatInterface"; +import { activeChatAtom, activeChatIdAtom } from "@/stores/chat/active-chat.atom"; +import { chatUIAtom } from "@/stores/chat/chat-ui.atom"; import { ChatPanelView } from "./ChatPanelView"; export interface GeneratePodcastRequest { @@ -13,68 +16,52 @@ export interface GeneratePodcastRequest { } export function ChatPanelContainer() { - const context = useContext(chatInterfaceContext); + const { + data: activeChatState, + isLoading: isChatLoading, + error: chatError, + } = useAtomValue(activeChatAtom); + const activeChatIdState = useAtomValue(activeChatIdAtom); + const authToken = localStorage.getItem("surfsense_bearer_token"); + const { isChatPannelOpen } = useAtomValue(chatUIAtom); - if (!context) { - throw new Error("chatInterfaceContext must be used within a ChatProvider"); - } - - const { isChatPannelOpen, setIsChatPannelOpen, chat_id: chatId } = context; - - const generatePodcast = async (request: GeneratePodcastRequest) => { + const handleGeneratePodcast = async (request: GeneratePodcastRequest) => { try { - const { podcast_title = "SurfSense Podcast" } = request; - - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`, - { - method: "POST", - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ ...request, podcast_title }), - } - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.detail || "Failed to generate podcast"); + if (!authToken) { + throw new Error("Authentication error. Please log in again."); } - + await generatePodcast(request, authToken); toast.success(`Podcast generation started!`); } catch (error) { + toast.error("Error generating podcast. Please log in again."); console.error("Error generating podcast:", error); - console.log(error); - } finally { } }; - return chatId && chatId !== "" ? ( + return activeChatIdState ? (
-
- -
+ {isChatLoading || chatError ? ( +
+ {isChatLoading ? ( +
+ +
+ ) : chatError ? ( +
+ +
+ ) : null} +
+ ) : null} -
- -
+ {!isChatLoading && !chatError && activeChatState?.chatDetails && ( + + )}
) : null; } diff --git a/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx index dc18afd0d..0f18a7e99 100644 --- a/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx +++ b/surfsense_web/components/chat/ChatPanel/ChatPanelView.tsx @@ -1,9 +1,11 @@ "use client"; +import { useAtom, useAtomValue } from "jotai"; import { AlertCircle, Pencil, Play, Podcast, RefreshCw } from "lucide-react"; import { useCallback, useContext, useTransition } from "react"; import { cn } from "@/lib/utils"; -import { chatInterfaceContext } from "../ChatInterface"; +import { activeChatAtom } from "@/stores/chat/active-chat.atom"; +import { chatUIAtom } from "@/stores/chat/chat-ui.atom"; import { getPodcastStalenessMessage, isPodcastStale } from "../PodcastUtils"; import type { GeneratePodcastRequest } from "./ChatPanelContainer"; import { ConfigModal } from "./ConfigModal"; @@ -14,12 +16,13 @@ interface ChatPanelViewProps { } export function ChatPanelView(props: ChatPanelViewProps) { - const context = useContext(chatInterfaceContext); - if (!context) { - throw new Error("chatInterfaceContext must be used within a ChatProvider"); - } + const [chatUIState, setChatUIState] = useAtom(chatUIAtom); + const { data: activeChatState } = useAtomValue(activeChatAtom); + + const { isChatPannelOpen } = chatUIState; + const podcast = activeChatState?.podcast; + const chatDetails = activeChatState?.chatDetails; - const { isChatPannelOpen, setIsChatPannelOpen, chatDetails, podcast } = context; const { generatePodcast } = props; // Check if podcast is stale @@ -40,7 +43,7 @@ export function ChatPanelView(props: ChatPanelViewProps) {
setIsChatPannelOpen(!isChatPannelOpen)} + onClick={() => + setChatUIState((prev) => ({ + ...prev, + isChatPannelOpen: !isChatPannelOpen, + })) + } className={cn( "p-2 rounded-full hover:bg-muted transition-colors", podcastIsStale && "text-amber-600 dark:text-amber-500" @@ -116,7 +124,7 @@ export function ChatPanelView(props: ChatPanelViewProps) { {podcast ? (
@@ -126,7 +134,7 @@ export function ChatPanelView(props: ChatPanelViewProps) {