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/actions/mail.ts b/apps/mail/actions/mail.ts index 457d5bc93..8aaf3399e 100644 --- a/apps/mail/actions/mail.ts +++ b/apps/mail/actions/mail.ts @@ -4,9 +4,6 @@ import { IGetThreadResponse } from '@/app/api/driver/types'; import { ParsedMessage } from '@/types'; export const getMail = async ({ id }: { id: string }): Promise => { - if (!id) { - throw new Error('Missing required fields'); - } try { const driver = await getActiveDriver(); const mailData = await driver.get(id); diff --git a/apps/mail/actions/send.ts b/apps/mail/actions/send.ts index 7792e1e5f..73464cb90 100644 --- a/apps/mail/actions/send.ts +++ b/apps/mail/actions/send.ts @@ -1,9 +1,9 @@ 'use server'; +import { updateWritingStyleMatrix } from '@/services/writing-style-service'; import { createDriver } from '@/app/api/driver'; import { getActiveConnection } from './utils'; import { ISendEmail } from '@/types'; -import { updateWritingStyleMatrix } from '@/services/writing-style-service'; import { after } from 'next/server'; export async function sendEmail({ @@ -19,7 +19,9 @@ export async function sendEmail({ draftId, }: ISendEmail & { draftId?: string }) { if (!to || !subject || !message) { - throw new Error('Missing required fields'); + throw new Error( + `Missing required fields, ${to ? '' : 'to'} ${subject ? '' : 'subject'} ${message ? '' : 'message'}`, + ); } const connection = await getActiveConnection(); @@ -56,14 +58,13 @@ export async function sendEmail({ after(async () => { try { - console.warn('Saving writing style matrix...') - await updateWritingStyleMatrix(connection.id, message) - console.warn('Saved writing style matrix.') + console.warn('Saving writing style matrix...'); + await updateWritingStyleMatrix(connection.id, message); + console.warn('Saved writing style matrix.'); } catch (error) { - console.error('Failed to save writing style matrix', error) + console.error('Failed to save writing style matrix', error); } - }) + }); return { success: true }; } - 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 }) {
void; - onReset?: () => void; -} - const renderThread = (thread: { id: string; title: string; snippet: string }) => { const [, setThreadId] = useQueryState('threadId'); const { data: getThread } = useThread(thread.id); @@ -102,26 +97,16 @@ const RenderThreads = ({ return
{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 +119,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 +186,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 +198,24 @@ 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) && ( +
Error, please try again later
+ )}
@@ -469,7 +242,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/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index 3ceee313c..06b8530d5 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -17,6 +17,7 @@ import { Command, Paperclip, Plus } from 'lucide-react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Avatar, AvatarFallback } from '../ui/avatar'; import { aiCompose } from '@/actions/ai-composer'; +import { cn, formatFileSize } from '@/lib/utils'; import { useThread } from '@/hooks/use-threads'; import { useSession } from '@/lib/auth-client'; import { createDraft } from '@/actions/drafts'; @@ -27,7 +28,6 @@ import { useRef, useState } from 'react'; import { useQueryState } from 'nuqs'; import pluralize from 'pluralize'; import { useEffect } from 'react'; -import { cn } from '@/lib/utils'; import { toast } from 'sonner'; import { z } from 'zod'; @@ -693,32 +693,38 @@ export function EmailComposer({ {attachments && attachments.length > 0 && ( - - - + +
-

Attachments

+

+ Attachments +

{attachments.map((file, index) => (

{file.name}

- {(file.size / (1024 * 1024)).toFixed(2)} MB + {formatFileSize(file.size)}

diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index 6bc67a5ba..24867cb54 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -631,7 +631,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => { {formatDate(emailData?.receivedOn)}
-
+

To:{' '} {(() => { @@ -761,7 +761,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => {

)} {emailData?.attachments && emailData?.attachments.length > 0 ? ( -
+
{emailData?.attachments.map((attachment, index) => (
) : null} -
+
- -

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 }; +}; diff --git a/apps/mail/lib/email-utils.client.tsx b/apps/mail/lib/email-utils.client.tsx index 3605bd7be..554a771c7 100644 --- a/apps/mail/lib/email-utils.client.tsx +++ b/apps/mail/lib/email-utils.client.tsx @@ -13,10 +13,6 @@ export const handleUnsubscribe = async ({ emailData }: { emailData: ParsedMessag listUnsubscribePost: emailData.listUnsubscribePost, }); if (listUnsubscribeAction) { - track('Unsubscribe', { - domain: emailData.sender.email.split('@')?.[1] ?? 'unknown', - }); - switch (listUnsubscribeAction.type) { case 'get': window.open(listUnsubscribeAction.url, '_blank'); @@ -48,12 +44,14 @@ export const handleUnsubscribe = async ({ emailData }: { emailData: ParsedMessag name: listUnsubscribeAction.emailAddress, }, ], - subject: listUnsubscribeAction.subject, + subject: listUnsubscribeAction.subject ?? 'Unsubscribe Request', message: 'Zero sent this email to unsubscribe from this mailing list.', - attachments: [], }); return true; } + track('Unsubscribe', { + domain: emailData.sender.email.split('@')?.[1] ?? 'unknown', + }); } } } catch (error) {