Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c1543ce
add emoji shortcodes to tiptap (#1595)
ahmetskilinc Jul 4, 2025
9ff981c
perf: add indexes to improve query speed (#1613)
devsargam Jul 4, 2025
528a986
Fix: restrict access to settings page for unauthenticated users (#160…
harshjdhv Jul 4, 2025
16125ff
text responsiveness corrected (#1594)
Fahad-Dezloper Jul 4, 2025
4da17dd
frontend: Settings: Adjust misalignments of placeholders (#1531)
SinghaAnirban005 Jul 4, 2025
e5e7e4d
i18n:sidebar:Add missing sidebar translation key (#1555)
SinghaAnirban005 Jul 4, 2025
49ae389
i18n: Override hindi translation(originally in english) for Feedback …
SinghaAnirban005 Jul 4, 2025
df1d2d1
fix: ui collapse for longer email and correctly showing "Display Name…
romitg2 Jul 4, 2025
9c7c8cd
feat: cleanup on settings-general page (#1572)
ahmetskilinc Jul 4, 2025
7702900
feat: Converting empty state svg to component (#1562)
samrathreddy Jul 4, 2025
f29e338
feat: add rich text formatting toolbar (#1563)
abhix4 Jul 4, 2025
859d05f
Fix: Example Queries Masking done Properly (#1549)
omraval18 Jul 4, 2025
1ac8dbc
Fix/draft types (#1545)
Fahad-Dezloper Jul 4, 2025
96f322c
Fix: Attachment Dropdown Trigger Fix (#1543)
Pheewww Jul 4, 2025
373bac8
on clicking download all attachement its makes the email ui collaspe …
Pankajkumar2608 Jul 4, 2025
58d19da
Fix/noname placeholder (#1527)
Fahad-Dezloper Jul 4, 2025
4b6d1ba
feat: Moved openAI model configuration to .env (#1540)
samrathreddy Jul 4, 2025
c8117ba
fix thread popover opacity (#1537)
abhix4 Jul 4, 2025
35e15bc
add attachments support to drafts (#1536)
abhix4 Jul 4, 2025
3263d88
Fix/email printing (#1529)
omraval18 Jul 4, 2025
1f20712
Hotfix (#1627)
MrgSub Jul 4, 2025
033b8cd
feat: update translations via @LingoDotDev (#1625)
github-actions[bot] Jul 4, 2025
c1ff7be
No prefetch (#1628)
MrgSub Jul 4, 2025
0b19bda
No batching (#1629)
MrgSub Jul 4, 2025
f60b0fc
mail-list performance (#1631)
MrgSub Jul 4, 2025
8845ee7
no clear cache on logout - revisit later (#1632)
MrgSub Jul 4, 2025
a64532a
minor fixes (#1634)
MrgSub Jul 5, 2025
cf88826
ai work (#1635)
MrgSub Jul 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=""

Expand Down
19 changes: 10 additions & 9 deletions apps/mail/app/(routes)/settings/general/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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"
>
<Clock className="mr-2 h-4 w-4 flex-shrink-0" />
<span className="truncate">{field.value}</span>
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<PopoverContent className="w-[300px] p-0" align="start">
<div className="px-3 py-2">
<input
className="border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50"
Expand Down Expand Up @@ -195,18 +196,18 @@ export default function GeneralPage() {
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>{m['pages.settings.general.language']()}</FormLabel>
<FormLabel className='flex'>{m['pages.settings.general.language']()}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-36 justify-start">
<SelectTrigger className="w-36 justify-start hover:bg-transparent">
<Globe className="mr-2 h-4 w-4" />
<SelectValue placeholder={m['pages.settings.general.selectLanguage']()} />
</SelectTrigger>
</FormControl>
<SelectContent>
{locales.map((locale) => (
<SelectItem key={locale} value={locale}>
{locale}
{localesData[locale as keyof typeof localesData]}
</SelectItem>
))}
</SelectContent>
Expand All @@ -219,7 +220,7 @@ export default function GeneralPage() {
name="timezone"
render={({ field }) => (
<FormItem>
<FormLabel>{m['pages.settings.general.timezone']()}</FormLabel>
<FormLabel className='flex'>{m['pages.settings.general.timezone']()}</FormLabel>
<TimezoneSelect field={field} />
</FormItem>
)}
Expand All @@ -230,11 +231,11 @@ export default function GeneralPage() {
name="defaultEmailAlias"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1">
<FormLabel className="!mb-1">
{m['pages.settings.general.defaultEmailAlias']()}{' '}
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="h-4 w-4" />
<InfoIcon className="h-[1em] w-[1em]" />
</TooltipTrigger>
<TooltipContent>
{m['pages.settings.general.defaultEmailDescription']()}
Expand All @@ -243,7 +244,7 @@ export default function GeneralPage() {
</FormLabel>
<Select onValueChange={field.onChange} value={field.value || ''}>
<FormControl>
<SelectTrigger className="w-[300px] justify-start">
<SelectTrigger className="w-[300px] justify-start hover:bg-transparent">
<Mail className="mr-2 h-4 w-4" />
<SelectValue
placeholder={m['pages.settings.general.selectDefaultEmail']()}
Expand Down
15 changes: 14 additions & 1 deletion apps/mail/app/(routes)/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { SettingsLayoutContent } from '@/components/ui/settings-content';
import { Outlet } from 'react-router';
import { authProxy } from '@/lib/auth-proxy';
import type { Route } from './+types/layout';

export async function clientLoader({ request }: Route.ClientLoaderArgs) {
const session = await authProxy.api.getSession({ headers: request.headers });

if (!session) {
return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`);
}


return null;
}

export default function SettingsLayout() {
return (
<SettingsLayoutContent>
<Outlet />
</SettingsLayoutContent>
);
}
}
32 changes: 32 additions & 0 deletions apps/mail/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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%;
}
}

Expand Down
2 changes: 1 addition & 1 deletion apps/mail/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const getServerTrpc = (req: Request) =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
maxItems: 8,
maxItems: 1,
url: getUrl(),
transformer: superjson,
headers: req.headers,
Expand Down
97 changes: 55 additions & 42 deletions apps/mail/components/create/ai-chat.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<div className="mt-6 flex w-full flex-col items-center gap-2">
<div className="horizontal-fade-mask mt-6 flex w-full flex-col items-center gap-2">
{/* First row */}
<div className="no-scrollbar relative flex w-full justify-center overflow-x-auto">
<div className="flex gap-4 px-4">
Expand All @@ -98,10 +99,6 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi
</button>
))}
</div>
{/* Left mask */}
<div className="from-panelLight dark:from-panelDark pointer-events-none absolute bottom-0 left-0 top-0 w-12 bg-gradient-to-r to-transparent"></div>
{/* Right mask */}
<div className="from-panelLight dark:from-panelDark pointer-events-none absolute bottom-0 right-0 top-0 w-12 bg-gradient-to-l to-transparent"></div>
</div>

{/* Second row */}
Expand All @@ -117,10 +114,6 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi
</button>
))}
</div>
{/* Left mask */}
<div className="from-panelLight dark:from-panelDark pointer-events-none absolute bottom-0 left-0 top-0 w-12 bg-gradient-to-r to-transparent"></div>
{/* Right mask */}
<div className="from-panelLight dark:from-panelDark pointer-events-none absolute bottom-0 right-0 top-0 w-12 bg-gradient-to-l to-transparent"></div>
</div>
</div>
);
Expand Down Expand Up @@ -193,15 +186,6 @@ const ToolResponse = ({ toolName, result, args }: { toolName: string; result: an
</div>
) : null;

case Tools.WebSearch:
return (
<div className="rounded-lg border border-purple-200/40 p-2 dark:border-purple-800/20">
<div className="prose dark:prose-invert max-w-none text-sm">
<p className="text-sm">{result}</p>
</div>
</div>
);

case Tools.ComposeEmail:
return result?.newBody ? (
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
Expand All @@ -212,13 +196,6 @@ const ToolResponse = ({ toolName, result, args }: { toolName: string; result: an
) : null;

default:
if (result?.success) {
return (
<div className="text-sm text-green-600 dark:text-green-400">
Operation completed successfully
</div>
);
}
return null;
}
};
Expand Down Expand Up @@ -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<HTMLFormElement>);
editor.commands.clearContent(true);
onSubmit(event as unknown as React.FormEvent<HTMLFormElement>);
}
},
});
Expand All @@ -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();
Expand All @@ -306,7 +285,7 @@ export function AIChat({
return (
<div className={cn('flex h-full flex-col', isFullScreen ? 'mx-auto max-w-xl' : '')}>
<div className="no-scrollbar flex-1 overflow-y-auto" ref={messagesContainerRef}>
<div className="min-h-full space-y-4 px-2 py-4">
<div className="min-h-full px-2 py-4">
{chatMessages && !chatMessages.enabled ? (
<div
onClick={() => setPricingDialog('true')}
Expand Down Expand Up @@ -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 (
<div key={`${message.id}-${index}`} className="flex flex-col gap-2">
<div key={`${message.id}-${index}`} className="flex flex-col">
{toolParts.map((part, idx) =>
part.toolInvocation && part.toolInvocation.result ? (
part.toolInvocation &&
part.toolInvocation.result &&
!streamingTools.includes(part.toolInvocation.toolName as Tools) ? (
<ToolResponse
key={idx}
toolName={part.toolInvocation.toolName}
Expand All @@ -358,7 +341,7 @@ export function AIChat({
/>
) : null,
)}
{!doesIncludeToolResult && textParts.length > 0 && (
{!doesIncludeStreamingTool && textParts.length > 0 && (
<p
className={cn(
'flex w-fit flex-col gap-2 rounded-lg text-sm',
Expand All @@ -368,15 +351,44 @@ export function AIChat({
)}
>
{textParts.map(
(part) => part.text && <span key={part.text}>{part.text || ' '}</span>,
(part) =>
part.text && (
<Markdown
markdownCustomStyles={{
h1: {
fontSize: '1rem',
},
h2: {
fontSize: '1rem',
},
h3: {
fontSize: '1rem',
},
h4: {
fontSize: '1rem',
},
h5: {
fontSize: '1rem',
},
h6: {
fontSize: '1rem',
},
p: {
fontSize: '1rem',
},
}}
key={part.text}
>
{part.text || ' '}
</Markdown>
),
)}
</p>
)}
</div>
);
})
)}
<div ref={messagesEndRef} />

{(status === 'submitted' || status === 'streaming') && (
<div className="flex flex-col gap-2 rounded-lg">
Expand All @@ -390,6 +402,7 @@ export function AIChat({
{(status === 'error' || !!error) && (
<div className="text-sm text-red-500">Error, please try again later</div>
)}
<div className="h-0 w-0" ref={messagesEndRef} />
</div>
</div>

Expand Down
Loading