mirror of
https://github.com/Mail-0/Zero.git
synced 2026-07-01 08:16:28 +00:00
Merge branch 'staging' into dakdevs/staging/grant-error-handling
This commit is contained in:
@@ -139,7 +139,7 @@ export function AIChat() {
|
||||
const [searchValue] = useSearchValue();
|
||||
|
||||
const { messages, input, setInput, error, handleSubmit, status, stop } = useChat({
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat`,
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/chat`,
|
||||
fetch: (url, options) => fetch(url, { ...options, method: 'POST', credentials: 'include' }),
|
||||
maxSteps: 5,
|
||||
body: {
|
||||
|
||||
@@ -869,7 +869,7 @@ export function EmailComposer({
|
||||
<Sparkles className="h-3.5 w-3.5 fill-black dark:fill-white" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center text-sm leading-none text-black dark:text-white">
|
||||
<div className="text-center text-sm leading-none text-black dark:text-white hidden md:block">
|
||||
Generate
|
||||
</div>
|
||||
</div>
|
||||
@@ -879,7 +879,7 @@ export function EmailComposer({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
disabled
|
||||
className="flex h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50"
|
||||
className="h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 hidden md:flex"
|
||||
>
|
||||
<Smile className="h-3 w-3 fill-[#9A9A9A]" />
|
||||
<span className="px-0.5 text-sm">Casual</span>
|
||||
@@ -893,7 +893,7 @@ export function EmailComposer({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
disabled
|
||||
className="flex h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50"
|
||||
className="flex h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 hidden md:flex"
|
||||
>
|
||||
{messageLength < 50 && <ShortStack className="h-3 w-3 fill-[#9A9A9A]" />}
|
||||
{messageLength >= 50 && messageLength < 200 && (
|
||||
|
||||
@@ -174,14 +174,9 @@ const Thread = memo(
|
||||
const { folder } = useParams<{ folder: string }>();
|
||||
const [{ refetch: refetchThreads }] = useThreads();
|
||||
const [threadId] = useQueryState('threadId');
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const isHovering = useRef<boolean>(false);
|
||||
const hasPrefetched = useRef<boolean>(false);
|
||||
const isMobile = useIsMobile();
|
||||
const [, setBackgroundQueue] = useAtom(backgroundQueueAtom);
|
||||
const { refetch: refetchStats } = useStats();
|
||||
const { data: getThreadData, isLoading, isGroupThread } = useThread(demo ? null : message.id);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isStarred, setIsStarred] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const trpc = useTRPC();
|
||||
@@ -289,55 +284,6 @@ const Thread = memo(
|
||||
return latestMessage.sender.name.trim().replace(/^['"]|['"]$/g, '');
|
||||
}, [latestMessage?.sender?.name]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (demo || !latestMessage) return;
|
||||
isHovering.current = true;
|
||||
setIsHovered(true);
|
||||
|
||||
// Prefetch only in single select mode
|
||||
if (selectMode === 'single' && sessionData?.userId && !hasPrefetched.current) {
|
||||
// Clear any existing timeout
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Set new timeout for prefetch
|
||||
hoverTimeoutRef.current = setTimeout(() => {
|
||||
if (isHovering.current) {
|
||||
const messageId = latestMessage.threadId ?? message.id;
|
||||
console.log(
|
||||
`🕒 Hover threshold reached for email ${messageId}, initiating prefetch...`,
|
||||
);
|
||||
void queryClient.prefetchQuery(trpc.mail.get.queryOptions({ id: messageId }));
|
||||
hasPrefetched.current = true;
|
||||
}
|
||||
}, HOVER_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.current = false;
|
||||
setIsHovered(false);
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('emailHover', { detail: { id: null } }));
|
||||
};
|
||||
|
||||
// Reset prefetch flag when message changes
|
||||
useEffect(() => {
|
||||
hasPrefetched.current = false;
|
||||
}, [message.id]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!demo && (isLoading || !latestMessage || !getThreadData)) return null;
|
||||
|
||||
const demoContent =
|
||||
@@ -345,8 +291,6 @@ const Thread = memo(
|
||||
<div className="p-1 px-3" onClick={onClick ? onClick(latestMessage) : undefined}>
|
||||
<div
|
||||
data-thread-id={latestMessage.threadId ?? message.id}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
key={latestMessage.threadId ?? message.id}
|
||||
className={cn(
|
||||
'hover:bg-offsetLight hover:bg-primary/5 group relative flex cursor-pointer flex-col items-start overflow-clip rounded-lg border border-transparent px-4 py-3 text-left text-sm transition-all hover:opacity-100',
|
||||
@@ -449,11 +393,9 @@ const Thread = memo(
|
||||
|
||||
const content =
|
||||
latestMessage && getThreadData ? (
|
||||
<div className={'select-none '} onClick={onClick ? onClick(latestMessage) : undefined}>
|
||||
<div className={'select-none'} onClick={onClick ? onClick(latestMessage) : undefined}>
|
||||
<div
|
||||
data-thread-id={latestMessage.threadId ?? latestMessage.id}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
key={latestMessage.threadId ?? latestMessage.id}
|
||||
className={cn(
|
||||
'hover:bg-offsetLight hover:bg-primary/5 group relative mx-1 flex cursor-pointer flex-col items-start rounded-lg border-transparent py-2 text-left text-sm transition-all hover:opacity-100',
|
||||
@@ -461,72 +403,70 @@ const Thread = memo(
|
||||
'border-border bg-primary/5 opacity-100',
|
||||
isKeyboardFocused && 'ring-primary/50',
|
||||
'relative',
|
||||
'group',
|
||||
)}
|
||||
>
|
||||
{/* Quick Action Row */}
|
||||
{isHovered && !isMobile && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-2 z-[25] flex -translate-y-1/2 items-center gap-1 rounded-xl border bg-white p-1 shadow-sm dark:bg-[#1A1A1A]',
|
||||
index === 0 ? 'top-4' : 'top-[-1]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-2 z-[25] flex -translate-y-1/2 items-center gap-1 rounded-xl border bg-white p-1 opacity-0 shadow-sm group-hover:opacity-100 dark:bg-[#1A1A1A]',
|
||||
index === 0 ? 'top-4' : 'top-[-1]',
|
||||
)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 overflow-visible [&_svg]:size-3.5"
|
||||
onClick={handleToggleStar}
|
||||
>
|
||||
<Star2
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
isStarred
|
||||
? 'fill-yellow-400 stroke-yellow-400'
|
||||
: 'fill-transparent stroke-[#9D9D9D] dark:stroke-[#9D9D9D]',
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="mb-1 bg-white dark:bg-[#1A1A1A]">
|
||||
{isStarred ? t('common.threadDisplay.unstar') : t('common.threadDisplay.star')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 [&_svg]:size-3.5"
|
||||
onClick={() => moveThreadTo('archive')}
|
||||
>
|
||||
<Archive2 className="fill-[#9D9D9D]" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="mb-1 bg-white dark:bg-[#1A1A1A]">
|
||||
{t('common.threadDisplay.archive')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{!isFolderBin ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 overflow-visible [&_svg]:size-3.5"
|
||||
onClick={handleToggleStar}
|
||||
className="h-6 w-6 hover:bg-[#FDE4E9] dark:hover:bg-[#411D23] [&_svg]:size-3.5"
|
||||
onClick={() => moveThreadTo('bin')}
|
||||
>
|
||||
<Star2
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
isStarred
|
||||
? 'fill-yellow-400 stroke-yellow-400'
|
||||
: 'fill-transparent stroke-[#9D9D9D] dark:stroke-[#9D9D9D]',
|
||||
)}
|
||||
/>
|
||||
<Trash className="fill-[#F43F5E]" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="mb-1 bg-white dark:bg-[#1A1A1A]">
|
||||
{isStarred ? t('common.threadDisplay.unstar') : t('common.threadDisplay.star')}
|
||||
{t('common.actions.Bin')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 [&_svg]:size-3.5"
|
||||
onClick={() => moveThreadTo('archive')}
|
||||
>
|
||||
<Archive2 className="fill-[#9D9D9D]" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="mb-1 bg-white dark:bg-[#1A1A1A]">
|
||||
{t('common.threadDisplay.archive')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{!isFolderBin ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-[#FDE4E9] dark:hover:bg-[#411D23] [&_svg]:size-3.5"
|
||||
onClick={() => moveThreadTo('bin')}
|
||||
>
|
||||
<Trash className="fill-[#F43F5E]" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="mb-1 bg-white dark:bg-[#1A1A1A]">
|
||||
{t('common.actions.Bin')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-4 px-4">
|
||||
<div>
|
||||
|
||||
@@ -1,56 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useState, useEffect } from 'react';
|
||||
import confetti from 'canvas-confetti';
|
||||
import Image from 'next/image';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Welcome to Zero Email!',
|
||||
description: 'Your new intelligent email experience starts here.',
|
||||
video: '/onboarding/get-started.png'
|
||||
video: '/onboarding/get-started.png',
|
||||
},
|
||||
{
|
||||
title: 'Chat with your inbox',
|
||||
description: 'Zero allows you to chat with your inbox and do tasks on your behalf.',
|
||||
video: '/onboarding/step2.gif'
|
||||
video: '/onboarding/step2.gif',
|
||||
},
|
||||
{
|
||||
title: 'AI Compose & Reply',
|
||||
description: 'Our AI assistant allows you to write emails with a single click.',
|
||||
video: '/onboarding/step1.gif'
|
||||
video: '/onboarding/step1.gif',
|
||||
},
|
||||
{
|
||||
title: 'Label your emails',
|
||||
description: 'Zero helps you label your emails and helps you focus on what matters.',
|
||||
video: '/onboarding/step3.gif'
|
||||
video: '/onboarding/step3.gif',
|
||||
},
|
||||
{
|
||||
title: 'Coming Soon',
|
||||
description: (
|
||||
<>
|
||||
<span className="mb-6 text-muted-foreground">We're excited to bring these powerful features to all users very soon!
|
||||
|
||||
<span className="text-muted-foreground mb-6">
|
||||
We're excited to bring these powerful features to all users very soon!
|
||||
</span>
|
||||
<div className="space-y-3 text-center">
|
||||
<div className="text-lg font-medium">Voice AI</div>
|
||||
<div className="text-lg font-medium">Actions</div>
|
||||
<div className="text-lg font-medium">Calendar Integration</div>
|
||||
<div className="text-lg font-medium text-muted-foreground">And much more!</div>
|
||||
<div className="text-muted-foreground text-lg font-medium">And much more!</div>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
video: null
|
||||
video: null,
|
||||
},
|
||||
{
|
||||
title: 'Ready to start?',
|
||||
description: 'Click below to begin your intelligent email experience!',
|
||||
video: '/onboarding/ready.png'
|
||||
video: '/onboarding/ready.png',
|
||||
},
|
||||
];
|
||||
|
||||
export function OnboardingDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
export function OnboardingDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -58,7 +65,7 @@ export function OnboardingDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
confetti({
|
||||
particleCount: 100,
|
||||
spread: 70,
|
||||
origin: { y: 0.6 }
|
||||
origin: { y: 0.6 },
|
||||
});
|
||||
}
|
||||
}, [currentStep]);
|
||||
@@ -85,8 +92,11 @@ export function OnboardingDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTitle></DialogTitle>
|
||||
<DialogContent showOverlay className="sm:max-w-[690px] mx-auto bg-panelLight dark:bg-[#111111] w-full rounded-xl p-4 border">
|
||||
<div className="flex flex-col p-6 gap-6">
|
||||
<DialogContent
|
||||
showOverlay
|
||||
className="bg-panelLight mx-auto w-full rounded-xl border p-4 sm:max-w-[690px] dark:bg-[#111111]"
|
||||
>
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex gap-1">
|
||||
{steps.map((_, index) => (
|
||||
@@ -100,28 +110,27 @@ export function OnboardingDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 text-center">
|
||||
<h2 className="text-4xl font-semibold">
|
||||
{steps[currentStep]?.title}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
||||
<h2 className="text-4xl font-semibold">{steps[currentStep]?.title}</h2>
|
||||
<p className="text-muted-foreground mx-auto max-w-md text-sm">
|
||||
{steps[currentStep]?.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Video/GIF Section */}
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="flex items-center justify-center">
|
||||
{steps[currentStep]?.video && (
|
||||
<div className="w-full max-w-4xl aspect-video rounded-lg overflow-hidden bg-muted">
|
||||
<img
|
||||
src={steps[currentStep].video}
|
||||
<div className="bg-muted aspect-video w-full max-w-4xl overflow-hidden rounded-lg">
|
||||
<Image
|
||||
priority
|
||||
src={steps[currentStep].video}
|
||||
alt={steps[currentStep].title}
|
||||
className="w-full h-full object-cover"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex mx-auto max-w-xl w-full gap-2">
|
||||
<div className="mx-auto flex w-full max-w-xl gap-2">
|
||||
<Button
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
variant="outline"
|
||||
@@ -130,10 +139,7 @@ export function OnboardingDialog({ open, onOpenChange }: { open: boolean; onOpen
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
className="h-8 w-full"
|
||||
>
|
||||
<Button onClick={handleNext} className="h-8 w-full">
|
||||
{currentStep === steps.length - 1 ? 'Get Started' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,8 @@ const api = new Hono<{ Variables: HonoVariables; Bindings: Env }>()
|
||||
c.set('session', session);
|
||||
await next();
|
||||
})
|
||||
.post('/chat', async (c) => chatHandler(c))
|
||||
.get('/mailto-handler', async (c) => mailtoHandler(c))
|
||||
.on(['GET', 'POST'], '/auth/*', (c) => c.var.auth.handler(c.req.raw))
|
||||
.use(
|
||||
trpcServer({
|
||||
@@ -39,8 +41,6 @@ const api = new Hono<{ Variables: HonoVariables; Bindings: Env }>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.post('/chat', async (c) => chatHandler(c))
|
||||
.get('/mailto-handler', async (c) => mailtoHandler(c))
|
||||
.onError(async (err, c) => {
|
||||
if (err instanceof Response) return err;
|
||||
console.error('Error in Hono handler:', err);
|
||||
|
||||
Reference in New Issue
Block a user