Merge branch 'staging' into dakdevs/staging/grant-error-handling

This commit is contained in:
Dak Washbrook
2025-05-08 23:09:36 -07:00
committed by GitHub
5 changed files with 93 additions and 147 deletions

View File

@@ -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: {

View File

@@ -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 && (

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);