diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ebc0a5587f..0c84913252 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -195,7 +195,7 @@ When implementing new features, follow these guidelines: 1. **Add English Source Strings** - - Place all user-facing text in `apps/mail/locales/en.json` + - Place all user-facing text in `apps/mail/messages/en.json` - Organize strings according to the existing structure - Use descriptive, hierarchical keys that identify the feature and context - Example: `"pages.settings.connections.disconnectSuccess": "Account disconnected successfully"` diff --git a/.github/TRANSLATION.md b/.github/TRANSLATION.md index a436e371cf..63ecabad5d 100644 --- a/.github/TRANSLATION.md +++ b/.github/TRANSLATION.md @@ -31,7 +31,7 @@ Here's an example of our i18n.json configuration: }, "buckets": { "json": { - "include": ["apps/mail/locales/[locale].json"] + "include": ["apps/mail/messages/[locale].json"] } } } diff --git a/apps/mail/app/(routes)/layout.tsx b/apps/mail/app/(routes)/layout.tsx index c570e2e198..2c1ed4d059 100644 --- a/apps/mail/app/(routes)/layout.tsx +++ b/apps/mail/app/(routes)/layout.tsx @@ -3,6 +3,7 @@ import { CommandPaletteProvider } from '@/components/context/command-palette-con import { Outlet } from 'react-router'; + export default function Layout() { return ( diff --git a/apps/mail/app/(routes)/settings/connections/page.tsx b/apps/mail/app/(routes)/settings/connections/page.tsx index 189f749f27..d8e192716d 100644 --- a/apps/mail/app/(routes)/settings/connections/page.tsx +++ b/apps/mail/app/(routes)/settings/connections/page.tsx @@ -164,7 +164,8 @@ export default function ConnectionsPage() { diff --git a/apps/mail/app/(routes)/settings/general/page.tsx b/apps/mail/app/(routes)/settings/general/page.tsx index b9bf5a2962..ff39f653f1 100644 --- a/apps/mail/app/(routes)/settings/general/page.tsx +++ b/apps/mail/app/(routes)/settings/general/page.tsx @@ -61,7 +61,7 @@ const TimezoneSelect = memo( variant="outline" role="combobox" aria-expanded={open} - className="md:w-46 flex !h-9 w-full items-center justify-start rounded-md hover:bg-transparent" + className="flex !h-9 w-full items-center justify-start rounded-md hover:bg-transparent" > {field.value} @@ -196,11 +196,11 @@ export default function GeneralPage() { control={form.control} name="language" render={({ field }) => ( - - {m['pages.settings.general.language']()} + + {m['pages.settings.general.language']()} - + import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/trpc'; @@ -82,6 +84,9 @@ export function Layout({ children }: PropsWithChildren) { {children} + diff --git a/apps/mail/components/create/ai-chat.tsx b/apps/mail/components/create/ai-chat.tsx index e9c4051040..e0c7a4e352 100644 --- a/apps/mail/components/create/ai-chat.tsx +++ b/apps/mail/components/create/ai-chat.tsx @@ -1,4 +1,3 @@ -import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useAIFullScreen, useAISidebar } from '../ui/ai-sidebar'; import { VoiceProvider } from '@/providers/voice-provider'; @@ -14,25 +13,26 @@ import { VoiceButton } from '../voice-button'; import { EditorContent } from '@tiptap/react'; import { CurvedArrow } from '../icons/icons'; import { Tools } from '../../types/tools'; -import { InfoIcon } from 'lucide-react'; import { Button } from '../ui/button'; import { format } from 'date-fns-tz'; import { useQueryState } from 'nuqs'; -const renderThread = (thread: { id: string; title: string; snippet: string }) => { +const ThreadPreview = ({ threadId }: { threadId: string }) => { const [, setThreadId] = useQueryState('threadId'); - const { data: getThread } = useThread(thread.id); + const { data: getThread } = useThread(threadId); const [, setIsFullScreen] = useQueryState('isFullScreen'); const handleClick = () => { - setThreadId(thread.id); + setThreadId(threadId); setIsFullScreen(null); }; - return getThread?.latest ? ( + if (!getThread?.latest) return null; + + return (
@@ -65,18 +65,11 @@ const renderThread = (thread: { id: string; title: string; snippet: string }) =>
- ) : null; -}; - -const RenderThreads = ({ - threads, -}: { - threads: { id: string; title: string; snippet: string }[]; -}) => { - return
{threads.map(renderThread)}
; + ); }; const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => void }) => { + const firstRowQueries = [ 'Find invoice from Stripe', 'Show unpaid invoices', @@ -86,7 +79,7 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi const secondRowQueries = ['Find all work meetings', 'What projects do i have coming up']; return ( -
+
{/* First row */}
@@ -94,14 +87,12 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi ))}
- {/* Second row */}
@@ -116,6 +107,10 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi ))}
+ {/* Left mask */} +
+ {/* Right mask */} +
); }; @@ -148,82 +143,55 @@ export interface AIChatProps { onModelChange?: (model: string) => void; } -declare global { - var DEBUG: boolean; -} - -const ToolResponse = ({ toolName, result, args }: { toolName: string; result: any; args: any }) => { - const renderContent = () => { - switch (toolName) { - case Tools.ListThreads: - case Tools.AskZeroMailbox: - return result?.threads ? : null; - - case Tools.GetThread: - return result?.thread ? ( -
-
- - - {result.thread.sender?.name?.[0]?.toUpperCase()} - -
-

{result.thread.sender?.name}

-

{result.thread.subject}

-
-
-
- {result.thread.body} -
-
- ) : null; - - case Tools.GetUserLabels: - return result?.labels ? ( -
- {result.labels.map((label: any) => ( - - ))} -
- ) : null; - - case Tools.ComposeEmail: - return result?.newBody ? ( -
-
- {result.newBody} -
-
- ) : null; - - default: - return null; - } - }; +// Subcomponents for ToolResponse +const GetThreadToolResponse = ({ result, args }: { result: any; args: any }) => { + // Extract threadId from result or args + let threadId: string | null = null; + if (typeof result === 'string') { + const match = result.match(//); + if (match?.[1]) threadId = match[1]; + } + if (!threadId && args?.id && typeof args.id === 'string') threadId = args.id; + if (!threadId) return null; + return ; +}; - const content = renderContent(); - if (!content) return null; +const GetUserLabelsToolResponse = ({ result }: { result: any }) => { + if (!result?.labels) return null; + return ( +
+ {result.labels.map((label: any) => ( + + ))} +
+ ); +}; +const ComposeEmailToolResponse = ({ result }: { result: any }) => { + if (!result?.newBody) return null; return ( -
- {globalThis.DEBUG ? ( - - - - - -
-

Tool Arguments:

-
{JSON.stringify(args, null, 2)}
-
-
-
- ) : null} - {content} +
+
+ {result.newBody} +
); }; +// Main ToolResponse switcher +const ToolResponse = ({ toolName, result, args }: { toolName: string; result: any; args: any }) => { + switch (toolName) { + case Tools.GetThread: + return ; + case Tools.GetUserLabels: + return ; + case Tools.ComposeEmail: + return ; + default: + return null; + } +}; + export function AIChat({ messages, setInput, @@ -233,7 +201,6 @@ export function AIChat({ }: AIChatProps): React.ReactElement { const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); - const inputRef = useRef(null); const { chatMessages } = useBilling(); const { isFullScreen } = useAIFullScreen(); const [, setPricingDialog] = useQueryState('pricingDialog'); @@ -275,6 +242,12 @@ export function AIChat({ }, 100); }; + const handleQueryClick = (query: string) => { + editor.commands.setContent(query); + setInput(query); + editor.commands.focus(); + }; + useEffect(() => { if (aiSidebarOpen === 'true') { editor.commands.focus(); @@ -287,13 +260,13 @@ export function AIChat({
{chatMessages && !chatMessages.enabled ? (
setPricingDialog('true')} - className="absolute inset-0 flex flex-col items-center justify-center" - > - - Upgrade to Zero Pro for unlimited AI chat - - + onClick={() => setPricingDialog('true')} + className="absolute inset-0 flex flex-col items-center justify-center" + > + + Upgrade to Zero Pro for unlimited AI chat + +
) : !messages.length ? (
@@ -309,39 +282,28 @@ export function AIChat({

{/* Example Thread */} - { - setInput(query); - inputRef.current?.focus(); - }} - /> +
) : ( messages.map((message, index) => { const textParts = message.parts.filter((part) => part.type === 'text'); const toolParts = message.parts.filter((part) => part.type === 'tool-invocation'); - const streamingTools = new Set([Tools.WebSearch]); - const doesIncludeStreamingTool = toolParts.some( - (part) => - streamingTools.has(part.toolInvocation?.toolName as Tools) && - part.toolInvocation?.result, - ); + return ( -
- {toolParts.map((part) => - part.toolInvocation && - part.toolInvocation.result && - !streamingTools.has(part.toolInvocation.toolName as Tools) ? ( - - ) : null, +
+ {toolParts.map( + (part, index) => + part.toolInvocation?.result && ( + + ), )} - {!doesIncludeStreamingTool && textParts.length > 0 && ( -

0 && ( +

@@ -382,7 +343,7 @@ export function AIChat({ ), )} -

+
)}
); @@ -390,12 +351,10 @@ export function AIChat({ )} {(status === 'submitted' || status === 'streaming') && ( -
-
- - zero is thinking... - -
+
+ + zero is thinking... +
)} {(status === 'error' || !!error) && ( @@ -443,7 +402,7 @@ export function AIChat({
- {/*
+ {/*