Skip to content

Commit c20868c

Browse files
committed
Replace hardcoded dark mode colors with theme variables (#1129)
This PR standardizes dark mode colors across the application by replacing hardcoded color values with semantic color variables. It also enhances the AI Chat component with improved tool response rendering and adds a new voice chat capability. Key changes: - Replaced hardcoded dark background colors (`#1A1A1A`) with the semantic `bg-panelDark` class - Replaced hardcoded text colors with semantic classes like `text-muted-foreground` - Enhanced the AI Chat component with better tool response visualization - Added tooltips to display tool arguments in AI responses - Improved email composer editor focus handling - Added a new Tools enum to better type and organize AI tool capabilities - [x] 🎨 UI/UX improvement - [x] ⚡ Performance improvement - [x] User Interface/Experience - [x] Manual testing performed - [x] Cross-browser testing (if UI changes) - [x] Mobile responsiveness verified (if UI changes) - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] My code follows the project's style guidelines This PR is part of our ongoing effort to standardize the UI color system and improve the dark mode experience. The changes to the AI Chat component make tool responses more informative and easier to understand. _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._
1 parent ab042e2 commit c20868c

File tree

21 files changed

+364
-203
lines changed

21 files changed

+364
-203
lines changed

apps/mail/app/(routes)/settings/labels/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export default function LabelsPage() {
135135
<span>{label.name}</span>
136136
</Badge>
137137
</div>
138-
<div className="absolute right-2 z-[25] flex items-center gap-1 rounded-xl border bg-white p-1 opacity-0 shadow-sm transition-opacity group-hover:opacity-100 dark:bg-[#1A1A1A]">
138+
<div className="dark:bg-panelDark absolute right-2 z-[25] flex items-center gap-1 rounded-xl border bg-white p-1 opacity-0 shadow-sm transition-opacity group-hover:opacity-100">
139139
<Tooltip>
140140
<TooltipTrigger asChild>
141141
<Button
@@ -147,7 +147,7 @@ export default function LabelsPage() {
147147
<Pencil className="text-[#898989]" />
148148
</Button>
149149
</TooltipTrigger>
150-
<TooltipContent className="mb-1 bg-white dark:bg-[#1A1A1A]">
150+
<TooltipContent className="dark:bg-panelDark mb-1 bg-white">
151151
Edit Label
152152
</TooltipContent>
153153
</Tooltip>
@@ -162,7 +162,7 @@ export default function LabelsPage() {
162162
<Bin className="fill-[#F43F5E]" />
163163
</Button>
164164
</TooltipTrigger>
165-
<TooltipContent className="mb-1 bg-white dark:bg-[#1A1A1A]">
165+
<TooltipContent className="dark:bg-panelDark mb-1 bg-white">
166166
Delete Label
167167
</TooltipContent>
168168
</Tooltip>

apps/mail/app/globals.css

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -289,17 +289,6 @@
289289
padding: 0.5rem;
290290
}
291291

292-
/* remove outline */
293-
.ProseMirror:focus {
294-
outline: none;
295-
}
296-
/* set */
297-
.ProseMirror {
298-
min-height: 200px;
299-
max-height: 300px;
300-
overflow: scroll;
301-
}
302-
303292
/* Animation keyframes */
304293
@keyframes shine {
305294
from {

apps/mail/components/context/thread-context.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ export function ThreadContextMenu({
434434
{children}
435435
</ContextMenuTrigger>
436436
<ContextMenuContent
437-
className="w-56 bg-white dark:bg-[#1A1A1A]"
437+
className="dark:bg-panelDark w-56 bg-white"
438438
onContextMenu={(e) => e.preventDefault()}
439439
>
440440
{primaryActions.map(renderAction)}
@@ -446,7 +446,7 @@ export function ThreadContextMenu({
446446
<Tag className="mr-2.5 h-4 w-4" />
447447
{t('common.mail.labels')}
448448
</ContextMenuSubTrigger>
449-
<ContextMenuSubContent className="w-48 bg-white dark:bg-[#1A1A1A]">
449+
<ContextMenuSubContent className="dark:bg-panelDark w-48 bg-white">
450450
<LabelsList threadId={threadId} />
451451
</ContextMenuSubContent>
452452
</ContextMenuSub>

apps/mail/components/create/ai-chat.tsx

Lines changed: 147 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
2+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
23
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
34
import { CurvedArrow, Puzzle, Stop } from '../icons/icons';
5+
import useComposeEditor from '@/hooks/use-compose-editor';
6+
import { InfoIcon, Mic, Mic2Icon } from 'lucide-react';
47
import { useRef, useCallback, useEffect } from 'react';
58
import { PricingDialog } from '../ui/pricing-dialog';
69
import { Markdown } from '@react-email/components';
710
import { useAIFullScreen } from '../ui/ai-sidebar';
811
import { useBilling } from '@/hooks/use-billing';
912
import { TextShimmer } from '../ui/text-shimmer';
1013
import { useThread } from '@/hooks/use-threads';
14+
import { useConversation } from '@11labs/react';
1115
import { MailLabels } from '../mail/mail-list';
1216
import { cn, getEmailLogo } from '@/lib/utils';
17+
import { EditorContent } from '@tiptap/react';
18+
import { Tools } from '../../types/tools';
1319
import { Button } from '../ui/button';
1420
import { format } from 'date-fns-tz';
1521
import { useQueryState } from 'nuqs';
@@ -26,7 +32,6 @@ const renderThread = (thread: { id: string; title: string; snippet: string }) =>
2632
const handleClick = () => {
2733
setThreadId(thread.id);
2834
setAiSidebarOpen(null);
29-
// Reset fullscreen state when clicking on a thread
3035
setIsFullScreen(null);
3136
};
3237

@@ -49,7 +54,7 @@ const renderThread = (thread: { id: string; title: string; snippet: string }) =>
4954
</Avatar>
5055
<div className="flex w-full flex-col gap-1.5">
5156
<div className="flex w-full items-center justify-between gap-2">
52-
<p className="text-sm font-medium text-black dark:text-white">
57+
<p className="max-w-[20ch] truncate text-sm font-medium text-black dark:text-white">
5358
{getThread.latest?.sender?.name}
5459
</p>
5560
<span className="max-w-[180px] truncate text-xs text-[#8C8C8C] dark:text-[#8C8C8C]">
@@ -140,6 +145,7 @@ interface Message {
140145
result?: {
141146
threads?: Array<{ id: string; title: string; snippet: string }>;
142147
};
148+
args?: any;
143149
};
144150
}>;
145151
}
@@ -156,6 +162,92 @@ export interface AIChatProps {
156162
onModelChange?: (model: string) => void;
157163
}
158164

165+
const ToolResponse = ({ toolName, result, args }: { toolName: string; result: any; args: any }) => {
166+
const renderContent = () => {
167+
switch (toolName) {
168+
case Tools.ListThreads:
169+
case Tools.AskZeroMailbox:
170+
return result?.threads ? <RenderThreads threads={result.threads} /> : null;
171+
172+
case Tools.GetThread:
173+
return result?.thread ? (
174+
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
175+
<div className="mb-2 flex items-center gap-2">
176+
<Avatar className="h-8 w-8">
177+
<AvatarImage src={getEmailLogo(result.thread.sender?.email)} />
178+
<AvatarFallback>{result.thread.sender?.name?.[0]?.toUpperCase()}</AvatarFallback>
179+
</Avatar>
180+
<div>
181+
<p className="font-medium">{result.thread.sender?.name}</p>
182+
<p className="text-sm text-gray-500">{result.thread.subject}</p>
183+
</div>
184+
</div>
185+
<div className="prose dark:prose-invert max-w-none">
186+
<Markdown>{result.thread.body}</Markdown>
187+
</div>
188+
</div>
189+
) : null;
190+
191+
case Tools.GetUserLabels:
192+
return result?.labels ? (
193+
<div className="flex flex-wrap gap-2">
194+
{result.labels.map((label: any) => (
195+
<MailLabels key={label.id} labels={[label]} />
196+
))}
197+
</div>
198+
) : null;
199+
200+
case Tools.WebSearch:
201+
return (
202+
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
203+
<div className="prose dark:prose-invert max-w-none">
204+
<Markdown>{result}</Markdown>
205+
</div>
206+
</div>
207+
);
208+
209+
case Tools.ComposeEmail:
210+
return result?.newBody ? (
211+
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
212+
<div className="prose dark:prose-invert max-w-none">
213+
<Markdown>{result.newBody}</Markdown>
214+
</div>
215+
</div>
216+
) : null;
217+
218+
default:
219+
if (result?.success) {
220+
return (
221+
<div className="text-sm text-green-600 dark:text-green-400">
222+
Operation completed successfully
223+
</div>
224+
);
225+
}
226+
return null;
227+
}
228+
};
229+
230+
const content = renderContent();
231+
if (!content) return null;
232+
233+
return (
234+
<div className="group relative">
235+
<Tooltip>
236+
<TooltipTrigger asChild>
237+
<InfoIcon className="fill-subtleWhite text-subtleBlack dark:fill-subtleBlack h-4 w-4 dark:text-[#373737]" />
238+
</TooltipTrigger>
239+
<TooltipContent>
240+
<div className="text-xs">
241+
<p className="mb-1 font-medium">Tool Arguments:</p>
242+
<pre className="whitespace-pre-wrap break-words">{JSON.stringify(args, null, 2)}</pre>
243+
</div>
244+
</TooltipContent>
245+
</Tooltip>
246+
{content}
247+
</div>
248+
);
249+
};
250+
159251
export function AIChat({
160252
messages,
161253
input,
@@ -178,6 +270,24 @@ export function AIChat({
178270
}
179271
}, []);
180272

273+
const editor = useComposeEditor({
274+
placeholder: 'Ask Zero to do anything...',
275+
onLengthChange: () => setInput(editor.getText()),
276+
onKeydown(event) {
277+
if (event.key === 'Enter' && !event.metaKey && !event.shiftKey) {
278+
event.preventDefault();
279+
handleSubmit(event as unknown as React.FormEvent<HTMLFormElement>);
280+
editor.commands.clearContent(true);
281+
}
282+
},
283+
});
284+
285+
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
286+
e.preventDefault();
287+
handleSubmit(e);
288+
editor.commands.clearContent(true);
289+
};
290+
181291
useEffect(() => {
182292
scrollToBottom();
183293
}, [messages, scrollToBottom]);
@@ -219,21 +329,22 @@ export function AIChat({
219329
</div>
220330
) : (
221331
messages.map((message, index) => {
222-
// Separate text and tool-invocation parts
223332
const textParts = message.parts.filter((part) => part.type === 'text');
224333
const toolParts = message.parts.filter((part) => part.type === 'tool-invocation');
225334
return (
226335
<div key={`${message.id}-${index}`} className="flex flex-col gap-2">
227336
{toolParts.map((part, idx) =>
228-
part.toolInvocation &&
229-
'result' in part.toolInvocation &&
230-
part.toolInvocation.result &&
231-
'threads' in part.toolInvocation.result ? (
232-
<RenderThreads threads={part.toolInvocation.result.threads ?? []} key={idx} />
337+
part.toolInvocation && part.toolInvocation.result ? (
338+
<ToolResponse
339+
key={idx}
340+
toolName={part.toolInvocation.toolName}
341+
result={part.toolInvocation.result}
342+
args={part.toolInvocation.args}
343+
/>
233344
) : null,
234345
)}
235346
{textParts.length > 0 && (
236-
<div
347+
<p
237348
className={cn(
238349
'flex w-fit flex-col gap-2 rounded-lg text-sm',
239350
message.role === 'user'
@@ -242,10 +353,9 @@ export function AIChat({
242353
)}
243354
>
244355
{textParts.map(
245-
(part) =>
246-
part.text && <Markdown key={part.text}>{part.text || ' '}</Markdown>,
356+
(part) => part.text && <span key={part.text}>{part.text || ' '}</span>,
247357
)}
248-
</div>
358+
</p>
249359
)}
250360
</div>
251361
);
@@ -270,45 +380,39 @@ export function AIChat({
270380

271381
{/* Fixed input at bottom */}
272382
<div className={cn('mb-4 flex-shrink-0 px-4', isFullScreen ? 'px-0' : '')}>
273-
<div className="bg-offsetLight relative rounded-lg dark:bg-[#141414]">
383+
<div className="bg-offsetLight relative rounded-lg p-2 dark:bg-[#202020]">
274384
{showVoiceChat ? (
275385
<VoiceChat onClose={() => setShowVoiceChat(false)} />
276386
) : (
277387
<div className="flex flex-col">
278388
<div className="w-full">
279-
<form id="ai-chat-form" onSubmit={handleSubmit} className="relative">
280-
<Input
281-
ref={inputRef}
282-
readOnly={!chatMessages.enabled}
283-
value={input}
284-
onChange={(e) => setInput(e.target.value)}
285-
placeholder="Ask Zero to do anything..."
286-
className="placeholder:text-muted-foreground h-8 w-full resize-none rounded-lg border-none bg-white px-3 py-2 pr-10 text-sm ring-0 focus:ring-0 focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[#141414]"
287-
/>
288-
{status === 'ready' ? (
289-
<button
290-
form="ai-chat-form"
291-
type="submit"
292-
className="absolute right-1 top-1/2 inline-flex h-6 -translate-y-1/2 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-lg"
293-
disabled={!input.trim() || !chatMessages.enabled}
294-
>
295-
<div className="dark:bg[#141414] flex h-5 items-center justify-center gap-1 rounded-sm bg-[#262626] px-1 pr-0.5">
296-
<CurvedArrow className="mt-1.5 h-4 w-4 fill-white dark:fill-[#929292]" />
297-
</div>
298-
</button>
299-
) : (
300-
<button
301-
onClick={stop}
302-
type="button"
303-
className="absolute right-1 top-1/2 inline-flex h-6 -translate-y-1/2 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-lg"
389+
<form id="ai-chat-form" onSubmit={onSubmit} className="relative">
390+
<div className="grow self-stretch overflow-y-auto bg-[#FFFFFF] outline-white/5 dark:bg-[#202020]">
391+
<div
392+
onClick={() => {
393+
editor.commands.focus();
394+
}}
395+
className={cn('max-h-[100px] w-full')}
304396
>
305-
<div className="flex h-5 items-center justify-center gap-1 rounded-sm px-1">
306-
<Stop className="h-4 w-4 fill-[#DE5555]" />
307-
</div>
308-
</button>
309-
)}
397+
<EditorContent editor={editor} className="h-full w-full" />
398+
</div>
399+
</div>
310400
</form>
311401
</div>
402+
<div className="grid">
403+
<div className="flex justify-end">
404+
<button
405+
form="ai-chat-form"
406+
type="submit"
407+
className="inline-flex cursor-pointer gap-1.5 rounded-lg"
408+
disabled={!chatMessages.enabled}
409+
>
410+
<div className="dark:bg[#141414] flex h-5 items-center justify-center gap-1 rounded-sm bg-[#262626] px-2 pr-1">
411+
<CurvedArrow className="mt-1.5 h-4 w-4 fill-white dark:fill-[#929292]" />
412+
</div>
413+
</button>
414+
</div>
415+
</div>
312416
</div>
313417
)}
314418
</div>

apps/mail/components/create/create-email.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function CreateEmail({
9292

9393
const userEmail = activeAccount?.email || activeConnection?.email || session?.user?.email || '';
9494
const userName = activeAccount?.name || activeConnection?.name || session?.user?.name || '';
95-
95+
9696
const handleSendEmail = async (data: {
9797
to: string[];
9898
cc?: string[];
@@ -111,9 +111,7 @@ export function CreateEmail({
111111
subject: data.subject,
112112
message: data.message,
113113
attachments: await serializeFiles(data.attachments),
114-
fromEmail: userName.trim()
115-
? `${userName.replace(/[<>]/g, '')} <${fromEmail}>`
116-
: fromEmail,
114+
fromEmail: userName.trim() ? `${userName.replace(/[<>]/g, '')} <${fromEmail}>` : fromEmail,
117115
draftId: draftId ?? undefined,
118116
});
119117

@@ -160,9 +158,11 @@ export function CreateEmail({
160158
<div className="flex min-h-screen flex-col items-center justify-center gap-1">
161159
<div className="flex w-[750px] justify-start">
162160
<DialogClose asChild className="flex">
163-
<button className="flex items-center gap-1 rounded-lg bg-[#F0F0F0] px-2 py-1.5 dark:bg-[#1A1A1A]">
164-
<X className="mt-0.5 h-3.5 w-3.5 fill-[#6D6D6D] dark:fill-[#929292]" />
165-
<span className="text-sm font-medium text-[#6D6D6D] dark:text-white">esc</span>
161+
<button className="dark:bg-panelDark flex items-center gap-1 rounded-lg bg-[#F0F0F0] px-2 py-1.5">
162+
<X className="fill-muted-foreground mt-0.5 h-3.5 w-3.5 dark:fill-[#929292]" />
163+
<span className="text-muted-foreground text-sm font-medium dark:text-white">
164+
esc
165+
</span>
166166
</button>
167167
</DialogClose>
168168
</div>

0 commit comments

Comments
 (0)