From 667552e015622e3d2eab4e22bfa8dacd540c409d Mon Sep 17 00:00:00 2001 From: Aj Wazzan Date: Thu, 1 May 2025 09:11:54 -0700 Subject: [PATCH] Refactor GetSummary and AIChat components. Introduce GetState function for fetching brain state. Clean up unused imports and improve error handling in AIChat. Update layout and nav-user components for better functionality. --- apps/mail/actions/getSummary.ts | 30 ++- apps/mail/app/(routes)/layout.tsx | 2 +- apps/mail/app/api/chat/route.ts | 2 +- apps/mail/components/create/ai-chat.tsx | 239 +----------------------- apps/mail/components/ui/nav-user.tsx | 25 ++- apps/mail/hooks/use-summary.ts | 19 +- 6 files changed, 57 insertions(+), 260 deletions(-) diff --git a/apps/mail/actions/getSummary.ts b/apps/mail/actions/getSummary.ts index eea576b96..520b2c4e6 100644 --- a/apps/mail/actions/getSummary.ts +++ b/apps/mail/actions/getSummary.ts @@ -1,10 +1,6 @@ 'use server'; import { getAuthenticatedUserId } from '@/app/api/utils'; -import { connection, summary } from '@zero/db/schema'; -import { headers } from 'next/headers'; -import { and, eq } from 'drizzle-orm'; -import { auth } from '@/lib/auth'; -import { db } from '@zero/db'; +import { getActiveConnection } from './utils'; import axios from 'axios'; export const GetSummary = async (threadId: string) => { @@ -14,11 +10,7 @@ export const GetSummary = async (threadId: string) => { return null; } - const response = await axios.get(process.env.BRAIN_URL + `/brain/thread/summary/${threadId}`, { - headers: { - // 'Authorization': `Bearer ${}` - }, - }); + const response = await axios.get(process.env.BRAIN_URL + `/brain/thread/summary/${threadId}`); return response.data ?? null; } catch (error) { @@ -26,3 +18,21 @@ export const GetSummary = async (threadId: string) => { return null; } }; + +export const GetState = async () => { + try { + if (!process.env.BRAIN_URL) { + return null; + } + const connection = await getActiveConnection(); + if (!connection) { + return null; + } + + const response = await axios.get(process.env.BRAIN_URL + `/limit/${connection.id}`); + return response.data ?? null; + } catch (error) { + console.error('Error getting summary:', error); + return null; + } +}; diff --git a/apps/mail/app/(routes)/layout.tsx b/apps/mail/app/(routes)/layout.tsx index 948b7ef2b..68867671e 100644 --- a/apps/mail/app/(routes)/layout.tsx +++ b/apps/mail/app/(routes)/layout.tsx @@ -12,7 +12,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{threads.map(renderThread)}
; }; -export function AIChat({ editor, onMessagesChange, onReset }: AIChatProps) { - const [value, setValue] = useState(''); +export function AIChat() { const [showVoiceChat, setShowVoiceChat] = useState(false); - const [expandedResults, setExpandedResults] = useState>(new Set()); - const [searchValue, setSearchValue] = useSearchValue(); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); - const fileInputRef = useRef(null); - const pathname = usePathname(); - const router = useRouter(); - const searchParams = useSearchParams(); - const { data: session } = useSession(); - const { data: connections } = useConnections(); const { messages, input, setInput, error, handleSubmit, status } = useChat({ api: '/api/chat', maxSteps: 5, }); - // Scroll to bottom function const scrollToBottom = useCallback(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); @@ -134,134 +125,10 @@ export function AIChat({ editor, onMessagesChange, onReset }: AIChatProps) { // if (onMessagesChange) { // onMessagesChange(messages); // } - }, [messages, onMessagesChange, scrollToBottom]); - - useEffect(() => { - if (onReset) { - onReset(); - } - }, [onReset]); - - // const handleSendMessage = async () => { - // if (!value.trim() || isLoading) return; - - // const userMessage: Message = { - // id: generateId(), - // role: 'user', - // content: value.trim(), - // timestamp: new Date(), - // }; - - // setMessages((prev) => [...prev, userMessage]); - // setValue(''); - // setIsLoading(true); - - // try { - // if (!response.ok) { - // throw new Error('Failed to get response'); - // } - - // const data = await response.json(); - - // // Update the search value - // setSearchValue({ - // value: data.searchQuery, - // highlight: value.trim(), - // isLoading: false, - // isAISearching: false, - // folder: searchValue.folder, - // }); - - // // Add assistant message with search results - // const assistantMessage: Message = { - // id: generateId(), - // role: 'assistant', - // content: data.content, - // timestamp: new Date(), - // type: 'search', - // searchContent: { - // searchDisplay: data.searchDisplay, - // results: data.results, - // }, - // }; - - // setMessages((prev) => [...prev, assistantMessage]); - // } catch (error) { - // console.error('Error:', error); - // toast.error('Failed to generate response. Please try again.'); - // } finally { - // setIsLoading(false); - // } - // }; - - const handleAcceptSuggestion = (emailContent: { subject?: string; content: string }) => { - if (!editor) { - toast.error('Editor not found'); - return; - } - - try { - // Format the content to preserve line breaks - const formattedContent = emailContent.content - .split('\n') - .map((line) => `

${line}

`) - .join(''); - - // Set the content in the editor - editor.commands.setContent(formattedContent); - - // Find the create-email component and update its content - const createEmailElement = document.querySelector('[data-create-email]'); - if (createEmailElement) { - const handler = (createEmailElement as any).onContentGenerated; - if (handler && typeof handler === 'function') { - handler({ content: emailContent.content, subject: emailContent.subject }); - } - } - - toast.success('Email content applied successfully'); - } catch (error) { - console.error('Error applying suggestion:', error); - toast.error('Failed to apply email content'); - } - }; - - const handleRejectSuggestion = (messageId: string) => { - toast.info('Email suggestion rejected'); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - handleSubmit(); - } - }; - - const generateId = () => nanoid(); - - const toggleExpandResults = (messageId: string) => { - setExpandedResults((prev) => { - const newSet = new Set(prev); - if (newSet.has(messageId)) { - newSet.delete(messageId); - } else { - newSet.add(messageId); - } - return newSet; - }); - }; - - const sanitizeSnippet = (snippet: string) => { - return snippet - .replace(/<\/?[^>]+(>|$)/g, '') // Remove HTML tags - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&'); - }; + }, [messages, messagesEndRef]); return (
- {/* Messages container */}
{!messages.length ? ( @@ -325,10 +192,6 @@ export function AIChat({ editor, onMessagesChange, onReset }: AIChatProps) { : 'overflow-wrap-anywhere mr-auto break-words bg-[#f0f0f0] p-2 dark:bg-[#313131]', // Assistant messages aligned to left )} > - {/*
- {message.content} -
*/} - {message.parts.map((part) => { if (part.type === 'text') { return {part.text}; @@ -341,108 +204,22 @@ export function AIChat({ editor, onMessagesChange, onReset }: AIChatProps) { ) : null) ); } - // if (part.type === 'source') { - // return

Source: {part.source.title}

; - // } - // if (part.type === 'step-start') { - // return ; - // } - // return

{part.type}

; })} - - {/* {message.type === 'search' && - message.searchContent && - message.searchContent.results.length > 0 && ( -
- {(expandedResults.has(message.id) - ? message.searchContent.results - : message.searchContent.results.slice(0, 5) - ).map((result: any, i: number) => ( -
-
-

- {result.subject.toLowerCase().includes('meeting') ? ( - 📅 {result.subject} - ) : ( - result.subject || 'No subject' - )} -

- - from {result.from || 'Unknown sender'} - -
-
- {sanitizeSnippet(result.snippet)} -
-
- -
-
- ))} - {message.searchContent.results.length > 5 && ( - - )} -
- )} - - {message.type === 'email' && message.emailContent && ( -
- {message.emailContent.subject && ( -
- Subject: {message.emailContent.subject} -
- )} -
{message.emailContent.content}
-
- - -
-
- )} */}
)) )} - {/* Invisible element to scroll to */}
- {JSON.stringify(error)} - {/* Loading indicator */} {status === 'submitted' && (
- zero is thinking... + + zero is thinking... +
)} + {status === 'error' &&
Error, please try again later
}
@@ -469,7 +246,7 @@ export function AIChat({ editor, onMessagesChange, onReset }: AIChatProps) { form="ai-chat-form" type="submit" className="border-border/50 inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-md border bg-white pl-1.5 pr-1 dark:bg-[#262626]" - disabled={!input.trim()} + disabled={!input.trim() || status !== 'ready'} >
diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx index bed745d8a..cf6595eaa 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -31,6 +31,7 @@ import { signOut, useSession } from '@/lib/auth-client'; import { AddConnectionDialog } from '../connection/add'; import { putConnection } from '@/actions/connections'; import { useSidebar } from '@/components/ui/sidebar'; +import { useBrainState } from '@/hooks/use-summary'; import { dexieStorageProvider } from '@/lib/idb'; import { EnableBrain } from '@/actions/brain'; import { useRouter } from 'next/navigation'; @@ -101,6 +102,8 @@ export function NavUser() { ); }; + const { data: brainState } = useBrainState(); + const handleThemeToggle = () => { setTheme(theme === 'dark' ? 'light' : 'dark'); }; @@ -266,14 +269,6 @@ export function NavUser() { Terms
- -

Debug

- -
- -

Clear Local Cache

-
-
@@ -394,12 +389,14 @@ export function NavUser() {

Clear Local Cache

- {/* -
- -

Enable Brain Activity

-
-
*/} + {!brainState?.enabled ? ( + +
+ +

Enable Brain Activity

+
+
+ ) : null}
diff --git a/apps/mail/hooks/use-summary.ts b/apps/mail/hooks/use-summary.ts index d04ec7f45..e25ab83e5 100644 --- a/apps/mail/hooks/use-summary.ts +++ b/apps/mail/hooks/use-summary.ts @@ -1,11 +1,12 @@ 'use client'; -import { GetSummary } from '@/actions/getSummary'; -import { string } from 'zod'; +import { GetState, GetSummary } from '@/actions/getSummary'; +import { useSession } from '@/lib/auth-client'; import useSWR from 'swr'; export const useSummary = (threadId: string | null) => { + const { data: session } = useSession(); const { data, isLoading } = useSWR<{ short: string; long: string } | null>( - threadId ? `ai:summary:${threadId}` : null, + session && threadId ? `ai:summary:${threadId}` : null, async () => { if (!threadId) return null; return await GetSummary(threadId); @@ -14,3 +15,15 @@ export const useSummary = (threadId: string | null) => { return { data, isLoading }; }; + +export const useBrainState = () => { + const { data: session } = useSession(); + const { data, isLoading } = useSWR<{ enabled: boolean } | null>( + session ? `brain:state:${session?.connectionId}` : null, + async () => { + return await GetState(); + }, + ); + + return { data, isLoading }; +};