diff --git a/.env.example b/.env.example index af9fa2775c..8c4f2052ab 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,10 @@ RESEND_API_KEY= OPENAI_API_KEY= PERPLEXITY_API_KEY= +# OpenAI Model names (gpt-4o, gpt-4o-mini etc) +OPENAI_MODEL= +OPENAI_MINI_MODEL= + #AI PROMPT AI_SYSTEM_PROMPT="" diff --git a/apps/mail/app/(routes)/settings/general/page.tsx b/apps/mail/app/(routes)/settings/general/page.tsx index ae96589ffc..3d42150f2f 100644 --- a/apps/mail/app/(routes)/settings/general/page.tsx +++ b/apps/mail/app/(routes)/settings/general/page.tsx @@ -30,6 +30,7 @@ import { useTRPC } from '@/providers/query-provider'; import { getBrowserTimezone } from '@/lib/timezones'; import { Textarea } from '@/components/ui/textarea'; import { useSettings } from '@/hooks/use-settings'; +import { locales as localesData } from '@/locales'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; // import { useRevalidator } from 'react-router'; @@ -60,14 +61,14 @@ const TimezoneSelect = memo( variant="outline" role="combobox" aria-expanded={open} - className="w-46 flex items-center justify-start" + className="w-46 flex !h-9 items-center justify-start rounded-md hover:bg-transparent" > {field.value} - +
( - {m['pages.settings.general.language']()} + {m['pages.settings.general.language']()} - + ); -} +} \ No newline at end of file diff --git a/apps/mail/app/globals.css b/apps/mail/app/globals.css index 4ce37a8aba..192b802ff5 100644 --- a/apps/mail/app/globals.css +++ b/apps/mail/app/globals.css @@ -9,6 +9,36 @@ .text-balance { text-wrap: balance; } + .horizontal-fade-mask { + @apply overflow-x-auto; + position: relative; + } + @supports (mask-image: linear-gradient(to right, transparent, black)) or + (-webkit-mask-image: linear-gradient(to right, transparent, black)) { + .horizontal-fade-mask::before, + .horizontal-fade-mask::after { + content: ''; + position: absolute; + top: 0; + height: 100%; + width: 15%; + pointer-events: none; + background: hsl(var(--panel)); + z-index: 1; + } + + .horizontal-fade-mask::before { + left: 0; + -webkit-mask-image: linear-gradient(to right, white, transparent); + mask-image: linear-gradient(to right, white, transparent); + } + + .horizontal-fade-mask::after { + right: 0; + -webkit-mask-image: linear-gradient(to left, white, transparent); + mask-image: linear-gradient(to left, white, transparent); + } + } } @layer base { @@ -53,6 +83,7 @@ --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; --icon-color: currentColor; + --panel: 0 0% 100%; } .dark { @@ -89,6 +120,7 @@ --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; --icon-color: currentColor; + --panel: 240 3.7% 10.2%; } } diff --git a/apps/mail/app/root.tsx b/apps/mail/app/root.tsx index c36afb2739..22acfdacd1 100644 --- a/apps/mail/app/root.tsx +++ b/apps/mail/app/root.tsx @@ -32,7 +32,7 @@ export const getServerTrpc = (req: Request) => createTRPCClient({ links: [ httpBatchLink({ - maxItems: 8, + maxItems: 1, url: getUrl(), transformer: superjson, headers: req.headers, diff --git a/apps/mail/components/create/ai-chat.tsx b/apps/mail/components/create/ai-chat.tsx index 435b967b65..ce0b000300 100644 --- a/apps/mail/components/create/ai-chat.tsx +++ b/apps/mail/components/create/ai-chat.tsx @@ -1,4 +1,5 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useAIFullScreen, useAISidebar } from '../ui/ai-sidebar'; import useComposeEditor from '@/hooks/use-compose-editor'; @@ -84,7 +85,7 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi const secondRowQueries = ['Find all work meetings', 'What projects do i have coming up']; return ( -
+
{/* First row */}
@@ -98,10 +99,6 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi ))}
- {/* Left mask */} -
- {/* Right mask */} -
{/* Second row */} @@ -117,10 +114,6 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi ))}
- {/* Left mask */} -
- {/* Right mask */} -
); @@ -193,15 +186,6 @@ const ToolResponse = ({ toolName, result, args }: { toolName: string; result: an ) : null; - case Tools.WebSearch: - return ( -
-
-

{result}

-
-
- ); - case Tools.ComposeEmail: return result?.newBody ? (
@@ -212,13 +196,6 @@ const ToolResponse = ({ toolName, result, args }: { toolName: string; result: an ) : null; default: - if (result?.success) { - return ( -
- Operation completed successfully -
- ); - } return null; } }; @@ -270,19 +247,22 @@ export function AIChat({ } }, []); + useEffect(() => { + if (!['submitted', 'streaming'].includes(status)) { + scrollToBottom(); + } + }, [status, scrollToBottom]); + const editor = useComposeEditor({ placeholder: 'Ask Zero to do anything...', onLengthChange: () => setInput(editor.getText()), onKeydown(event) { - // Cmd+0 to toggle the AI sidebar (Added explicitly since TipTap editor doesn't bubble up the event) if (event.key === '0' && event.metaKey) { return toggleOpen(); } if (event.key === 'Enter' && !event.metaKey && !event.shiftKey) { - event.preventDefault(); - handleSubmit(event as unknown as React.FormEvent); - editor.commands.clearContent(true); + onSubmit(event as unknown as React.FormEvent); } }, }); @@ -291,12 +271,11 @@ export function AIChat({ e.preventDefault(); handleSubmit(e); editor.commands.clearContent(true); + setTimeout(() => { + scrollToBottom(); + }, 100); }; - useEffect(() => { - scrollToBottom(); - }, [messages, scrollToBottom]); - useEffect(() => { if (aiSidebarOpen === 'true') { editor.commands.focus(); @@ -306,7 +285,7 @@ export function AIChat({ return (
-
+
{chatMessages && !chatMessages.enabled ? (
setPricingDialog('true')} @@ -342,14 +321,18 @@ export function AIChat({ messages.map((message, index) => { const textParts = message.parts.filter((part) => part.type === 'text'); const toolParts = message.parts.filter((part) => part.type === 'tool-invocation'); - const toolResultOnlyTools = [Tools.WebSearch]; - const doesIncludeToolResult = toolParts.some((part) => - toolResultOnlyTools.includes(part.toolInvocation?.toolName as Tools), + const streamingTools = [Tools.WebSearch]; + const doesIncludeStreamingTool = toolParts.some( + (part) => + streamingTools.includes(part.toolInvocation?.toolName as Tools) && + part.toolInvocation?.result, ); return ( -
+
{toolParts.map((part, idx) => - part.toolInvocation && part.toolInvocation.result ? ( + part.toolInvocation && + part.toolInvocation.result && + !streamingTools.includes(part.toolInvocation.toolName as Tools) ? ( ) : null, )} - {!doesIncludeToolResult && textParts.length > 0 && ( + {!doesIncludeStreamingTool && textParts.length > 0 && (

{textParts.map( - (part) => part.text && {part.text || ' '}, + (part) => + part.text && ( + + {part.text || ' '} + + ), )}

)} @@ -376,7 +389,6 @@ export function AIChat({ ); }) )} -
{(status === 'submitted' || status === 'streaming') && (
@@ -390,6 +402,7 @@ export function AIChat({ {(status === 'error' || !!error) && (
Error, please try again later
)} +
diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index efe86ca501..9265469c00 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -18,6 +18,7 @@ import { X } from '../icons/icons'; import posthog from 'posthog-js'; import { toast } from 'sonner'; import './prosemirror.css'; +import type { Attachment } from '@/types'; // Define the draft type to include CC and BCC fields type DraftType = { @@ -27,8 +28,11 @@ type DraftType = { to?: string[]; cc?: string[]; bcc?: string[]; + attachments?: File[] }; + + // Define the connection type type Connection = { id: string; @@ -101,9 +105,9 @@ export function CreateEmail({ : ''; await sendEmail({ - to: data.to.map((email) => ({ email, name: email.split('@')[0] || email })), - cc: data.cc?.map((email) => ({ email, name: email.split('@')[0] || email })), - bcc: data.bcc?.map((email) => ({ email, name: email.split('@')[0] || email })), + to: data.to.map((email) => ({ email, name: email?.split('@')[0] || email })), + cc: data.cc?.map((email) => ({ email, name: email?.split('@')[0] || email })), + bcc: data.bcc?.map((email) => ({ email, name: email?.split('@')[0] || email })), subject: data.subject, message: data.message + zeroSignature, attachments: await serializeFiles(data.attachments), @@ -144,6 +148,7 @@ export function CreateEmail({ // Cast draft to our extended type that includes CC and BCC const typedDraft = draft as unknown as DraftType; + const handleDialogClose = (open: boolean) => { setIsComposeOpen(open ? 'true' : null); if (!open) { @@ -151,6 +156,26 @@ export function CreateEmail({ } }; + const base64ToFile = (base64: string, filename: string, mimeType: string): File | null => { + try { + const byteString = atob(base64); + const byteArray = new Uint8Array(byteString.length); + for (let i = 0; i < byteString.length; i++) { + byteArray[i] = byteString.charCodeAt(i); + } + return new File([byteArray], filename, { type: mimeType }); + } catch (error) { + console.error('Failed to convert base64 to file', error) + return null; + } + } + + // convert the attachments into File[] + const files: File[] = ((typedDraft?.attachments as Attachment[] | undefined) || []) + .map((att: Attachment) => base64ToFile(att.body, att.filename, att.mimeType)) + .filter((file): file is File => file !== null); + + return ( <> @@ -196,6 +221,7 @@ export function CreateEmail({ setIsComposeOpen(null); setDraftId(null); }} + initialAttachments={files} initialSubject={typedDraft?.subject || initialSubject} autofocus={false} settingsLoading={settingsLoading} diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index fbc0571c04..e46a379fc8 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -13,20 +13,27 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Check, Command, Loader, Paperclip, Plus, X as XIcon } from 'lucide-react'; + +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; +import { Check, Command, Loader, Paperclip, Plus, Type, X as XIcon } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { TextEffect } from '@/components/motion-primitives/text-effect'; +import { ImageCompressionSettings } from './image-compression-settings'; import { useActiveConnection } from '@/hooks/use-connections'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useEmailAliases } from '@/hooks/use-email-aliases'; +import type { ImageQuality } from '@/lib/image-compression'; import useComposeEditor from '@/hooks/use-compose-editor'; import { CurvedArrow, Sparkles, X } from '../icons/icons'; +import { compressImages } from '@/lib/image-compression'; +import { gitHubEmojis } from '@tiptap/extension-emoji'; import { AnimatePresence, motion } from 'motion/react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Avatar, AvatarFallback } from '../ui/avatar'; import { useTRPC } from '@/providers/query-provider'; import { useMutation } from '@tanstack/react-query'; import { useSettings } from '@/hooks/use-settings'; +import { useIsMobile } from '@/hooks/use-mobile'; import { cn, formatFileSize } from '@/lib/utils'; import { useThread } from '@/hooks/use-threads'; import { serializeFiles } from '@/lib/schemas'; @@ -35,13 +42,11 @@ import { EditorContent } from '@tiptap/react'; import { useForm } from 'react-hook-form'; import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; +import { Toolbar } from './toolbar'; import pluralize from 'pluralize'; import { toast } from 'sonner'; import { z } from 'zod'; -import { ImageCompressionSettings } from './image-compression-settings'; -import { compressImages } from '@/lib/image-compression'; -import type { ImageQuality } from '@/lib/image-compression'; -import { useIsMobile } from '@/hooks/use-mobile'; +const shortcodeRegex = /:([a-zA-Z0-9_+-]+):/g; type ThreadContent = { from: string; @@ -76,8 +81,13 @@ interface EmailComposerProps { } const isValidEmail = (email: string): boolean => { - const emailRegex = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - return emailRegex.test(email); + // for format like test@example.com + const simpleEmailRegex = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + + // for format like name + const displayNameEmailRegex = /^.+\s*<\s*[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\s*>$/; + + return simpleEmailRegex.test(email) || displayNameEmailRegex.test(email); }; const schema = z.object({ @@ -141,7 +151,7 @@ export function EmailComposer({ const [imageQuality, setImageQuality] = useState( settings?.settings?.imageCompression || 'medium', ); - + const [toggleToolbar, setToggleToolbar] = useState(false); const processAndSetAttachments = async ( filesToProcess: File[], quality: ImageQuality, @@ -190,7 +200,10 @@ export function EmailComposer({ }); if (totalOriginalSize > totalCompressedSize) { - const savings = (((totalOriginalSize - totalCompressedSize) / totalOriginalSize) * 100).toFixed(1); + const savings = ( + ((totalOriginalSize - totalCompressedSize) / totalOriginalSize) * + 100 + ).toFixed(1); if (parseFloat(savings) > 0.1) { toast.success(`Images compressed: ${savings}% smaller`); } @@ -686,6 +699,16 @@ export function EmailComposer({ await processAndSetAttachments(originalAttachments, newQuality, true); }; + const replaceEmojiShortcodes = (text: string): string => { + if (!text.trim().length || !text.includes(':')) return text; + return text.replace(shortcodeRegex, (match, shortcode): string => { + const emoji = gitHubEmojis.find( + (e) => e.shortcodes.includes(shortcode) || e.name === shortcode, + ); + return emoji?.emoji ?? match; + }); + }; + return (
- {email} + + {email} +
{/* Bottom Actions */} -
-
+
+
+ {toggleToolbar && }
+ + Formatting options + +
@@ -1538,8 +1585,8 @@ export function EmailComposer({ Attachment Warning - Looks like you mentioned an attachment in your message, but there are no files attached. - Are you sure you want to send this email? + Looks like you mentioned an attachment in your message, but there are no files + attached. Are you sure you want to send this email? diff --git a/apps/mail/components/create/extensions.ts b/apps/mail/components/create/extensions.ts index a0114f5fcb..e6a6930aac 100644 --- a/apps/mail/components/create/extensions.ts +++ b/apps/mail/components/create/extensions.ts @@ -104,12 +104,12 @@ const horizontalRule = HorizontalRule.configure({ const starterKit = StarterKit.configure({ bulletList: { HTMLAttributes: { - class: cx('list-disc list-outside leading-3 -mt-2'), + class: cx('list-disc list-outside leading-2 -mt-2'), }, }, orderedList: { HTMLAttributes: { - class: cx('list-decimal list-outside leading-3 -mt-2'), + class: cx('list-decimal list-outside leading-2 -mt-2'), }, }, listItem: { @@ -119,7 +119,13 @@ const starterKit = StarterKit.configure({ }, blockquote: { HTMLAttributes: { - class: cx('border-l-4 border-primary'), + class: cx('border-l-2 border-primary'), + }, + }, + heading: { + levels: [1, 2, 3], + HTMLAttributes: { + class: cx('text-primary'), }, }, codeBlock: { diff --git a/apps/mail/components/create/toolbar.tsx b/apps/mail/components/create/toolbar.tsx new file mode 100644 index 0000000000..1c090f6661 --- /dev/null +++ b/apps/mail/components/create/toolbar.tsx @@ -0,0 +1,244 @@ +import { + Bold, + Italic, + Strikethrough, + Underline, + Code, + Link as LinkIcon, + List, + ListOrdered, + Heading1, + Heading2, + Heading3, + Undo2, + Redo2, + TextQuote, +} from 'lucide-react'; + +import { TooltipContent, TooltipProvider, TooltipTrigger, Tooltip } from '../ui/tooltip'; +import { Separator } from '@/components/ui/separator'; +import { Button } from '../ui/button'; + +import type { Editor } from '@tiptap/core'; +import { m } from '@/paraglide/messages'; + +export const Toolbar = ({ editor }: { editor: Editor | null }) => { + if (!editor) return null; + + return ( +
+ +
+
+
+ + + + + Undo + + + + + + + Redo + +
+ +
+ + + + + H1 + + + + + + H2 + + + + + + H3 + +
+ +
+ + + + + {m['pages.createEmail.editor.menuBar.bold']()} + + + + + + {m['pages.createEmail.editor.menuBar.italic']()} + + + + + + + {m['pages.createEmail.editor.menuBar.strikethrough']()} + + + + + + + {m['pages.createEmail.editor.menuBar.underline']()} + +
+ + + +
+ + + + + + {m['pages.createEmail.editor.menuBar.bulletList']()} + + + + + + + + {m['pages.createEmail.editor.menuBar.orderedList']()} + + + + + + + Block Quote + +
+
+
+
+
+ ); +}; diff --git a/apps/mail/components/icons/empty-state-svg.tsx b/apps/mail/components/icons/empty-state-svg.tsx new file mode 100644 index 0000000000..ef2cd422a2 --- /dev/null +++ b/apps/mail/components/icons/empty-state-svg.tsx @@ -0,0 +1,191 @@ +import { useTheme } from 'next-themes'; + +interface EmptyStateSVGProps { + width?: number; + height?: number; + className?: string; +} + +interface EmptyStateBaseProps extends EmptyStateSVGProps { + isDarkTheme?: boolean; +} + +const EmptyStateBase = ({ width = 200, height = 200, className, isDarkTheme = true }: EmptyStateBaseProps) => { + // Theme-specific values + const viewBox = isDarkTheme ? "0 0 192 192" : "0 0 192 198"; + const bgFill = isDarkTheme ? "#141414" : "#FAFAFA"; + const bgOpacity = isDarkTheme ? "0.25" : "1"; + const borderColor = isDarkTheme ? "white" : "#DBDBDB"; + const borderOpacity = isDarkTheme ? "0.15" : "1"; + const borderWidth = isDarkTheme ? "1" : "0.5"; + + // Icon-specific elements - only light theme uses these + const filterElements = !isDarkTheme ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : null; + + // Configure fill colors for elements + const clipFill = isDarkTheme ? "white" : "white"; + const envelopeLetterFill = isDarkTheme ? "white" : "#B0B0B0"; + const envelopeLetterOpacity = isDarkTheme ? "0.3" : "1"; + const lineColors = !isDarkTheme + ? ["#E7E7E7", "#F0F0F0", "#F6F6F6", "#FAFAFA"] + : [ + "white", "white", "white", "white" + ]; + const lineOpacities = isDarkTheme + ? ["0.1", "0.075", "0.05", "0.025"] + : ["1", "1", "1", "1"]; + + // Paint definitions + const paint0Stop0Color = isDarkTheme ? "white" : "white"; + const paint0Stop0Opacity = isDarkTheme ? "0.1" : "1"; + const paint0Stop1Color = isDarkTheme ? "white" : "white"; + const paint0Stop1Opacity = isDarkTheme ? "0.05" : "1"; + + const paint1Stop0Color = "white"; + const paint1Stop0Opacity = "0.1"; + const paint1Stop1Color = "#323232"; + const paint1Stop1Opacity = "0"; + + const paint2Stop0Color = isDarkTheme ? "white" : "white"; + const paint2Stop0Opacity = isDarkTheme ? "0.1" : "1"; + const paint2Stop1Color = isDarkTheme ? "white" : "white"; + const paint2Stop1Opacity = isDarkTheme ? "0.05" : "1"; + + return ( + + {/* Main background circle */} + {isDarkTheme ? ( + + ) : ( + + )} + + {/* Border */} + {isDarkTheme ? ( + + ) : ( + + )} + + {/* Envelope shape - shadow layer */} + + + + + + {/* Main envelope */} + + + + + {/* Envelope details */} + + + + + + {/* Envelope content lines */} + + + + + + + {/* Envelope border */} + + + + {/* Gradients and clips */} + + {filterElements} + + {/* Gradients for coloring */} + + + + + + + + + + + + + + + + + + {/* Clip paths */} + + + + + + + + + ); +}; + +export const EmptyState = (props: EmptyStateSVGProps) => { + return ; +}; + +export const EmptyStateLight = (props: EmptyStateSVGProps) => { + return ; +}; + +export const EmptyStateIcon = ({ width = 200, height = 200, className }: EmptyStateSVGProps) => { + const { resolvedTheme } = useTheme(); + + // Explicitly check for 'dark' theme, use light theme as fallback for all other cases + return resolvedTheme === 'dark' ? ( + + ) : ( + + ); +}; \ No newline at end of file diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index 53306ced90..c0285c8c14 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -55,6 +55,7 @@ import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; import { Badge } from '../ui/badge'; import { format } from 'date-fns'; +import { cleanHtml } from '@/lib/email-utils'; import { toast } from 'sonner'; // HTML escaping function to prevent XSS attacks @@ -1631,8 +1632,10 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: {m['common.mailDisplay.print']()} + {(emailData.attachments?.length ?? 0) > 0 && ( { e.stopPropagation(); e.preventDefault(); @@ -1645,6 +1648,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: Download All Attachments + )}
diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index ff53f96e97..fbce38fedb 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -23,18 +23,20 @@ import { useState, type ComponentProps, } from 'react'; +import { useIsFetching, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-state'; import { focusedIndexAtom, useMailNavigation } from '@/hooks/use-mail-navigation'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import type { MailSelectMode, ParsedMessage, ThreadProps } from '@/types'; +import type { ParsedDraft } from '../../../server/src/lib/driver/types'; import { ThreadContextMenu } from '@/components/context/thread-context'; import { useOptimisticActions } from '@/hooks/use-optimistic-actions'; -import { useIsFetching, useQueryClient } from '@tanstack/react-query'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useMail, type Config } from '@/components/mail/use-mail'; import { type ThreadDestination } from '@/lib/thread-actions'; import { useThread, useThreads } from '@/hooks/use-threads'; import { useSearchValue } from '@/hooks/use-search-value'; +import { EmptyStateIcon } from '../icons/empty-state-svg'; import { highlightText } from '@/lib/email-utils.client'; import { useHotkeysContext } from 'react-hotkeys-hook'; import { AnimatePresence, motion } from 'motion/react'; @@ -79,85 +81,76 @@ const Thread = memo( const [, setActiveReplyId] = useQueryState('activeReplyId'); const [focusedIndex, setFocusedIndex] = useAtom(focusedIndexAtom); - // const latestReceivedMessage = useMemo(() => { - // if (!getThreadData?.messages) return getThreadData?.latest; + const { latestMessage, idToUse, cleanName } = useMemo(() => { + const latestMessage = getThreadData?.latest; + const idToUse = latestMessage?.threadId ?? latestMessage?.id; + const cleanName = latestMessage?.sender?.name + ? latestMessage.sender.name.trim().replace(/^['"]|['"]$/g, '') + : ''; - // const nonDraftMessages = getThreadData.messages.filter((msg) => !msg.isDraft); - // if (nonDraftMessages.length === 0) return getThreadData?.latest; - - // return ( - // nonDraftMessages.sort((a, b) => { - // const dateA = new Date(a.receivedOn).getTime(); - // const dateB = new Date(b.receivedOn).getTime(); - // return dateB - dateA; - // })[0] || getThreadData?.latest - // ); - // }, [getThreadData?.messages, getThreadData?.latest]); - - const latestMessage = getThreadData?.latest; - const idToUse = useMemo(() => latestMessage?.threadId ?? latestMessage?.id, [latestMessage]); + return { latestMessage, idToUse, cleanName }; + }, [getThreadData?.latest]); const { data: settingsData } = useSettings(); const queryClient = useQueryClient(); - // Check if thread has notes - const { data: threadNotes } = useThreadNotes(idToUse || ''); - const hasNotes = useMemo(() => { - return (threadNotes?.notes && threadNotes.notes.length > 0) || false; - }, [threadNotes?.notes]); + // // Check if thread has notes + // const { data: threadNotes } = useThreadNotes(idToUse || ''); + // const hasNotes = useMemo(() => { + // return (threadNotes?.notes && threadNotes.notes.length > 0) || false; + // }, [threadNotes?.notes]); const optimisticState = useOptimisticThreadState(idToUse ?? ''); - const displayStarred = useMemo(() => { - if (optimisticState.optimisticStarred !== null) { - return optimisticState.optimisticStarred; - } - return getThreadData?.latest?.tags?.some((tag) => tag.name === 'STARRED') ?? false; - }, [optimisticState.optimisticStarred, getThreadData?.latest?.tags]); - - const displayImportant = useMemo(() => { - if (optimisticState.optimisticImportant !== null) { - return optimisticState.optimisticImportant; - } - return getThreadData?.latest?.tags?.some((tag) => tag.name === 'IMPORTANT') ?? false; - }, [optimisticState.optimisticImportant, getThreadData?.latest?.tags]); - - const displayUnread = useMemo(() => { - if (optimisticState.optimisticRead !== null) { - return !optimisticState.optimisticRead; - } - return getThreadData?.hasUnread ?? false; - }, [optimisticState.optimisticRead, getThreadData?.hasUnread]); - - const optimisticLabels = useMemo(() => { - if (!getThreadData?.labels) return []; - - let labels = [...getThreadData.labels]; - const hasStarredLabel = labels.some((label) => label.name === 'STARRED'); - - if (optimisticState.optimisticStarred !== null) { - if (optimisticState.optimisticStarred && !hasStarredLabel) { - labels.push({ id: 'starred-optimistic', name: 'STARRED' }); - } else if (!optimisticState.optimisticStarred && hasStarredLabel) { - labels = labels.filter((label) => label.name !== 'STARRED'); + const { displayStarred, displayImportant, displayUnread, optimisticLabels } = useMemo(() => { + const displayStarred = + optimisticState.optimisticStarred !== null + ? optimisticState.optimisticStarred + : (getThreadData?.latest?.tags?.some((tag) => tag.name === 'STARRED') ?? false); + + const displayImportant = + optimisticState.optimisticImportant !== null + ? optimisticState.optimisticImportant + : (getThreadData?.latest?.tags?.some((tag) => tag.name === 'IMPORTANT') ?? false); + + const displayUnread = + optimisticState.optimisticRead !== null + ? !optimisticState.optimisticRead + : (getThreadData?.hasUnread ?? false); + + let labels: { id: string; name: string }[] = []; + if (getThreadData?.labels) { + labels = [...getThreadData.labels]; + const hasStarredLabel = labels.some((label) => label.name === 'STARRED'); + + if (optimisticState.optimisticStarred !== null) { + if (optimisticState.optimisticStarred && !hasStarredLabel) { + labels.push({ id: 'starred-optimistic', name: 'STARRED' }); + } else if (!optimisticState.optimisticStarred && hasStarredLabel) { + labels = labels.filter((label) => label.name !== 'STARRED'); + } } - } - if (optimisticState.optimisticLabels) { - labels = labels.filter( - (label) => !optimisticState.optimisticLabels.removedLabelIds.includes(label.id), - ); + if (optimisticState.optimisticLabels) { + labels = labels.filter( + (label) => !optimisticState.optimisticLabels.removedLabelIds.includes(label.id), + ); - optimisticState.optimisticLabels.addedLabelIds.forEach((labelId) => { - if (!labels.some((label) => label.id === labelId)) { - labels.push({ id: labelId, name: labelId }); - } - }); + optimisticState.optimisticLabels.addedLabelIds.forEach((labelId) => { + if (!labels.some((label) => label.id === labelId)) { + labels.push({ id: labelId, name: labelId }); + } + }); + } } - return labels; + return { displayStarred, displayImportant, displayUnread, optimisticLabels: labels }; }, [ - getThreadData?.labels, optimisticState.optimisticStarred, + optimisticState.optimisticImportant, + optimisticState.optimisticRead, + getThreadData?.latest?.tags, + getThreadData?.hasUnread, + getThreadData?.labels, optimisticState.optimisticLabels, ]); @@ -274,15 +267,15 @@ const Thread = memo( const isMailBulkSelected = idToUse ? mailState.bulkSelected.includes(idToUse) : false; - const isFolderInbox = folder === FOLDERS.INBOX || !folder; - const isFolderSpam = folder === FOLDERS.SPAM; - const isFolderSent = folder === FOLDERS.SENT; - const isFolderBin = folder === FOLDERS.BIN; - - const cleanName = useMemo(() => { - if (!latestMessage?.sender?.name) return ''; - return latestMessage.sender.name.trim().replace(/^['"]|['"]$/g, ''); - }, [latestMessage?.sender?.name]); + const { isFolderInbox, isFolderSpam, isFolderSent, isFolderBin } = useMemo( + () => ({ + isFolderInbox: folder === FOLDERS.INBOX || !folder, + isFolderSpam: folder === FOLDERS.SPAM, + isFolderSent: folder === FOLDERS.SENT, + isFolderBin: folder === FOLDERS.BIN, + }), + [folder], + ); // Check if thread has a draft const hasDraft = useMemo(() => { @@ -292,10 +285,7 @@ const Thread = memo( const content = latestMessage && getThreadData ? (
{ window.dispatchEvent(new CustomEvent('emailHover', { detail: { id: idToUse } })); @@ -418,7 +408,9 @@ const Thread = memo( ) : null}
-
+
Draft ) : null} - {hasNotes ? ( + {/* {hasNotes ? ( - ) : null} + ) : null} */}
{latestMessage.receivedOn ? ( @@ -628,7 +620,8 @@ const Thread = memo( ); const Draft = memo(({ message }: { message: { id: string } }) => { - const { data: draft } = useDraft(message.id); + const draftQuery = useDraft(message.id) as UseQueryResult; + const draft = draftQuery.data; const [, setComposeOpen] = useQueryState('isComposeOpen'); const [, setDraftId] = useQueryState('draftId'); const handleMailClick = useCallback(() => { @@ -695,7 +688,7 @@ const Draft = memo(({ message }: { message: { id: string } }) => { )} > - {cleanNameDisplay(draft?.to?.[0] || 'noname') || ''} + {cleanNameDisplay(draft?.to?.[0] || 'No Recipient') || ''}
@@ -738,8 +731,10 @@ export const MailList = memo( useThreads(); const trpc = useTRPC(); const isFetchingMail = useIsFetching({ queryKey: trpc.mail.get.queryKey() }) > 0; - const itemsRef = useRef(items); + const parentRef = useRef(null); + const vListRef = useRef(null); + useEffect(() => { itemsRef.current = items; }, [items]); @@ -776,9 +771,6 @@ export const MailList = memo( return () => window.removeEventListener('refreshMailList', handleRefresh); }, [refetch]); - const parentRef = useRef(null); - const vListRef = useRef(null); - const handleNavigateToThread = useCallback( (threadId: string | null) => { setThreadId(threadId); @@ -929,7 +921,7 @@ export const MailList = memo( const filteredItems = useMemo(() => items.filter((item) => item.id), [items]); - const Comp = folder === FOLDERS.DRAFT ? Draft : Thread; + const Comp = useMemo(() => (folder === FOLDERS.DRAFT ? Draft : Thread), [folder]); const vListRenderer = useCallback( (index: number) => { @@ -954,6 +946,7 @@ export const MailList = memo( ); }, [ + folder, filteredItems, focusedIndex, keyboardActive, @@ -983,13 +976,7 @@ export const MailList = memo( ) : !items || items.length === 0 ? (
- Empty Inbox +

It's empty here

diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index cd638b1e4f..353df3b5f7 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -385,14 +385,11 @@ export function MailLayout() { const isMobile = useIsMobile(); const navigate = useNavigate(); const { data: session, isPending } = useSession(); - const { data: connections } = useConnections(); const prevFolderRef = useRef(folder); const { enableScope, disableScope } = useHotkeysContext(); const { data: activeConnection } = useActiveConnection(); const { activeFilters, clearAllFilters } = useCommandPalette(); - const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useQueryState('isCommandPaletteOpen'); - - const { data: activeAccount } = useActiveConnection(); + const [, setIsCommandPaletteOpen] = useQueryState('isCommandPaletteOpen'); useEffect(() => { if (prevFolderRef.current !== folder && mail.bulkSelected.length > 0) { @@ -632,8 +629,8 @@ export function MailLayout() {

)} - - + {activeConnection?.id ? : null} + {activeConnection?.id ? : null}
diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index e125ebf4b3..d9bb5ef4db 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -14,6 +14,7 @@ import { Trash, X, } from '../icons/icons'; +import { EmptyStateIcon } from '../icons/empty-state-svg'; import { DropdownMenu, DropdownMenuContent, @@ -38,6 +39,7 @@ import type { ParsedMessage, Attachment } from '@/types'; import { MailDisplaySkeleton } from './mail-skeleton'; import { useTRPC } from '@/providers/query-provider'; import { useIsMobile } from '@/hooks/use-mobile'; +import { cleanHtml } from '@/lib/email-utils'; import { Button } from '@/components/ui/button'; import { useStats } from '@/hooks/use-stats'; import ReplyCompose from './reply-composer'; @@ -610,7 +612,7 @@ export function ThreadDisplay() { @@ -752,12 +754,7 @@ export function ThreadDisplay() { {!id ? (
- Empty Thread +

It's empty here

diff --git a/apps/mail/components/ui/ai-sidebar.tsx b/apps/mail/components/ui/ai-sidebar.tsx index 2fce4311f3..b14b3c3628 100644 --- a/apps/mail/components/ui/ai-sidebar.tsx +++ b/apps/mail/components/ui/ai-sidebar.tsx @@ -348,11 +348,10 @@ function AISidebar({ className }: AISidebarProps) { const { isPro, track, refetch: refetchBilling } = useBilling(); const queryClient = useQueryClient(); const trpc = useTRPC(); - const [threadId, setThreadId] = useQueryState('threadId'); + const [threadId] = useQueryState('threadId'); const { folder } = useParams<{ folder: string }>(); const { refetch: refetchLabels } = useLabels(); const [searchValue] = useSearchValue(); - const { data: session } = useSession(); const { data: activeConnection } = useActiveConnection(); const agent = useAgent({ @@ -363,7 +362,6 @@ function AISidebar({ className }: AISidebarProps) { const chatState = useAgentChat({ agent, - initialMessages: [], maxSteps: 5, body: { threadId: threadId ?? undefined, diff --git a/apps/mail/components/ui/app-sidebar.tsx b/apps/mail/components/ui/app-sidebar.tsx index a598f375c8..11548f688d 100644 --- a/apps/mail/components/ui/app-sidebar.tsx +++ b/apps/mail/components/ui/app-sidebar.tsx @@ -157,7 +157,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { className="mt-3 inline-flex h-7 w-full items-center justify-center gap-0.5 overflow-hidden rounded-lg bg-[#8B5CF6] px-2" >

-
+
Start 7 day free trial
diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx index 9f558cd7ea..4b35d91960 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -152,7 +152,7 @@ export function NavUser() { success: () => 'Signed out successfully!', error: 'Error signing out', async finally() { - await handleClearCache(); + // await handleClearCache(); window.location.href = '/login'; }, }); diff --git a/apps/mail/hooks/use-compose-editor.ts b/apps/mail/hooks/use-compose-editor.ts index 71d0c3b79a..7852d75e4a 100644 --- a/apps/mail/hooks/use-compose-editor.ts +++ b/apps/mail/hooks/use-compose-editor.ts @@ -1,6 +1,7 @@ import { useEditor, type KeyboardShortcutCommand, Extension, generateJSON } from '@tiptap/react'; import { AutoComplete } from '@/components/create/editor-autocomplete'; import { defaultExtensions } from '@/components/create/extensions'; +import Emoji, { gitHubEmojis } from '@tiptap/extension-emoji'; import { FileHandler } from '@tiptap/extension-file-handler'; import Placeholder from '@tiptap/extension-placeholder'; import { Plugin, PluginKey } from '@tiptap/pm/state'; @@ -258,6 +259,11 @@ const useComposeEditor = ({ Placeholder.configure({ placeholder, }), + Emoji.configure({ + emojis: gitHubEmojis, + enableEmoticons: true, + // suggestion, + }), // breaks the image upload // ...(onAttachmentsChange // ? [ diff --git a/apps/mail/hooks/use-connections.ts b/apps/mail/hooks/use-connections.ts index 2242e0e386..999f4f0323 100644 --- a/apps/mail/hooks/use-connections.ts +++ b/apps/mail/hooks/use-connections.ts @@ -9,6 +9,14 @@ export const useConnections = () => { export const useActiveConnection = () => { const trpc = useTRPC(); - const connectionsQuery = useQuery(trpc.connections.getDefault.queryOptions()); + const connectionsQuery = useQuery( + trpc.connections.getDefault.queryOptions(void 0, { + staleTime: 1000 * 60 * 60, // 1 hour, + gcTime: 1000 * 60 * 60 * 24, // 24 hours + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + }), + ); return connectionsQuery; }; diff --git a/apps/mail/hooks/use-threads.ts b/apps/mail/hooks/use-threads.ts index f524701597..a9877373da 100644 --- a/apps/mail/hooks/use-threads.ts +++ b/apps/mail/hooks/use-threads.ts @@ -69,8 +69,8 @@ export const useThread = (threadId: string | null, historyId?: string | null) => const [_threadId] = useQueryState('threadId'); const id = threadId ? threadId : _threadId; const trpc = useTRPC(); - const { data } = useSettings(); - const { resolvedTheme } = useTheme(); + // const { data } = useSettings(); + // const { resolvedTheme } = useTheme(); const previousHistoryId = usePrevious(historyId ?? null); const queryClient = useQueryClient(); @@ -92,44 +92,44 @@ export const useThread = (threadId: string | null, historyId?: string | null) => ), ); - const isTrustedSender = useMemo( - () => - !!data?.settings?.externalImages || - !!data?.settings?.trustedSenders?.includes(threadQuery.data?.latest?.sender.email ?? ''), - [data?.settings, threadQuery.data?.latest?.sender.email], - ); + // const isTrustedSender = useMemo( + // () => + // !!data?.settings?.externalImages || + // !!data?.settings?.trustedSenders?.includes(threadQuery.data?.latest?.sender.email ?? ''), + // [data?.settings, threadQuery.data?.latest?.sender.email], + // ); const latestDraft = useMemo(() => { if (!threadQuery.data?.latest?.id) return undefined; return threadQuery.data.messages.findLast((e) => e.isDraft); }, [threadQuery]); - const { mutateAsync: processEmailContent } = useMutation( - trpc.mail.processEmailContent.mutationOptions(), - ); - - const prefetchEmailContent = async (message: ParsedMessage) => { - return queryClient.prefetchQuery({ - queryKey: ['email-content', message.id, isTrustedSender, resolvedTheme], - queryFn: async () => { - const result = await processEmailContent({ - html: message.decodedBody ?? '', - shouldLoadImages: isTrustedSender, - theme: (resolvedTheme as 'light' | 'dark') || 'light', - }); - - return { - html: result.processedHtml, - hasBlockedImages: result.hasBlockedImages, - }; - }, - }); - }; - - useEffect(() => { - if (!threadQuery.data?.latest?.id) return; - prefetchEmailContent(threadQuery.data.latest); - }, [threadQuery.data?.latest]); + // const { mutateAsync: processEmailContent } = useMutation( + // trpc.mail.processEmailContent.mutationOptions(), + // ); + + // const prefetchEmailContent = async (message: ParsedMessage) => { + // return queryClient.prefetchQuery({ + // queryKey: ['email-content', message.id, isTrustedSender, resolvedTheme], + // queryFn: async () => { + // const result = await processEmailContent({ + // html: message.decodedBody ?? '', + // shouldLoadImages: isTrustedSender, + // theme: (resolvedTheme as 'light' | 'dark') || 'light', + // }); + + // return { + // html: result.processedHtml, + // hasBlockedImages: result.hasBlockedImages, + // }; + // }, + // }); + // }; + + // useEffect(() => { + // if (!threadQuery.data?.latest?.id) return; + // prefetchEmailContent(threadQuery.data.latest); + // }, [threadQuery.data?.latest]); const isGroupThread = useMemo(() => { if (!threadQuery.data?.latest?.id) return false; diff --git a/apps/mail/lib/email-utils.ts b/apps/mail/lib/email-utils.ts index af84480ded..5ef6683b55 100644 --- a/apps/mail/lib/email-utils.ts +++ b/apps/mail/lib/email-utils.ts @@ -1,5 +1,6 @@ import * as emailAddresses from 'email-addresses'; import type { Sender } from '@/types'; +import DOMPurify from 'dompurify'; import Color from 'color'; export const fixNonReadableColors = ( @@ -207,3 +208,15 @@ export const wasSentWithTLS = (receivedHeaders: string[]) => { return false; }; + +// cleans up html string for xss attacks and returns html +export const cleanHtml = (html: string) => { + if (!html) return '

No email content available

'; + + try { + return DOMPurify.sanitize(html); + } catch (error) { + console.warn('DOMPurify Failed or not Available, falling back to Default HTML ', error); + return '

No email content available

'; + } +}; diff --git a/apps/mail/lib/trpc.server.ts b/apps/mail/lib/trpc.server.ts index 6210a468ac..78a5b127a7 100644 --- a/apps/mail/lib/trpc.server.ts +++ b/apps/mail/lib/trpc.server.ts @@ -8,7 +8,7 @@ export const getServerTrpc = (req: Request) => createTRPCClient({ links: [ httpBatchLink({ - maxItems: 8, + maxItems: 1, url: getUrl(), transformer: superjson, headers: req.headers, diff --git a/apps/mail/locales.ts b/apps/mail/locales.ts new file mode 100644 index 0000000000..98c280f558 --- /dev/null +++ b/apps/mail/locales.ts @@ -0,0 +1,177 @@ +export const locales = { + en: 'English', + ar: 'Arabic', + ca: 'Catalan', + cs: 'Czech', + de: 'German', + es: 'Spanish', + fr: 'French', + hi: 'Hindi', + nl: 'Dutch', + ja: 'Japanese', + ko: 'Korean', + lv: 'Latvian', + pl: 'Polish', + pt: 'Portuguese', + ru: 'Russian', + tr: 'Turkish', + hu: 'Hungarian', + fa: 'Persian', + vi: 'Vietnamese', + 'zh-CN': 'Chinese (Simplified)', + 'zh-TW': 'Chinese (Traditional)', + af: 'Afrikaans', + sq: 'Albanian', + am: 'Amharic', + hy: 'Armenian', + az: 'Azerbaijani', + eu: 'Basque', + be: 'Belarusian', + bn: 'Bengali', + bs: 'Bosnian', + bg: 'Bulgarian', + hr: 'Croatian', + da: 'Danish', + fi: 'Finnish', + gl: 'Galician', + ka: 'Georgian', + el: 'Greek', + gu: 'Gujarati', + ht: 'Haitian Creole', + ha: 'Hausa', + he: 'Hebrew', + is: 'Icelandic', + ig: 'Igbo', + id: 'Indonesian', + ga: 'Irish', + it: 'Italian', + jv: 'Javanese', + kn: 'Kannada', + kk: 'Kazakh', + km: 'Khmer', + rw: 'Kinyarwanda', + ku: 'Kurdish', + ky: 'Kyrgyz', + la: 'Latin', + lt: 'Lithuanian', + lb: 'Luxembourgish', + mk: 'Macedonian', + mg: 'Malagasy', + ms: 'Malay', + ml: 'Malayalam', + mt: 'Maltese', + mi: 'Maori', + mr: 'Marathi', + mn: 'Mongolian', + ne: 'Nepali', + nb: 'Norwegian Bokmål', + nn: 'Norwegian Nynorsk', + or: 'Odia', + ps: 'Pashto', + pa: 'Punjabi', + ro: 'Romanian', + sm: 'Samoan', + gd: 'Scottish Gaelic', + sr: 'Serbian', + st: 'Sesotho', + sn: 'Shona', + sd: 'Sindhi', + si: 'Sinhala', + sk: 'Slovak', + sl: 'Slovenian', + so: 'Somali', + su: 'Sundanese', + sw: 'Swahili', + sv: 'Swedish', + tl: 'Tagalog', + tg: 'Tajik', + ta: 'Tamil', + tt: 'Tatar', + te: 'Telugu', + th: 'Thai', + ti: 'Tigrinya', + to: 'Tongan', + tk: 'Turkmen', + uk: 'Ukrainian', + ur: 'Urdu', + ug: 'Uyghur', + uz: 'Uzbek', + cy: 'Welsh', + fy: 'Western Frisian', + xh: 'Xhosa', + yi: 'Yiddish', + yo: 'Yoruba', + zu: 'Zulu', + fil: 'Filipino', + dv: 'Divehi', + eo: 'Esperanto', + et: 'Estonian', + fo: 'Faroese', + fj: 'Fijian', + gsw: 'Swiss German', + haw: 'Hawaiian', + kl: 'Greenlandic', + ln: 'Lingala', + mh: 'Marshallese', + nv: 'Navajo', + ny: 'Chichewa', + qu: 'Quechua', + rap: 'Rapa Nui', + tvl: 'Tuvaluan', + war: 'Waray', + wo: 'Wolof', + ts: 'Tsonga', + tn: 'Tswana', + ve: 'Venda', + vo: 'Volapük', + wa: 'Walloon', + chr: 'Cherokee', + iu: 'Inuktitut', + oj: 'Ojibwe', + cr: 'Cree', + lo: 'Lao', + my: 'Burmese', + dz: 'Dzongkha', + bm: 'Bambara', + ee: 'Ewe', + ff: 'Fulah', + ki: 'Kikuyu', + lg: 'Ganda', + nd: 'North Ndebele', + nr: 'South Ndebele', + nso: 'Northern Sotho', + om: 'Oromo', + rn: 'Rundi', + sg: 'Sango', + ss: 'Swati', + ak: 'Akan', + tw: 'Twi', + bh: 'Bihari', + gn: 'Guarani', + ay: 'Aymara', + co: 'Corsican', + io: 'Ido', + ii: 'Sichuan Yi', + kw: 'Cornish', + li: 'Limburgish', + oc: 'Occitan', + os: 'Ossetic', + se: 'Northern Sami', + sc: 'Sardinian', + za: 'Zhuang', + kab: 'Kabyle', + ber: 'Berber', + an: 'Aragonese', + ast: 'Asturian', + br: 'Breton', + ckb: 'Central Kurdish', + cv: 'Chuvash', + mzn: 'Mazanderani', + pms: 'Piedmontese', + sah: 'Sakha', + szl: 'Silesian', + tcy: 'Tulu', + vec: 'Venetian', + wuu: 'Wu Chinese', + yue: 'Cantonese', +}; diff --git a/apps/mail/messages/ar.json b/apps/mail/messages/ar.json index f84e448423..03a72e62f3 100644 --- a/apps/mail/messages/ar.json +++ b/apps/mail/messages/ar.json @@ -401,6 +401,7 @@ "spam": "البريد المزعج", "archive": "الأرشيف", "bin": "سلة المحذوفات", + "livesupport": "الدعم المباشر", "feedback": "الملاحظات", "settings": "الإعدادات", "voice": "المساعد الصوتي" diff --git a/apps/mail/messages/ca.json b/apps/mail/messages/ca.json index 9dc677cd6b..5be164ca3f 100644 --- a/apps/mail/messages/ca.json +++ b/apps/mail/messages/ca.json @@ -401,6 +401,7 @@ "spam": "Contingut no desitjat", "archive": "Arxivats", "bin": "Paperera", + "livesupport": "Suport en directe", "feedback": "Feedback", "settings": "Configuració", "voice": "Assistent de veu" diff --git a/apps/mail/messages/cs.json b/apps/mail/messages/cs.json index 939f3fefe0..a3514b9255 100644 --- a/apps/mail/messages/cs.json +++ b/apps/mail/messages/cs.json @@ -401,6 +401,7 @@ "spam": "Nevyžádaná pošta", "archive": "Archiv", "bin": "Odstraněná pošta", + "livesupport": "Živá podpora", "feedback": "Zpětná vazba", "settings": "Nastavení", "voice": "Hlasový asistent" diff --git a/apps/mail/messages/de.json b/apps/mail/messages/de.json index 42793938a0..c52575fb44 100644 --- a/apps/mail/messages/de.json +++ b/apps/mail/messages/de.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archiv", "bin": "Papierkorb", + "livesupport": "Live-Support", "feedback": "Rückmeldung", "settings": "Einstellungen", "voice": "Sprachassistent" diff --git a/apps/mail/messages/en.json b/apps/mail/messages/en.json index 6aaacb8baa..98c8a2fb9e 100644 --- a/apps/mail/messages/en.json +++ b/apps/mail/messages/en.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archive", "bin": "Bin", + "livesupport": "Live Support", "feedback": "Feedback", "settings": "Settings", "voice": "Voice Assistant" diff --git a/apps/mail/messages/es.json b/apps/mail/messages/es.json index 7f7b466df3..4d28272c47 100644 --- a/apps/mail/messages/es.json +++ b/apps/mail/messages/es.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archivados", "bin": "Papelera de reciclaje", + "livesupport": "Soporte en vivo", "feedback": "Sugerencias", "settings": "Configuración", "voice": "Asistente de voz" diff --git a/apps/mail/messages/fa.json b/apps/mail/messages/fa.json index 6a9ce00e78..e806099f06 100644 --- a/apps/mail/messages/fa.json +++ b/apps/mail/messages/fa.json @@ -401,6 +401,7 @@ "spam": "هرزنامه", "archive": "آرشیو", "bin": "سطل زباله", + "livesupport": "پشتیبانی زنده", "feedback": "بازخورد", "settings": "تنظیمات", "voice": "دستیار صوتی" diff --git a/apps/mail/messages/fr.json b/apps/mail/messages/fr.json index 123cc97f34..488c979680 100644 --- a/apps/mail/messages/fr.json +++ b/apps/mail/messages/fr.json @@ -401,6 +401,7 @@ "spam": "Pourriels", "archive": "Archives", "bin": "Corbeille", + "livesupport": "Support en direct", "feedback": "Vos commentaires", "settings": "Paramètres", "voice": "Assistant vocal" diff --git a/apps/mail/messages/hi.json b/apps/mail/messages/hi.json index a0bb813c6e..5eac04d0e7 100644 --- a/apps/mail/messages/hi.json +++ b/apps/mail/messages/hi.json @@ -401,7 +401,8 @@ "spam": "स्पैम", "archive": "आर्काइव", "bin": "बिन", - "feedback": "Feedback", + "livesupport": "लाइव सपोर्ट", + "feedback": "प्रतिक्रिया", "settings": "सेटिंग्स", "voice": "वॉइस असिस्टेंट" }, diff --git a/apps/mail/messages/hu.json b/apps/mail/messages/hu.json index e2af343132..4b3c028aba 100644 --- a/apps/mail/messages/hu.json +++ b/apps/mail/messages/hu.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archívum", "bin": "Lomtár", + "livesupport": "Élő támogatás", "feedback": "Visszajelzés", "settings": "Beállítások", "voice": "Hangasszisztens" diff --git a/apps/mail/messages/ja.json b/apps/mail/messages/ja.json index a14e6e4b03..41d3d32a1e 100644 --- a/apps/mail/messages/ja.json +++ b/apps/mail/messages/ja.json @@ -401,6 +401,7 @@ "spam": "迷惑メール", "archive": "アーカイブ", "bin": "ごみ箱", + "livesupport": "ライブサポート", "feedback": "フィードバック", "settings": "設定", "voice": "音声アシスタント" diff --git a/apps/mail/messages/ko.json b/apps/mail/messages/ko.json index c2550440b6..e4fa26eef8 100644 --- a/apps/mail/messages/ko.json +++ b/apps/mail/messages/ko.json @@ -401,6 +401,7 @@ "spam": "스팸", "archive": "보관함", "bin": "휴지통", + "livesupport": "실시간 지원", "feedback": "피드백", "settings": "설정", "voice": "음성 비서" diff --git a/apps/mail/messages/lv.json b/apps/mail/messages/lv.json index 340796a12f..6e6f622af7 100644 --- a/apps/mail/messages/lv.json +++ b/apps/mail/messages/lv.json @@ -401,6 +401,7 @@ "spam": "Mēstules", "archive": "Arhīvs", "bin": "Miskaste", + "livesupport": "Tiešsaistes atbalsts", "feedback": "Atsauksmes", "settings": "Iestatījumi", "voice": "Balss asistents" diff --git a/apps/mail/messages/nl.json b/apps/mail/messages/nl.json index 836138b47f..9c099e23c5 100644 --- a/apps/mail/messages/nl.json +++ b/apps/mail/messages/nl.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archief", "bin": "Prullenbak", + "livesupport": "Live ondersteuning", "feedback": "Feedback", "settings": "Instellingen", "voice": "Spraakassistent" diff --git a/apps/mail/messages/pl.json b/apps/mail/messages/pl.json index 8682ba4f14..7f509df2e0 100644 --- a/apps/mail/messages/pl.json +++ b/apps/mail/messages/pl.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archiwum", "bin": "Kosz", + "livesupport": "Wsparcie na żywo", "feedback": "Opinie", "settings": "Ustawienia", "voice": "Asystent głosowy" diff --git a/apps/mail/messages/pt.json b/apps/mail/messages/pt.json index 61ea4190ff..fcf438dcbe 100644 --- a/apps/mail/messages/pt.json +++ b/apps/mail/messages/pt.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Arquivados", "bin": "Lixeira", + "livesupport": "Suporte ao vivo", "feedback": "Feedback", "settings": "Configurações", "voice": "Assistente de voz" diff --git a/apps/mail/messages/ru.json b/apps/mail/messages/ru.json index 5f391841e1..e2c5bb0e81 100644 --- a/apps/mail/messages/ru.json +++ b/apps/mail/messages/ru.json @@ -401,6 +401,7 @@ "spam": "Спам", "archive": "Архив", "bin": "Корзина", + "livesupport": "Поддержка в реальном времени", "feedback": "Отзывы", "settings": "Настройки", "voice": "Голосовой помощник" diff --git a/apps/mail/messages/tr.json b/apps/mail/messages/tr.json index 33c2e87491..238b3039ae 100644 --- a/apps/mail/messages/tr.json +++ b/apps/mail/messages/tr.json @@ -401,6 +401,7 @@ "spam": "İstenmeyen E-posta", "archive": "Arşiv", "bin": "Çöp", + "livesupport": "Canlı Destek", "feedback": "Geribildirim", "settings": "Ayarlar", "voice": "Sesli Asistan" diff --git a/apps/mail/messages/vi.json b/apps/mail/messages/vi.json index 00b4007ed1..d95ebba7b4 100644 --- a/apps/mail/messages/vi.json +++ b/apps/mail/messages/vi.json @@ -401,6 +401,7 @@ "spam": "Thư rác", "archive": "Lưu trữ", "bin": "Thùng rác", + "livesupport": "Hỗ trợ trực tuyến", "feedback": "Phản hồi", "settings": "Cài đặt", "voice": "Trợ lý giọng nói" diff --git a/apps/mail/messages/zh_CN.json b/apps/mail/messages/zh_CN.json index b96166f41b..af8d4353a1 100644 --- a/apps/mail/messages/zh_CN.json +++ b/apps/mail/messages/zh_CN.json @@ -401,6 +401,7 @@ "spam": "垃圾邮件", "archive": "存档", "bin": "回收站", + "livesupport": "在线支持", "feedback": "反馈", "settings": "设置", "voice": "语音助手" diff --git a/apps/mail/messages/zh_TW.json b/apps/mail/messages/zh_TW.json index 44b61ebf7a..f412931ac7 100644 --- a/apps/mail/messages/zh_TW.json +++ b/apps/mail/messages/zh_TW.json @@ -401,6 +401,7 @@ "spam": "垃圾郵件", "archive": "封存", "bin": "垃圾桶", + "livesupport": "即時支援", "feedback": "意見反饋", "settings": "設定", "voice": "語音助理" diff --git a/apps/mail/package.json b/apps/mail/package.json index d433fa75be..bd8ddb349d 100644 --- a/apps/mail/package.json +++ b/apps/mail/package.json @@ -35,6 +35,7 @@ "@tiptap/core": "2.23.0", "@tiptap/extension-bold": "2.23.0", "@tiptap/extension-document": "2.23.0", + "@tiptap/extension-emoji": "2.23.1", "@tiptap/extension-file-handler": "2.23.0", "@tiptap/extension-image": "2.23.0", "@tiptap/extension-link": "2.23.0", diff --git a/apps/mail/providers/query-provider.tsx b/apps/mail/providers/query-provider.tsx index 0e498c1789..2c0f429955 100644 --- a/apps/mail/providers/query-provider.tsx +++ b/apps/mail/providers/query-provider.tsx @@ -90,7 +90,7 @@ export const trpcClient = createTRPCClient({ transformer: superjson, url: getUrl(), methodOverride: 'POST', - maxItems: 8, + maxItems: 1, fetch: (url, options) => fetch(url, { ...options, credentials: 'include' }).then((res) => { const currentPath = new URL(window.location.href).pathname; diff --git a/apps/server/package.json b/apps/server/package.json index 4bf3f70f06..ee6834eda4 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,6 +24,7 @@ "@ai-sdk/openai": "^1.3.21", "@ai-sdk/perplexity": "1.1.9", "@ai-sdk/ui-utils": "1.2.11", + "@arcadeai/arcadejs": "1.8.1", "@coinbase/cookie-manager": "1.1.8", "@googleapis/gmail": "12.0.0", "@googleapis/people": "3.0.9", @@ -56,6 +57,7 @@ "hono-party": "^0.0.12", "jose": "6.0.11", "jsonrepair": "^3.12.0", + "mime-types": "3.0.1", "mimetext": "^3.0.27", "p-retry": "6.2.1", "partyserver": "^0.0.71", @@ -67,10 +69,9 @@ "string-strip-html": "^13.4.12", "superjson": "catalog:", "twilio": "5.7.0", - "wrangler": "catalog:", - "zod": "catalog:", "uuid": "11.1.0", - "mime-types": "3.0.1" + "wrangler": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@types/he": "1.2.3", diff --git a/apps/server/src/db/migrations/0035_uneven_shiva.sql b/apps/server/src/db/migrations/0035_uneven_shiva.sql new file mode 100644 index 0000000000..86335fc541 --- /dev/null +++ b/apps/server/src/db/migrations/0035_uneven_shiva.sql @@ -0,0 +1,46 @@ +ALTER TABLE "mail0_account" DROP CONSTRAINT "mail0_account_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_connection" DROP CONSTRAINT "mail0_connection_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_session" DROP CONSTRAINT "mail0_session_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_user_hotkeys" DROP CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_user_settings" DROP CONSTRAINT "mail0_user_settings_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_account" ADD CONSTRAINT "mail0_account_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_connection" ADD CONSTRAINT "mail0_connection_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_session" ADD CONSTRAINT "mail0_session_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_summary" ADD CONSTRAINT "mail0_summary_connection_id_mail0_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."mail0_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_user_hotkeys" ADD CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_user_settings" ADD CONSTRAINT "mail0_user_settings_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "account_user_id_idx" ON "mail0_account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "account_provider_user_id_idx" ON "mail0_account" USING btree ("provider_id","user_id");--> statement-breakpoint +CREATE INDEX "account_expires_at_idx" ON "mail0_account" USING btree ("access_token_expires_at");--> statement-breakpoint +CREATE INDEX "connection_user_id_idx" ON "mail0_connection" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "connection_expires_at_idx" ON "mail0_connection" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "connection_provider_id_idx" ON "mail0_connection" USING btree ("provider_id");--> statement-breakpoint +CREATE INDEX "early_access_is_early_access_idx" ON "mail0_early_access" USING btree ("is_early_access");--> statement-breakpoint +CREATE INDEX "jwks_created_at_idx" ON "mail0_jwks" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "note_user_id_idx" ON "mail0_note" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "note_thread_id_idx" ON "mail0_note" USING btree ("thread_id");--> statement-breakpoint +CREATE INDEX "note_user_thread_idx" ON "mail0_note" USING btree ("user_id","thread_id");--> statement-breakpoint +CREATE INDEX "note_is_pinned_idx" ON "mail0_note" USING btree ("is_pinned");--> statement-breakpoint +CREATE INDEX "oauth_access_token_user_id_idx" ON "mail0_oauth_access_token" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "oauth_access_token_client_id_idx" ON "mail0_oauth_access_token" USING btree ("client_id");--> statement-breakpoint +CREATE INDEX "oauth_access_token_expires_at_idx" ON "mail0_oauth_access_token" USING btree ("access_token_expires_at");--> statement-breakpoint +CREATE INDEX "oauth_application_user_id_idx" ON "mail0_oauth_application" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "oauth_application_disabled_idx" ON "mail0_oauth_application" USING btree ("disabled");--> statement-breakpoint +CREATE INDEX "oauth_consent_user_id_idx" ON "mail0_oauth_consent" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "oauth_consent_client_id_idx" ON "mail0_oauth_consent" USING btree ("client_id");--> statement-breakpoint +CREATE INDEX "oauth_consent_given_idx" ON "mail0_oauth_consent" USING btree ("consent_given");--> statement-breakpoint +CREATE INDEX "session_user_id_idx" ON "mail0_session" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "session_expires_at_idx" ON "mail0_session" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "summary_connection_id_idx" ON "mail0_summary" USING btree ("connection_id");--> statement-breakpoint +CREATE INDEX "summary_connection_id_saved_idx" ON "mail0_summary" USING btree ("connection_id","saved");--> statement-breakpoint +CREATE INDEX "summary_saved_idx" ON "mail0_summary" USING btree ("saved");--> statement-breakpoint +CREATE INDEX "user_hotkeys_shortcuts_idx" ON "mail0_user_hotkeys" USING btree ("shortcuts");--> statement-breakpoint +CREATE INDEX "user_settings_settings_idx" ON "mail0_user_settings" USING btree ("settings");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "mail0_verification" USING btree ("identifier");--> statement-breakpoint +CREATE INDEX "verification_expires_at_idx" ON "mail0_verification" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "writing_style_matrix_style_idx" ON "mail0_writing_style_matrix" USING btree ("style"); \ No newline at end of file diff --git a/apps/server/src/db/migrations/meta/0035_snapshot.json b/apps/server/src/db/migrations/meta/0035_snapshot.json new file mode 100644 index 0000000000..211511a509 --- /dev/null +++ b/apps/server/src/db/migrations/meta/0035_snapshot.json @@ -0,0 +1,1610 @@ +{ + "id": "0b7459fd-f13f-4111-b30a-f0bd16a5b406", + "prevId": "185ab778-1f86-44d6-ac1a-90e9399b1342", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.mail0_account": { + "name": "mail0_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_provider_user_id_idx": { + "name": "account_provider_user_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_expires_at_idx": { + "name": "account_expires_at_idx", + "columns": [ + { + "expression": "access_token_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_account_user_id_mail0_user_id_fk": { + "name": "mail0_account_user_id_mail0_user_id_fk", + "tableFrom": "mail0_account", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_connection": { + "name": "mail0_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_user_id_idx": { + "name": "connection_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_expires_at_idx": { + "name": "connection_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_provider_id_idx": { + "name": "connection_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_connection_user_id_mail0_user_id_fk": { + "name": "mail0_connection_user_id_mail0_user_id_fk", + "tableFrom": "mail0_connection", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_connection_user_id_email_unique": { + "name": "mail0_connection_user_id_email_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_early_access": { + "name": "mail0_early_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_early_access": { + "name": "is_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "has_used_ticket": { + "name": "has_used_ticket", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": { + "early_access_is_early_access_idx": { + "name": "early_access_is_early_access_idx", + "columns": [ + { + "expression": "is_early_access", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_early_access_email_unique": { + "name": "mail0_early_access_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_jwks": { + "name": "mail0_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "jwks_created_at_idx": { + "name": "jwks_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_note": { + "name": "mail0_note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "note_user_id_idx": { + "name": "note_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_thread_id_idx": { + "name": "note_thread_id_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_user_thread_idx": { + "name": "note_user_thread_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_is_pinned_idx": { + "name": "note_is_pinned_idx", + "columns": [ + { + "expression": "is_pinned", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_note_user_id_mail0_user_id_fk": { + "name": "mail0_note_user_id_mail0_user_id_fk", + "tableFrom": "mail0_note", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_access_token": { + "name": "mail0_oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_user_id_idx": { + "name": "oauth_access_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "access_token_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_oauth_access_token_access_token_unique": { + "name": "mail0_oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": [ + "access_token" + ] + }, + "mail0_oauth_access_token_refresh_token_unique": { + "name": "mail0_oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": [ + "refresh_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_application": { + "name": "mail0_oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_u_r_ls": { + "name": "redirect_u_r_ls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_application_user_id_idx": { + "name": "oauth_application_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_application_disabled_idx": { + "name": "oauth_application_disabled_idx", + "columns": [ + { + "expression": "disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_oauth_application_client_id_unique": { + "name": "mail0_oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_consent": { + "name": "mail0_oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_consent_user_id_idx": { + "name": "oauth_consent_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_consent_given_idx": { + "name": "oauth_consent_given_idx", + "columns": [ + { + "expression": "consent_given", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_session": { + "name": "mail0_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_expires_at_idx": { + "name": "session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_session_user_id_mail0_user_id_fk": { + "name": "mail0_session_user_id_mail0_user_id_fk", + "tableFrom": "mail0_session", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_session_token_unique": { + "name": "mail0_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_summary": { + "name": "mail0_summary", + "schema": "", + "columns": { + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "saved": { + "name": "saved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_reply": { + "name": "suggested_reply", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "summary_connection_id_idx": { + "name": "summary_connection_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_connection_id_saved_idx": { + "name": "summary_connection_id_saved_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "saved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_saved_idx": { + "name": "summary_saved_idx", + "columns": [ + { + "expression": "saved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_summary_connection_id_mail0_connection_id_fk": { + "name": "mail0_summary_connection_id_mail0_connection_id_fk", + "tableFrom": "mail0_summary", + "tableTo": "mail0_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user": { + "name": "mail0_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "default_connection_id": { + "name": "default_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_prompt": { + "name": "custom_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number_verified": { + "name": "phone_number_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_email_unique": { + "name": "mail0_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "mail0_user_phone_number_unique": { + "name": "mail0_user_phone_number_unique", + "nullsNotDistinct": false, + "columns": [ + "phone_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_hotkeys": { + "name": "mail0_user_hotkeys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "shortcuts": { + "name": "shortcuts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_hotkeys_shortcuts_idx": { + "name": "user_hotkeys_shortcuts_idx", + "columns": [ + { + "expression": "shortcuts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_user_hotkeys_user_id_mail0_user_id_fk": { + "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_hotkeys", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_settings": { + "name": "mail0_user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"autoRead\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"icon\":\"Lightning\",\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"icon\":\"Mail\",\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"icon\":\"User\",\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"icon\":\"Tag\",\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"icon\":\"Bell\",\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"icon\":\"ScanEye\",\"isDefault\":false}],\"imageCompression\":\"medium\"}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_settings_settings_idx": { + "name": "user_settings_settings_idx", + "columns": [ + { + "expression": "settings", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_user_settings_user_id_mail0_user_id_fk": { + "name": "mail0_user_settings_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_settings", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_settings_user_id_unique": { + "name": "mail0_user_settings_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_verification": { + "name": "mail0_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_writing_style_matrix": { + "name": "mail0_writing_style_matrix", + "schema": "", + "columns": { + "connectionId": { + "name": "connectionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "numMessages": { + "name": "numMessages", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "writing_style_matrix_style_idx": { + "name": "writing_style_matrix_style_idx", + "columns": [ + { + "expression": "style", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": { + "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk", + "tableFrom": "mail0_writing_style_matrix", + "tableTo": "mail0_connection", + "columnsFrom": [ + "connectionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mail0_writing_style_matrix_connectionId_pk": { + "name": "mail0_writing_style_matrix_connectionId_pk", + "columns": [ + "connectionId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/src/db/migrations/meta/_journal.json b/apps/server/src/db/migrations/meta/_journal.json index e1106690ec..530acb75a0 100644 --- a/apps/server/src/db/migrations/meta/_journal.json +++ b/apps/server/src/db/migrations/meta/_journal.json @@ -253,6 +253,13 @@ "when": 1751008013033, "tag": "0034_mushy_runaways", "breakpoints": true + }, + { + "idx": 35, + "version": "7", + "when": 1751568728663, + "tag": "0035_uneven_shiva", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index ffdd57a967..bbe8016dee 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -7,6 +7,7 @@ import { jsonb, primaryKey, unique, + index, } from 'drizzle-orm/pg-core'; import { defaultUserSettings } from '../lib/schemas'; @@ -26,63 +27,93 @@ export const user = createTable('user', { phoneNumberVerified: boolean('phone_number_verified'), }); -export const session = createTable('session', { - id: text('id').primaryKey(), - expiresAt: timestamp('expires_at').notNull(), - token: text('token').notNull().unique(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id') - .notNull() - .references(() => user.id), -}); +export const session = createTable( + 'session', + { + id: text('id').primaryKey(), + expiresAt: timestamp('expires_at').notNull(), + token: text('token').notNull().unique(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + }, + (t) => [ + index('session_user_id_idx').on(t.userId), + index('session_expires_at_idx').on(t.expiresAt), + ], +); -export const account = createTable('account', { - id: text('id').primaryKey(), - accountId: text('account_id').notNull(), - providerId: text('provider_id').notNull(), - userId: text('user_id') - .notNull() - .references(() => user.id), - accessToken: text('access_token'), - refreshToken: text('refresh_token'), - idToken: text('id_token'), - accessTokenExpiresAt: timestamp('access_token_expires_at'), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), - scope: text('scope'), - password: text('password'), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), -}); +export const account = createTable( + 'account', + { + id: text('id').primaryKey(), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + idToken: text('id_token'), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + scope: text('scope'), + password: text('password'), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + }, + (t) => [ + index('account_user_id_idx').on(t.userId), + index('account_provider_user_id_idx').on(t.providerId, t.userId), + index('account_expires_at_idx').on(t.accessTokenExpiresAt), + ], +); -export const userHotkeys = createTable('user_hotkeys', { - userId: text('user_id') - .primaryKey() - .references(() => user.id), - shortcuts: jsonb('shortcuts').notNull(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), -}); +export const userHotkeys = createTable( + 'user_hotkeys', + { + userId: text('user_id') + .primaryKey() + .references(() => user.id, { onDelete: 'cascade' }), + shortcuts: jsonb('shortcuts').notNull(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + }, + (t) => [index('user_hotkeys_shortcuts_idx').on(t.shortcuts)], +); -export const verification = createTable('verification', { - id: text('id').primaryKey(), - identifier: text('identifier').notNull(), - value: text('value').notNull(), - expiresAt: timestamp('expires_at').notNull(), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), -}); +export const verification = createTable( + 'verification', + { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + }, + (t) => [ + index('verification_identifier_idx').on(t.identifier), + index('verification_expires_at_idx').on(t.expiresAt), + ], +); -export const earlyAccess = createTable('early_access', { - id: text('id').primaryKey(), - email: text('email').notNull().unique(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - isEarlyAccess: boolean('is_early_access').notNull().default(false), - hasUsedTicket: text('has_used_ticket').default(''), -}); +export const earlyAccess = createTable( + 'early_access', + { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + isEarlyAccess: boolean('is_early_access').notNull().default(false), + hasUsedTicket: text('has_used_ticket').default(''), + }, + (t) => [index('early_access_is_early_access_idx').on(t.isEarlyAccess)], +); export const connection = createTable( 'connection', @@ -90,7 +121,7 @@ export const connection = createTable( id: text('id').primaryKey(), userId: text('user_id') .notNull() - .references(() => user.id), + .references(() => user.id, { onDelete: 'cascade' }), email: text('email').notNull(), name: text('name'), picture: text('picture'), @@ -102,45 +133,73 @@ export const connection = createTable( createdAt: timestamp('created_at').notNull(), updatedAt: timestamp('updated_at').notNull(), }, - (t) => [unique().on(t.userId, t.email)], + (t) => [ + unique().on(t.userId, t.email), + index('connection_user_id_idx').on(t.userId), + index('connection_expires_at_idx').on(t.expiresAt), + index('connection_provider_id_idx').on(t.providerId), + ], ); -export const summary = createTable('summary', { - messageId: text('message_id').primaryKey(), - content: text('content').notNull(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - connectionId: text('connection_id').notNull(), - saved: boolean('saved').notNull().default(false), - tags: text('tags'), - suggestedReply: text('suggested_reply'), -}); +export const summary = createTable( + 'summary', + { + messageId: text('message_id').primaryKey(), + content: text('content').notNull(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + connectionId: text('connection_id') + .notNull() + .references(() => connection.id, { onDelete: 'cascade' }), + saved: boolean('saved').notNull().default(false), + tags: text('tags'), + suggestedReply: text('suggested_reply'), + }, + (t) => [ + index('summary_connection_id_idx').on(t.connectionId), + index('summary_connection_id_saved_idx').on(t.connectionId, t.saved), + index('summary_saved_idx').on(t.saved), + ], +); // Testing -export const note = createTable('note', { - id: text('id').primaryKey(), - userId: text('user_id') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - threadId: text('thread_id').notNull(), - content: text('content').notNull(), - color: text('color').notNull().default('default'), - isPinned: boolean('is_pinned').default(false), - order: integer('order').notNull().default(0), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), -}); +export const note = createTable( + 'note', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + threadId: text('thread_id').notNull(), + content: text('content').notNull(), + color: text('color').notNull().default('default'), + isPinned: boolean('is_pinned').default(false), + order: integer('order').notNull().default(0), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (t) => [ + index('note_user_id_idx').on(t.userId), + index('note_thread_id_idx').on(t.threadId), + index('note_user_thread_idx').on(t.userId, t.threadId), + index('note_is_pinned_idx').on(t.isPinned), + ], +); -export const userSettings = createTable('user_settings', { - id: text('id').primaryKey(), - userId: text('user_id') - .notNull() - .references(() => user.id) - .unique(), - settings: jsonb('settings').notNull().default(defaultUserSettings), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), -}); +export const userSettings = createTable( + 'user_settings', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }) + .unique(), + settings: jsonb('settings').notNull().default(defaultUserSettings), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + }, + (t) => [index('user_settings_settings_idx').on(t.settings)], +); export const writingStyleMatrix = createTable( 'writing_style_matrix', @@ -162,51 +221,79 @@ export const writingStyleMatrix = createTable( primaryKey({ columns: [table.connectionId], }), + index('writing_style_matrix_style_idx').on(table.style), ]; }, ); -export const jwks = createTable('jwks', { - id: text('id').primaryKey(), - publicKey: text('public_key').notNull(), - privateKey: text('private_key').notNull(), - createdAt: timestamp('created_at').notNull(), -}); +export const jwks = createTable( + 'jwks', + { + id: text('id').primaryKey(), + publicKey: text('public_key').notNull(), + privateKey: text('private_key').notNull(), + createdAt: timestamp('created_at').notNull(), + }, + (t) => [index('jwks_created_at_idx').on(t.createdAt)], +); -export const oauthApplication = createTable('oauth_application', { - id: text('id').primaryKey(), - name: text('name'), - icon: text('icon'), - metadata: text('metadata'), - clientId: text('client_id').unique(), - clientSecret: text('client_secret'), - redirectURLs: text('redirect_u_r_ls'), - type: text('type'), - disabled: boolean('disabled'), - userId: text('user_id'), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), -}); +export const oauthApplication = createTable( + 'oauth_application', + { + id: text('id').primaryKey(), + name: text('name'), + icon: text('icon'), + metadata: text('metadata'), + clientId: text('client_id').unique(), + clientSecret: text('client_secret'), + redirectURLs: text('redirect_u_r_ls'), + type: text('type'), + disabled: boolean('disabled'), + userId: text('user_id'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + }, + (t) => [ + index('oauth_application_user_id_idx').on(t.userId), + index('oauth_application_disabled_idx').on(t.disabled), + ], +); -export const oauthAccessToken = createTable('oauth_access_token', { - id: text('id').primaryKey(), - accessToken: text('access_token').unique(), - refreshToken: text('refresh_token').unique(), - accessTokenExpiresAt: timestamp('access_token_expires_at'), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), - clientId: text('client_id'), - userId: text('user_id'), - scopes: text('scopes'), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), -}); +export const oauthAccessToken = createTable( + 'oauth_access_token', + { + id: text('id').primaryKey(), + accessToken: text('access_token').unique(), + refreshToken: text('refresh_token').unique(), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + clientId: text('client_id'), + userId: text('user_id'), + scopes: text('scopes'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + }, + (t) => [ + index('oauth_access_token_user_id_idx').on(t.userId), + index('oauth_access_token_client_id_idx').on(t.clientId), + index('oauth_access_token_expires_at_idx').on(t.accessTokenExpiresAt), + ], +); -export const oauthConsent = createTable('oauth_consent', { - id: text('id').primaryKey(), - clientId: text('client_id'), - userId: text('user_id'), - scopes: text('scopes'), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), - consentGiven: boolean('consent_given'), -}); +export const oauthConsent = createTable( + 'oauth_consent', + { + id: text('id').primaryKey(), + clientId: text('client_id'), + userId: text('user_id'), + scopes: text('scopes'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + consentGiven: boolean('consent_given'), + }, + (t) => [ + index('oauth_consent_user_id_idx').on(t.userId), + index('oauth_consent_client_id_idx').on(t.clientId), + index('oauth_consent_given_idx').on(t.consentGiven), + ], +); diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index 8fcc4d5406..b42c5d10ee 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -484,7 +484,7 @@ export class GoogleMailManager implements MailManager { throw new Error('Draft not found'); } - const parsedDraft = this.parseDraft(res.data); + const parsedDraft = await this.parseDraft(res.data); if (!parsedDraft) { throw new Error('Failed to parse draft'); } @@ -1111,7 +1111,8 @@ export class GoogleMailManager implements MailManager { raw: encodedMessage, }; } - private parseDraft(draft: gmail_v1.Schema$Draft) { + + private async parseDraft(draft: gmail_v1.Schema$Draft) { if (!draft.message) return null; const headers = draft.message.payload?.headers || []; @@ -1121,26 +1122,61 @@ export class GoogleMailManager implements MailManager { ?.value?.split(',') .map((e) => e.trim()) .filter(Boolean) || []; - const subject = headers.find((h) => h.name === 'Subject')?.value; - - let content = ''; - const payload = draft.message.payload; - - if (payload) { - if (payload.parts) { - const textPart = payload.parts.find((part) => part.mimeType === 'text/html'); - if (textPart?.body?.data) { - content = fromBinary(textPart.body.data); - } - } else if (payload.body?.data) { - content = fromBinary(payload.body.data); - } - } + const subject = headers.find((h) => h.name === 'Subject')?.value; + const cc = draft.message.payload?.headers?.find((h) => h.name === 'Cc')?.value?.split(',') || []; const bcc = draft.message.payload?.headers?.find((h) => h.name === 'Bcc')?.value?.split(',') || []; + + const payload = draft.message.payload; + let content = ''; + let attachments: { + filename: string; + mimeType: string; + size: number; + attachmentId: string; + headers: { name: string; value: string }[]; + body: string; + }[] = []; + + if (payload?.parts) { + // Get body + const htmlPart = payload.parts.find((part) => part.mimeType === 'text/html'); + if (htmlPart?.body?.data) { + content = fromBinary(htmlPart.body.data); + } + + // Get attachments + const attachmentParts = payload.parts.filter( + (part) => !!part.filename && !!part.body?.attachmentId + ); + + attachments = await Promise.all( + attachmentParts.map(async (part) => { + try { + const attachmentData = await this.getAttachment(draft.message!.id!, part.body!.attachmentId!); + return { + filename: part.filename || '', + mimeType: part.mimeType || '', + size: Number(part.body?.size || 0), + attachmentId: part.body!.attachmentId!, + headers: + part.headers?.map((h) => ({ + name: h.name ?? '', + value: h.value ?? '', + })) ?? [], + body: attachmentData ?? '', + }; + } catch (e) { + return null; + } + }) + ).then((a) => a.filter((a): a is NonNullable => a !== null)); + } else if (payload?.body?.data) { + content = fromBinary(payload.body.data); + } return { id: draft.id || '', @@ -1150,8 +1186,10 @@ export class GoogleMailManager implements MailManager { rawMessage: draft.message, cc, bcc, + attachments, }; } + private async withErrorHandler( operation: string, fn: () => Promise | T, diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts index 0e442833bf..28d9b784e8 100644 --- a/apps/server/src/lib/driver/types.ts +++ b/apps/server/src/lib/driver/types.ts @@ -24,7 +24,9 @@ export interface ParsedDraft { to?: string[]; subject?: string; content?: string; - rawMessage?: T; + rawMessage?: { + internalDate?: string; + }; cc?: string[]; bcc?: string[]; } diff --git a/apps/server/src/routes/agent/orchestrator.ts b/apps/server/src/routes/agent/orchestrator.ts new file mode 100644 index 0000000000..5e4129da83 --- /dev/null +++ b/apps/server/src/routes/agent/orchestrator.ts @@ -0,0 +1,84 @@ +import { streamText, tool, type DataStreamWriter, type ToolSet } from 'ai'; +import { perplexity } from '@ai-sdk/perplexity'; +import { env } from 'cloudflare:workers'; +import { Tools } from '../../types'; +import { z } from 'zod'; + +/** + * Orchestrator that handles the distinction between tools and agents. + * Tools execute and return results, while agents stream responses directly. + */ +export class ToolOrchestrator { + private dataStream: DataStreamWriter; + private streamingTools: Set = new Set([Tools.WebSearch]); + + constructor(dataStream: DataStreamWriter) { + this.dataStream = dataStream; + } + + /** + * Determines if a tool should be treated as an agent that streams + */ + isStreamingTool(toolName: string): boolean { + return this.streamingTools.has(toolName); + } + + /** + * Creates a streaming agent wrapper for tools that should stream responses directly + */ + createStreamingAgent(toolName: string, originalTool: any) { + if (!this.isStreamingTool(toolName)) { + return originalTool; + } + + // For webSearch, we want to stream the response directly without wrapping it as a tool result + if (toolName === Tools.WebSearch) { + return tool({ + description: 'Search the web for information using Perplexity AI', + parameters: z.object({ + query: z.string().describe('The query to search the web for'), + }), + execute: async ({ query }) => { + try { + const response = streamText({ + model: perplexity('sonar'), + messages: [ + { role: 'system', content: 'Be precise and concise.' }, + { role: 'system', content: 'Do not include sources in your response.' }, + { role: 'system', content: 'Do not use markdown formatting in your response.' }, + { role: 'user', content: query }, + ], + maxTokens: 1024, + }); + + // Stream the response directly to the data stream + response.mergeIntoDataStream(this.dataStream); + + // Return a placeholder result since the actual streaming happens above + return { type: 'streaming_response', toolName }; + } catch (error) { + console.error('Error searching the web:', error); + throw new Error('Failed to search the web'); + } + }, + }); + } + + return originalTool; + } + + /** + * Processes all tools and returns modified versions for streaming tools + */ + processTools(tools: T): T { + const processedTools = { ...tools }; + + for (const [toolName, toolInstance] of Object.entries(tools)) { + if (this.isStreamingTool(toolName)) { + processedTools[toolName as keyof T] = this.createStreamingAgent(toolName, toolInstance); + } + } + + return processedTools; + } +} diff --git a/apps/server/src/routes/agent/tools.ts b/apps/server/src/routes/agent/tools.ts index fd07d48d71..1147e3e268 100644 --- a/apps/server/src/routes/agent/tools.ts +++ b/apps/server/src/routes/agent/tools.ts @@ -1,9 +1,11 @@ +import { toZodToolSet, executeOrAuthorizeZodTool } from '@arcadeai/arcadejs/lib'; +import { generateText, streamText, tool, type DataStreamWriter } from 'ai'; import { composeEmail } from '../../trpc/routes/ai/compose'; import type { MailManager } from '../../lib/driver/types'; import { perplexity } from '@ai-sdk/perplexity'; +import { Arcade } from '@arcadeai/arcadejs'; import { colors } from '../../lib/prompts'; import { env } from 'cloudflare:workers'; -import { generateText, tool } from 'ai'; import { Tools } from '../../types'; import { z } from 'zod'; @@ -328,33 +330,52 @@ const deleteLabel = (driver: MailManager) => }, }); -export const webSearch = tool({ - description: 'Search the web for information using Perplexity AI', - parameters: z.object({ - query: z.string().describe('The query to search the web for'), - }), - execute: async ({ query }) => { - try { - const { text } = await generateText({ - model: perplexity('sonar'), - messages: [ - { role: 'system', content: 'Be precise and concise.' }, - { role: 'system', content: 'Do not include sources in your response.' }, - { role: 'system', content: 'Do not use markdown formatting in your response.' }, - { role: 'user', content: query }, - ], - maxTokens: 1024, - }); +const getGoogleTools = async (connectionId: string) => { + const arcade = new Arcade(); + const googleToolkit = await arcade.tools.list({ toolkit: 'google', limit: 30 }); + const googleTools = toZodToolSet({ + tools: googleToolkit.items, + client: arcade, + userId: connectionId, // Your app's internal ID for the user (an email, UUID, etc). It's used internally to identify your user in Arcade + executeFactory: executeOrAuthorizeZodTool, // Checks if tool is authorized and executes it, or returns authorization URL if needed + }); + return googleTools; +}; + +export const webSearch = (dataStream: DataStreamWriter) => + tool({ + description: 'Search the web for information using Perplexity AI', + parameters: z.object({ + query: z.string().describe('The query to search the web for'), + }), + execute: async ({ query }) => { + try { + const response = streamText({ + model: perplexity('sonar'), + messages: [ + { role: 'system', content: 'Be precise and concise.' }, + { role: 'system', content: 'Do not include sources in your response.' }, + { role: 'system', content: 'Do not use markdown formatting in your response.' }, + { role: 'user', content: query }, + ], + maxTokens: 1024, + }); - return text; - } catch (error) { - console.error('Error searching the web:', error); - throw new Error('Failed to search the web'); - } - }, -}); + response.mergeIntoDataStream(dataStream); -export const tools = (driver: MailManager, connectionId: string) => { + return { type: 'streaming_response', query }; + } catch (error) { + console.error('Error searching the web:', error); + throw new Error('Failed to search the web'); + } + }, + }); + +export const tools = async ( + driver: MailManager, + connectionId: string, + dataStream: DataStreamWriter, +) => { return { [Tools.GetThread]: getEmail(driver), [Tools.ComposeEmail]: composeEmailTool(connectionId), @@ -368,8 +389,9 @@ export const tools = (driver: MailManager, connectionId: string) => { [Tools.BulkDelete]: bulkDelete(driver), [Tools.BulkArchive]: bulkArchive(driver), [Tools.DeleteLabel]: deleteLabel(driver), - [Tools.AskZeroMailbox]: askZeroMailbox(connectionId), - [Tools.AskZeroThread]: askZeroThread(connectionId), - [Tools.WebSearch]: webSearch, + // [Tools.AskZeroMailbox]: askZeroMailbox(connectionId), + // [Tools.AskZeroThread]: askZeroThread(connectionId), + [Tools.WebSearch]: webSearch(dataStream), + // ...(await getGoogleTools(connectionId)), }; }; diff --git a/apps/server/src/routes/ai.ts b/apps/server/src/routes/ai.ts index 7234dd2a45..a1d3c21eeb 100644 --- a/apps/server/src/routes/ai.ts +++ b/apps/server/src/routes/ai.ts @@ -145,7 +145,7 @@ aiRouter.post('/call', async (c) => { const driver = connectionToDriver(connection); const { text } = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: systemPrompt, prompt: data.query, tools: { @@ -158,7 +158,7 @@ aiRouter.post('/call', async (c) => { console.log('[DEBUG] buildGmailSearchQuery', params); const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: params.query, }); diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 6df33dc171..0722432247 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -18,6 +18,7 @@ import type { IGetThreadResponse, MailManager } from '../lib/driver/types'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createSimpleAuth, type SimpleAuth } from '../lib/auth'; import { connectionToDriver } from '../lib/server-utils'; +import { ToolOrchestrator } from './agent/orchestrator'; import type { CreateDraftData } from '../lib/schemas'; import { FOLDERS, parseHeaders } from '../lib/utils'; import { env, RpcTarget } from 'cloudflare:workers'; @@ -352,7 +353,12 @@ export class ZeroAgent extends AIChatAgent { throw new Error('Unauthorized no driver or connectionId [2]'); } } - const tools = { ...authTools(this.driver, connectionId), buildGmailSearchQuery }; + const orchestrator = new ToolOrchestrator(dataStream); + const rawTools = { + ...(await authTools(this.driver, connectionId, dataStream)), + buildGmailSearchQuery, + }; + const tools = orchestrator.processTools(rawTools); const processedMessages = await processToolCalls( { messages: this.messages, @@ -363,7 +369,7 @@ export class ZeroAgent extends AIChatAgent { ); const result = streamText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), messages: processedMessages, tools, onFinish, @@ -689,7 +695,7 @@ export class ZeroAgent extends AIChatAgent { async buildGmailSearchQuery(query: string) { const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: query, }); @@ -1242,7 +1248,7 @@ export class ZeroMCP extends McpAgent { }, async (s) => { const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: s.query, }); @@ -1587,7 +1593,7 @@ const buildGmailSearchQuery = tool({ }), execute: async ({ query }) => { const result = await generateObject({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: query, schema: z.object({ diff --git a/apps/server/src/services/mcp-service/mcp.ts b/apps/server/src/services/mcp-service/mcp.ts index 71bb913509..822d29dc32 100644 --- a/apps/server/src/services/mcp-service/mcp.ts +++ b/apps/server/src/services/mcp-service/mcp.ts @@ -55,7 +55,7 @@ export class ZeroMCP extends McpAgent }, async (s) => { const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: s.query, }); diff --git a/apps/server/src/trpc/routes/ai/compose.ts b/apps/server/src/trpc/routes/ai/compose.ts index 9ab24a7cc3..f0fbc5b890 100644 --- a/apps/server/src/trpc/routes/ai/compose.ts +++ b/apps/server/src/trpc/routes/ai/compose.ts @@ -11,6 +11,7 @@ import { stripHtml } from 'string-strip-html'; import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import { z } from 'zod'; +import { env } from 'cloudflare:workers'; type ComposeEmailInput = { prompt: string; @@ -84,7 +85,7 @@ export async function composeEmail(input: ComposeEmailInput) { ]; const { text } = await generateText({ - model: openai('gpt-4o-mini'), + model: openai(env.OPENAI_MINI_MODEL || 'gpt-4o-mini'), messages: [ { role: 'system', @@ -273,7 +274,7 @@ const generateSubject = async (message: string, styleProfile?: WritingStyleMatri ); const { text } = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), messages: [ { role: 'system', diff --git a/apps/server/src/trpc/routes/ai/search.ts b/apps/server/src/trpc/routes/ai/search.ts index 51f26aba1f..88c0c4c24d 100644 --- a/apps/server/src/trpc/routes/ai/search.ts +++ b/apps/server/src/trpc/routes/ai/search.ts @@ -5,6 +5,7 @@ import { import { activeDriverProcedure } from '../../trpc'; import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; +import { env } from 'cloudflare:workers'; import { z } from 'zod'; export const generateSearchQuery = activeDriverProcedure @@ -21,7 +22,7 @@ export const generateSearchQuery = activeDriverProcedure : ''; const result = await generateObject({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: systemPrompt, prompt: input.query, schema: z.object({ diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index 9df2dee965..780b4b313f 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -12,6 +12,10 @@ services: RESEND_API_KEY: ${RESEND_API_KEY} AI_SYSTEM_PROMPT: ${AI_SYSTEM_PROMPT} GROQ_API_KEY: ${GROQ_API_KEY} + PERPLEXITY_API_KEY: ${PERPLEXITY_API_KEY} + OPENAI_API_KEY: ${OPENAI_API_KEY} + OPENAI_MODEL: ${OPENAI_MODEL} + OPENAI_MINI_MODEL: ${OPENAI_MINI_MODEL} NEXT_PUBLIC_ELEVENLABS_AGENT_ID: ${NEXT_PUBLIC_ELEVENLABS_AGENT_ID} NEXT_PUBLIC_IMAGE_PROXY: ${NEXT_PUBLIC_IMAGE_PROXY} NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY} diff --git a/i18n.lock b/i18n.lock index 33136c09e7..c6d90c6fe4 100644 --- a/i18n.lock +++ b/i18n.lock @@ -852,6 +852,7 @@ checksums: navigation/sidebar/spam: 904064026d3ce87cd872e0b819a15310 navigation/sidebar/archive: fa813ab3074103e5daad07462af25789 navigation/sidebar/bin: e95691895f3a89d896838716e48290bd + navigation/sidebar/livesupport: 087e6998c099b3c08c5ade57c5f68752 navigation/sidebar/feedback: 6fac88806e0c269a30777b283988c61c navigation/sidebar/settings: 8df6777277469c1fd88cc18dde2f1cc3 navigation/sidebar/voice: 81a94ad8770dca9c3cbb5a88329b6a6f diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a300f3660..1a1df2feab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,9 @@ importers: '@tiptap/extension-document': specifier: 2.23.0 version: 2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0)) + '@tiptap/extension-emoji': + specifier: 2.23.1 + version: 2.23.1(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0)(@tiptap/suggestion@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0))(emojibase@16.0.0) '@tiptap/extension-file-handler': specifier: 2.23.0 version: 2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/extension-text-style@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))) @@ -482,6 +485,9 @@ importers: '@ai-sdk/ui-utils': specifier: 1.2.11 version: 1.2.11(zod@3.25.67) + '@arcadeai/arcadejs': + specifier: 1.8.1 + version: 1.8.1 '@coinbase/cookie-manager': specifier: 1.1.8 version: 1.1.8 @@ -759,6 +765,9 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@arcadeai/arcadejs@1.8.1': + resolution: {integrity: sha512-ZTj2UvdfFmFn1as4gdDiZD8nbnEFZcZUzH9XtTmjRbgf/1V8s1wEtlzlI3vct+dA+KZ+NhS79AEw5lx/Ki0xSw==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -3435,6 +3444,13 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-emoji@2.23.1': + resolution: {integrity: sha512-bqTn+hbq0bDIcrPIIjVq3GndJ/PYQfReMDlyTv0mUCtRbP7zReJ1oFx02d25RmwgS6XL3U8WW4kEFomhliwWSQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/suggestion': ^2.7.0 + '@tiptap/extension-file-handler@2.23.0': resolution: {integrity: sha512-rTimkgFtMIbYYydf2suvIpF+GnFRU80BppnrOUNfW+HzaI0i1p0gKzEDKJuPBMAEFfG/Q7Yxetk6rO6Y5Sq6Mw==} peerDependencies: @@ -3693,6 +3709,12 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + + '@types/node@18.19.115': + resolution: {integrity: sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==} + '@types/node@22.13.8': resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==} @@ -3841,6 +3863,10 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + agents@0.0.93: resolution: {integrity: sha512-W25kx492Txn5XYY9gx2YBhGmfC8C/N3JQzfjbmq9GjhYtAFCsJdIw6C5xbLt/ev2x3Uor/8XMHXYiw/2YbTSkQ==} peerDependencies: @@ -4628,6 +4654,15 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojibase-data@15.3.2: + resolution: {integrity: sha512-TpDyTDDTdqWIJixV5sTA6OQ0P0JfIIeK2tFRR3q56G9LK65ylAZ7z3KyBXokpvTTJ+mLUXQXbLNyVkjvnTLE+A==} + peerDependencies: + emojibase: '*' + + emojibase@16.0.0: + resolution: {integrity: sha512-Nw2m7JLIO4Ou2X/yZPRNscHQXVbbr6SErjkJ7EooG7MbR3yDZszCv9KTizsXFc7yZl0n3WF+qUKIC/Lw6H9xaQ==} + engines: {node: '>=18.12.0'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -4956,6 +4991,9 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data-encoder@4.1.0: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} @@ -4964,6 +5002,10 @@ packages: resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + formdata-node@6.0.3: resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} engines: {node: '>= 18'} @@ -5224,6 +5266,9 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -5339,6 +5384,9 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-emoji-supported@0.0.5: + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -5942,6 +5990,11 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -7309,6 +7362,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -7529,6 +7585,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} @@ -7742,6 +7802,19 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@arcadeai/arcadejs@1.8.1': + dependencies: + '@types/node': 18.19.115 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + zod: 3.25.67 + transitivePeerDependencies: + - encoding + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -10372,6 +10445,17 @@ snapshots: '@tiptap/core': 2.23.0(@tiptap/pm@2.23.0) '@tiptap/pm': 2.23.0 + '@tiptap/extension-emoji@2.23.1(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0)(@tiptap/suggestion@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0))(emojibase@16.0.0)': + dependencies: + '@tiptap/core': 2.23.0(@tiptap/pm@2.23.0) + '@tiptap/pm': 2.23.0 + '@tiptap/suggestion': 2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0) + emoji-regex: 10.4.0 + emojibase-data: 15.3.2(emojibase@16.0.0) + is-emoji-supported: 0.0.5 + transitivePeerDependencies: + - emojibase + '@tiptap/extension-file-handler@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/extension-text-style@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0)))': dependencies: '@tiptap/core': 2.23.0(@tiptap/pm@2.23.0) @@ -10640,6 +10724,15 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.15.29 + form-data: 4.0.3 + + '@types/node@18.19.115': + dependencies: + undici-types: 5.26.5 + '@types/node@22.13.8': dependencies: undici-types: 6.20.0 @@ -10804,6 +10897,10 @@ snapshots: agent-base@7.1.3: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + agents@0.0.93(@cloudflare/workers-types@4.20250628.0)(react@19.1.0): dependencies: '@modelcontextprotocol/sdk': 1.12.0 @@ -11581,6 +11678,12 @@ snapshots: emoji-regex@9.2.2: {} + emojibase-data@15.3.2(emojibase@16.0.0): + dependencies: + emojibase: 16.0.0 + + emojibase@16.0.0: {} + encodeurl@2.0.0: {} encoding-sniffer@0.2.1: @@ -12101,6 +12204,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data-encoder@1.7.2: {} + form-data-encoder@4.1.0: {} form-data@4.0.3: @@ -12111,6 +12216,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + formdata-node@6.0.3: {} forwarded@0.2.0: {} @@ -12413,6 +12523,10 @@ snapshots: human-signals@2.1.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + husky@9.1.7: {} hyphenate-style-name@1.1.0: {} @@ -12518,6 +12632,8 @@ snapshots: is-decimal@2.0.1: {} + is-emoji-supported@0.0.5: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -13213,6 +13329,8 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -14860,6 +14978,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@5.26.5: {} + undici-types@6.20.0: {} undici-types@6.21.0: {} @@ -15075,6 +15195,8 @@ snapshots: w3c-keyname@2.2.8: {} + web-streams-polyfill@4.0.0-beta.3: {} + web-vitals@4.2.4: {} webidl-conversions@3.0.1: {}