Implement prompts dialog and integrate prompt management in brain functionality. Add new prompts for summarization and enhance the brain router to fetch prompts based on connection ID. Clean up unused imports and refactor components for better structure.

This commit is contained in:
Aj Wazzan
2025-05-18 22:26:41 -07:00
parent 1711d2c315
commit d26f69507b
7 changed files with 570 additions and 391 deletions

View File

@@ -293,7 +293,7 @@ const AutoLabelingSettings = () => {
</ScrollArea>
<DialogFooter className="mt-4">
<div className="flex w-full justify-between">
<Button variant="outline" size="sm">
<Button onClick={handleToggleAutolabeling} variant="outline" size="sm">
{brainState?.enabled ? 'Disable' : 'Enable'}
</Button>
<div className="flex gap-2">

View File

@@ -5,7 +5,6 @@ import {
FileText,
Expand,
Plus,
GitBranchPlus,
Maximize2 as LucideMaximize2,
Minimize2 as LucideMinimize2,
} from 'lucide-react';
@@ -27,8 +26,8 @@ import {
useRef,
} from 'react';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip';
import { ArrowsPointingIn, ArrowsPointingOut, PanelLeftOpen, Paper, Phone } from '../icons/icons';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
import { ArrowsPointingIn, ArrowsPointingOut, PanelLeftOpen, Phone } from '../icons/icons';
import { AI_SIDEBAR_COOKIE_NAME, SIDEBAR_COOKIE_MAX_AGE } from '@/lib/constants';
import { StyledEmailAssistantSystemPrompt, AiChatPrompt } from '@/lib/prompts';
import { usePathname, useSearchParams, useParams } from 'next/navigation';
@@ -39,6 +38,7 @@ import { useTRPC } from '@/providers/query-provider';
import { Tools } from '../../../server/src/types';
import { useBilling } from '@/hooks/use-billing';
import { PricingDialog } from './pricing-dialog';
import { PromptsDialog } from './prompts-dialog';
import { Button } from '@/components/ui/button';
import { useHotkeys } from 'react-hotkeys-hook';
import { useLabels } from '@/hooks/use-labels';
@@ -54,6 +54,133 @@ import Image from 'next/image';
import { toast } from 'sonner';
import Link from 'next/link';
interface ChatHeaderProps {
onClose: () => void;
onToggleFullScreen: () => void;
onToggleViewMode: () => void;
isFullScreen: boolean;
isPopup: boolean;
chatMessages: { remaining: number };
isPro: boolean;
onUpgrade: () => void;
onNewChat: () => void;
}
function ChatHeader({
onClose,
onToggleFullScreen,
onToggleViewMode,
isFullScreen,
isPopup,
chatMessages,
isPro,
onUpgrade,
onNewChat,
}: ChatHeaderProps) {
return (
<div className="relative flex items-center justify-between border-b border-[#E7E7E7] px-2.5 pb-[10px] pt-[13px] dark:border-[#252525]">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onClose} variant="ghost" className="md:h-fit md:px-2">
<X className="dark:text-iconDark text-iconLight" />
<span className="sr-only">Close chat</span>
</Button>
</TooltipTrigger>
<TooltipContent>Close chat</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="flex items-center gap-2">
{isFullScreen ? (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={onToggleFullScreen}
variant="ghost"
className="hidden md:flex md:h-fit md:px-2"
>
<ArrowsPointingIn className="dark:fill-iconDark fill-iconLight" />
<span className="sr-only">Toggle view mode</span>
</Button>
</TooltipTrigger>
<TooltipContent>Remove full screen</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={onToggleFullScreen}
variant="ghost"
className="hidden md:flex md:h-fit md:px-2 [&>svg]:size-2"
>
<Expand className="dark:text-iconDark text-iconLight" />
<span className="sr-only">Toggle view mode</span>
</Button>
</TooltipTrigger>
<TooltipContent>Go to full screen</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onToggleViewMode} variant="ghost" className="md:h-fit md:px-2">
{isPopup ? (
<PanelLeftOpen className="dark:fill-iconDark fill-iconLight" />
) : (
<Phone className="dark:fill-iconDark fill-iconLight" />
)}
<span className="sr-only"></span>
</Button>
</TooltipTrigger>
<TooltipContent>Go to {isPopup ? 'sidebar' : 'popup'}</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
{!isPro && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild className="md:h-fit md:px-2">
<div>
<Gauge value={50 - chatMessages.remaining!} size="small" showValue={true} />
</div>
</TooltipTrigger>
<TooltipContent>
<p>You've used {50 - chatMessages.remaining!} out of 50 chat messages.</p>
<p className="mb-2">Upgrade for unlimited messages!</p>
<Button onClick={onUpgrade} className="h-8 w-full">
Upgrade
</Button>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<PromptsDialog />
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={onNewChat} variant="ghost" className="md:h-fit md:px-2">
<Plus className="dark:text-iconDark text-iconLight" />
<span className="sr-only">New chat</span>
</Button>
</TooltipTrigger>
<TooltipContent>New chat</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
);
}
interface AISidebarProps {
className?: string;
}
@@ -62,14 +189,14 @@ type ViewMode = 'sidebar' | 'popup' | 'fullscreen';
export function useAIFullScreen() {
const [isFullScreenQuery, setIsFullScreenQuery] = useQueryState('isFullScreen');
// Initialize isFullScreen state from query parameter or localStorage
const [isFullScreen, setIsFullScreenState] = useState<boolean>(() => {
// First check query parameter
if (isFullScreenQuery) {
return isFullScreenQuery === 'true';
}
// Then check localStorage if on client
if (typeof window !== 'undefined') {
const savedFullScreen = localStorage.getItem('ai-fullscreen');
@@ -77,23 +204,23 @@ export function useAIFullScreen() {
return savedFullScreen === 'true';
}
}
return false;
});
// Update both query parameter and localStorage when fullscreen state changes
const setIsFullScreen = useCallback(
(value: boolean) => {
// Immediately update local state for faster UI response
setIsFullScreenState(value);
// For exiting fullscreen, we need to be extra careful to ensure state is updated properly
if (!value) {
// Force immediate removal from localStorage for faster response
if (typeof window !== 'undefined') {
localStorage.removeItem('ai-fullscreen');
}
// Use setTimeout to ensure the state update happens in the next tick
// This helps prevent the need for double-clicking
setTimeout(() => {
@@ -102,7 +229,7 @@ export function useAIFullScreen() {
} else {
// For entering fullscreen, we can use the normal flow
setIsFullScreenQuery('true').catch(console.error);
// Save to localStorage for persistence across sessions
if (typeof window !== 'undefined') {
localStorage.setItem('ai-fullscreen', 'true');
@@ -111,7 +238,7 @@ export function useAIFullScreen() {
},
[setIsFullScreenQuery],
);
// Sync with query parameter on mount or when it changes
useEffect(() => {
const queryValue = isFullScreenQuery === 'true';
@@ -119,7 +246,7 @@ export function useAIFullScreen() {
setIsFullScreenState(queryValue);
}
}, [isFullScreenQuery, isFullScreen]);
// Initialize from localStorage on mount if query parameter is not set
useEffect(() => {
if (typeof window !== 'undefined' && !isFullScreenQuery) {
@@ -128,13 +255,13 @@ export function useAIFullScreen() {
setIsFullScreenQuery('true');
}
}
// Force a re-render when exiting fullscreen mode
if (isFullScreenQuery === null && isFullScreen) {
setIsFullScreenState(false);
}
}, [isFullScreenQuery, setIsFullScreenQuery, isFullScreen]);
return {
isFullScreen,
setIsFullScreen,
@@ -145,11 +272,11 @@ export function useAISidebar() {
const [open, setOpenQuery] = useQueryState('aiSidebar');
const [viewModeQuery, setViewModeQuery] = useQueryState('viewMode');
const { isFullScreen, setIsFullScreen } = useAIFullScreen();
// Initialize viewMode from query parameter, localStorage, or default to 'sidebar'
const [viewMode, setViewModeState] = useState<ViewMode>(() => {
if (viewModeQuery) return viewModeQuery as ViewMode;
// Check localStorage for saved state if on client
if (typeof window !== 'undefined') {
const savedViewMode = localStorage.getItem('ai-viewmode');
@@ -157,7 +284,7 @@ export function useAISidebar() {
return savedViewMode as ViewMode;
}
}
return 'popup';
});
@@ -166,15 +293,15 @@ export function useAISidebar() {
async (mode: ViewMode) => {
setViewModeState(mode);
await setViewModeQuery(mode === 'popup' ? null : mode);
// Save to localStorage for persistence across sessions
if (typeof window !== 'undefined') {
localStorage.setItem('ai-viewmode', mode);
}
},
[setViewModeQuery]
[setViewModeQuery],
);
// Function to set open state and save to localStorage
const setOpen = useCallback(
(openState: boolean) => {
@@ -184,7 +311,7 @@ export function useAISidebar() {
if (typeof window !== 'undefined') {
localStorage.removeItem('ai-sidebar-open');
}
// Use setTimeout to ensure the query update happens in the next tick
// This helps prevent the need for double-clicking
setTimeout(() => {
@@ -193,24 +320,21 @@ export function useAISidebar() {
} else {
// For opening, we can use the normal flow
setOpenQuery('true').catch(console.error);
// Save to localStorage
if (typeof window !== 'undefined') {
localStorage.setItem('ai-sidebar-open', 'true');
}
}
},
[setOpenQuery]
[setOpenQuery],
);
// Toggle open state
const toggleOpen = useCallback(
() => {
const newState = !(open === 'true');
setOpen(newState);
},
[open, setOpen]
);
const toggleOpen = useCallback(() => {
const newState = !(open === 'true');
setOpen(newState);
}, [open, setOpen]);
// Initialize from localStorage on mount
useEffect(() => {
@@ -245,16 +369,16 @@ export function useAISidebar() {
}
function AISidebar({ className }: AISidebarProps) {
const {
open,
setOpen,
viewMode,
const {
open,
setOpen,
viewMode,
setViewMode,
isFullScreen,
setIsFullScreen,
isFullScreen,
setIsFullScreen,
toggleViewMode,
isSidebar,
isPopup
isPopup,
} = useAISidebar();
const [resetKey, setResetKey] = useState(0);
const [showPricing, setShowPricing] = useState(false);
@@ -369,173 +493,22 @@ function AISidebar({ className }: AISidebarProps) {
>
<div className={cn('h-[calc(98vh+6px)]', 'flex flex-col', '', className)}>
<div className="flex h-full flex-col">
<div className="relative flex items-center justify-between border-b border-[#E7E7E7] px-2.5 pb-[10px] pt-[13px] dark:border-[#252525]">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => {
setOpen(false);
setIsFullScreen(false);
}}
variant="ghost"
className="md:h-fit md:px-2"
>
<X className="dark:text-iconDark text-iconLight" />
<span className="sr-only">Close chat</span>
</Button>
</TooltipTrigger>
<TooltipContent>Close chat</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="flex items-center gap-2">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => setIsFullScreen(!isFullScreen)}
// onClick={toggleViewMode}
variant="ghost"
className="hidden md:flex md:h-fit md:px-2 [&>svg]:size-2"
>
<Expand className="dark:text-iconDark text-iconLight" />
<span className="sr-only">Toggle view mode</span>
</Button>
</TooltipTrigger>
<TooltipContent>Go to full screen</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={toggleViewMode}
variant="ghost"
className="md:h-fit md:px-2"
>
{isFullScreen ? (
<LucideMinimize2 className="dark:fill-iconDark fill-iconLight" />
) : (
<Phone className="dark:fill-iconDark fill-iconLight" />
)}
<span className="sr-only"></span>
</Button>
</TooltipTrigger>
<TooltipContent>Go to popup</TooltipContent>
</Tooltip>
</TooltipProvider>
{!isPro && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild className="md:h-fit md:px-2">
<div>
<Gauge
value={50 - chatMessages.remaining!}
size="small"
showValue={true}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
You've used {50 - chatMessages.remaining!} out of 50 chat
messages.
</p>
<p className="mb-2">Upgrade for unlimited messages!</p>
<Button onClick={handleUpgrade} className="h-8 w-full">
Upgrade
</Button>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<TooltipProvider delayDuration={0}>
<Dialog>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
variant="ghost"
className="md:h-fit md:px-2 [&>svg]:size-3"
>
<Paper className="dark:fill-iconDark fill-iconLight h-3.5 w-3.5" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>Prompts</TooltipContent>
</Tooltip>
<DialogContent showOverlay={true}>
<DialogHeader>
<DialogTitle>AI System Prompts</DialogTitle>
<DialogDescription>
We believe in Open Source, so we're open sourcing our AI system
prompts. Soon you will be able to customize them to your liking.
For now, here are the default prompts:
</DialogDescription>
</DialogHeader>
<div className="text-muted-foreground mb-1 mt-4 flex gap-2 text-sm">
<span>Zero Chat / System Prompt</span>
<Link
href={'https://github.com/Mail-0/Zero.git'}
target="_blank"
className="flex items-center gap-1 underline"
>
<span>Contribute</span>
<GitBranchPlus className="h-4 w-4" />
</Link>
</div>
<Textarea
className="min-h-60"
readOnly
value={AiChatPrompt('', '', '')}
/>
<div className="text-muted-foreground mb-1 mt-4 flex gap-2 text-sm">
<span>Zero Compose / System Prompt</span>
<Link
href={'https://github.com/Mail-0/Zero.git'}
target="_blank"
className="flex items-center gap-1 underline"
>
<span>Contribute</span>
<GitBranchPlus className="h-4 w-4" />
</Link>
</div>
<Textarea
className="min-h-60"
readOnly
value={StyledEmailAssistantSystemPrompt().trim()}
/>
</DialogContent>
</Dialog>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={handleNewChat}
variant="ghost"
className="md:h-fit md:px-2"
>
<Plus className="dark:text-iconDark text-iconLight" />
<span className="sr-only">New chat</span>
</Button>
</TooltipTrigger>
<TooltipContent>New chat</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div className="b relative flex-1 overflow-hidden">
<AIChat
key={resetKey}
{...chatState}
// Pass the chat state to preserve conversation when switching between desktop/mobile
/>
<ChatHeader
onClose={() => {
setOpen(false);
setIsFullScreen(false);
}}
onToggleFullScreen={() => setIsFullScreen(!isFullScreen)}
onToggleViewMode={toggleViewMode}
isFullScreen={isFullScreen}
isPopup={isPopup}
chatMessages={chatMessages}
isPro={isPro}
onUpgrade={handleUpgrade}
onNewChat={handleNewChat}
/>
<div className="relative flex-1 overflow-hidden">
<AIChat key={resetKey} {...chatState} />
</div>
</div>
</div>
@@ -547,20 +520,17 @@ function AISidebar({ className }: AISidebarProps) {
<div
className={cn(
'fixed inset-0 z-50 flex items-center justify-center bg-transparent p-4 opacity-40 backdrop-blur-sm transition-opacity duration-150 hover:opacity-100 sm:inset-auto sm:bottom-4 sm:right-4 sm:flex-col sm:items-end sm:justify-end sm:p-0',
'md:hidden', // Hide on md+ screens by default
isPopup && !isFullScreen && 'md:flex', // Show on md+ screens when in popup mode
isFullScreen && '!inset-0 !flex !p-0 !opacity-100 !backdrop-blur-none', // Full screen mode
'md:hidden',
isPopup && !isFullScreen && 'md:flex',
isFullScreen && '!inset-0 !flex !p-0 !opacity-100 !backdrop-blur-none',
)}
>
{/* Chat popup container - only visible on small screens or when popup mode is selected */}
<div
className={cn(
'bg-panelLight dark:bg-panelDark w-full overflow-hidden rounded-2xl border border-[#E7E7E7] shadow-lg dark:border-[#252525]',
'md:hidden', // Hide on md+ screens by default
isPopup &&
!isFullScreen &&
'w-[600px] max-w-[90vw] sm:w-[400px] md:block', // Show on md+ screens when in popup mode
isFullScreen && '!block !max-w-none !rounded-none !border-none', // Full screen mode
'md:hidden',
isPopup && !isFullScreen && 'w-[600px] max-w-[90vw] sm:w-[400px] md:block',
isFullScreen && '!block !max-w-none !rounded-none !border-none',
)}
>
<div
@@ -569,182 +539,20 @@ function AISidebar({ className }: AISidebarProps) {
isFullScreen ? 'h-screen' : 'h-[90vh] sm:h-[600px] sm:max-h-[85vh]',
)}
>
<div className="relative flex items-center justify-between border-b border-[#E7E7E7] px-1 py-2 pb-1 dark:border-[#252525]">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => {
setOpen(false);
setIsFullScreen(false);
}}
variant="ghost"
className="md:h-fit md:px-2"
>
<X className="dark:text-iconDark text-iconLight" />
<span className="sr-only">Close chat</span>
</Button>
</TooltipTrigger>
<TooltipContent>Close chat</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="flex items-center gap-2">
{isFullScreen && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => setIsFullScreen(!isFullScreen)}
variant="ghost"
className="hidden md:flex md:h-fit md:px-2"
>
<ArrowsPointingIn className="dark:fill-iconDark fill-iconLight" />
<span className="sr-only">Toggle view mode</span>
</Button>
</TooltipTrigger>
<TooltipContent>Remove full screen</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{!isFullScreen && (
<>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => setIsFullScreen(!isFullScreen)}
// onClick={toggleViewMode}
variant="ghost"
className="hidden md:flex md:h-fit md:px-2 [&>svg]:size-2"
>
<Expand className="dark:text-iconDark text-iconLight" />
<span className="sr-only">Toggle view mode</span>
</Button>
</TooltipTrigger>
<TooltipContent>Go to full screen</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={toggleViewMode}
variant="ghost"
className="md:h-fit md:px-2"
>
<PanelLeftOpen className="dark:fill-iconDark fill-iconLight" />
<span className="sr-only"></span>
</Button>
</TooltipTrigger>
<TooltipContent>Go to sidebar</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
{!isPro && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild className="md:h-fit md:px-2">
<div>
<Gauge
value={50 - chatMessages.remaining!}
size="small"
showValue={true}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
You've used {50 - chatMessages.remaining!} out of 50 chat messages.
</p>
<p className="mb-2">Upgrade for unlimited messages!</p>
<Button onClick={handleUpgrade} className="h-8 w-full">
Upgrade
</Button>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<TooltipProvider delayDuration={0}>
<Dialog>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button variant="ghost" className="md:h-fit md:px-2 [&>svg]:size-3">
<Paper className="dark:fill-iconDark fill-iconLight h-3.5 w-3.5" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>Prompts</TooltipContent>
</Tooltip>
<DialogContent showOverlay={true}>
<DialogHeader>
<DialogTitle>AI System Prompts</DialogTitle>
<DialogDescription>
We believe in Open Source, so we're open sourcing our AI system
prompts. Soon you will be able to customize them to your liking. For
now, here are the default prompts:
</DialogDescription>
</DialogHeader>
<div className="text-muted-foreground mb-1 mt-4 flex gap-2 text-sm">
<span>Zero Chat / System Prompt</span>
<Link
href={'https://github.com/Mail-0/Zero.git'}
target="_blank"
className="flex items-center gap-1 underline"
>
<span>Contribute</span>
<GitBranchPlus className="h-4 w-4" />
</Link>
</div>
<Textarea
className="min-h-60"
readOnly
value={AiChatPrompt('', '', '')}
/>
<div className="text-muted-foreground mb-1 mt-4 flex gap-2 text-sm">
<span>Zero Compose / System Prompt</span>
<Link
href={'https://github.com/Mail-0/Zero.git'}
target="_blank"
className="flex items-center gap-1 underline"
>
<span>Contribute</span>
<GitBranchPlus className="h-4 w-4" />
</Link>
</div>
<Textarea
className="min-h-60"
readOnly
value={StyledEmailAssistantSystemPrompt().trim()}
/>
</DialogContent>
</Dialog>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={handleNewChat}
variant="ghost"
className="md:h-fit md:px-2"
>
<Plus className="dark:text-iconDark text-iconLight" />
<span className="sr-only">New chat</span>
</Button>
</TooltipTrigger>
<TooltipContent>New chat</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<ChatHeader
onClose={() => {
setOpen(false);
setIsFullScreen(false);
}}
onToggleFullScreen={() => setIsFullScreen(!isFullScreen)}
onToggleViewMode={toggleViewMode}
isFullScreen={isFullScreen}
isPopup={isPopup}
chatMessages={chatMessages}
isPro={isPro}
onUpgrade={handleUpgrade}
onNewChat={handleNewChat}
/>
<div className="relative flex-1 overflow-hidden">
<AIChat key={resetKey} {...chatState} />
</div>

View File

@@ -0,0 +1,127 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from './dialog';
import {
BookDashedIcon,
GitBranchPlus,
MessageSquareIcon,
RefreshCcwDotIcon,
SendIcon,
} from 'lucide-react';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip';
import { AiChatPrompt, StyledEmailAssistantSystemPrompt } from '@/lib/prompts';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { Button } from '@/components/ui/button';
import { Paper } from '../icons/icons';
import { Textarea } from './textarea';
import Link from 'next/link';
export function PromptsDialog() {
const trpc = useTRPC();
const { data: prompts } = useQuery(trpc.brain.getPrompts.queryOptions());
return (
<TooltipProvider delayDuration={0}>
<Dialog>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button variant="ghost" className="md:h-fit md:px-2 [&>svg]:size-3">
<Paper className="dark:fill-iconDark fill-iconLight h-3.5 w-3.5" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>Prompts</TooltipContent>
</Tooltip>
<DialogContent className="max-w-screen-lg" showOverlay={true}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
ZeroAI System Prompts{' '}
<Link
href={'https://github.com/Mail-0/Zero.git'}
target="_blank"
className="flex items-center gap-1 text-xs underline"
>
<span>Contribute</span>
<GitBranchPlus className="h-4 w-4" />
</Link>
</DialogTitle>
<DialogDescription>
We believe in Open Source, so we're open sourcing our AI system prompts. Soon you will
be able to customize them to your liking.
</DialogDescription>
</DialogHeader>
<Tabs className="mt-2">
<TabsList className="w-full justify-start">
<TabsTrigger value="chat">
<MessageSquareIcon className="mr-2 h-4 w-4" /> Chat
</TabsTrigger>
<TabsTrigger value="compose">
<SendIcon className="mr-2 h-4 w-4" /> Compose
</TabsTrigger>
<TabsTrigger value="summarizeThread">
<BookDashedIcon className="mr-2 h-4 w-4" /> Summarize Thread
</TabsTrigger>
<TabsTrigger value="reSummarizeThread">
<RefreshCcwDotIcon className="mr-2 h-4 w-4" /> Re-Summarize Thread
</TabsTrigger>
<TabsTrigger value="summarizeMessage">
<BookDashedIcon className="mr-2 h-4 w-4" /> Summarize Message
</TabsTrigger>
</TabsList>
<TabsContent value="chat">
<span className="text-muted-foreground mb-2 flex gap-2 text-sm">
This system prompt is used in the chat sidebar agent. The agent has multiple tools
available.
</span>
<Textarea className="min-h-60" readOnly value={AiChatPrompt('', '', '')} />
</TabsContent>
<TabsContent value="compose">
<span className="text-muted-foreground mb-2 flex gap-2 text-sm">
This system prompt is used to compose emails that sound like you.
</span>
<Textarea
className="min-h-60"
readOnly
value={StyledEmailAssistantSystemPrompt().trim()}
/>
</TabsContent>
{prompts ? (
<TabsContent value="summarizeThread">
<span className="text-muted-foreground mb-2 flex gap-2 text-sm">
This system prompt is used to summarize threads. It takes the entire thread and
key information and summarizes them.
</span>
<Textarea className="min-h-60" readOnly value={prompts?.SummarizeThread} />
</TabsContent>
) : null}
{prompts ? (
<TabsContent value="reSummarizeThread">
<span className="text-muted-foreground mb-2 flex gap-2 text-sm">
This system prompt is used to re-summarize threads. It's used when the thread
messages change and a new context is needed.
</span>
<Textarea className="min-h-60" readOnly value={prompts?.ReSummarizeThread} />
</TabsContent>
) : null}
{prompts ? (
<TabsContent value="summarizeMessage">
<span className="text-muted-foreground mb-2 flex gap-2 text-sm">
This system prompt is used to summarize messages. It takes a single message and
summarizes it.
</span>
<Textarea className="min-h-60" readOnly value={prompts?.SummarizeMessage} />
</TabsContent>
) : null}
</Tabs>
</DialogContent>
</Dialog>
</TooltipProvider>
);
}

View File

@@ -0,0 +1,192 @@
export const SummarizeMessage = `
<system_prompt>
<role>You are a high-accuracy email summarization agent. Your task is to extract and summarize emails in XML format with absolute precision, ensuring no critical details are lost while maintaining high efficiency.</role>
<instructions>
<extract>
<item>Sender, recipient, and CC names (exclude email addresses)</item>
<item>Exact date and time of the email</item>
<item>All actionable details, including confirmations, requests, deadlines, and follow-ups</item>
</extract>
<omit>
<item>Email addresses</item>
<item>Greetings, sign-offs, and generic pleasantries</item>
<item>Unnecessary or redundant information</item>
</omit>
<format>
<item>Ensure structured, concise, and complete summaries</item>
<item>No omissions, distortions, or misinterpretations</item>
<item>Use parties names, never say "the recipient" or "the sender"</item>
<item>If there are not additional details to add, do not add anything. Do not say "no additional details provided in the body of the email"</item>
<item>If there is not content, say "None". do not say "no content" or "with no message content provided".</item>
</format>
</instructions>
<example_input>
<message>
<from>Josh</from>
<to>Adam</to>
<cc>Emily</cc>
<date>2025-03-24T14:23:00</date>
<subject>83(b) Election Mailing</subject>
<body>Adam,
Nothing further needed on your end I've asked our mail team to expedite the mailing of Adam's 83(b) election, which will go out tomorrow. I'll send the proof of mailing to YC after it is sent out and will separately confirm when done with you.
Best,
Josh</body>
</message>
</example_input>
<expected_output>
<summary>On Monday, March 24, at 2:23 PM, Josh informs Adam (CC: Emily) that no further action is required. The mail team will expedite the mailing of Adam's 83(b) election tomorrow. Josh will send the proof of mailing to YC and confirm separately with Adam once it is sent.</summary>
</expected_output>
<strict_guidelines>Strictly follow these rules. No missing details. No extra fluff. Just precise, high-performance summarization. Never say "Here is"</strict_guidelines>
</system_prompt>`;
export const SummarizeThread = `
<system_prompt>
<role>You are a high-accuracy email thread summarization agent. Your task is to process a full email thread with multiple messages and generate a structured, limited-length summary that retains all critical details, ensuring no information is lost.</role>
<instructions>
<input_structure>
<item>Thread title</item>
<item>List of participants (sender, recipients, CCs)</item>
<item>Ordered sequence of messages, each containing:</item>
<subitem>Sender name</subitem>
<subitem>Timestamp (exact date and time)</subitem>
<subitem>Message content</subitem>
</input_structure>
<output_requirements>
<item>Summarize each message concisely while preserving its exact meaning.</item>
<item>Include all participants and timestamps for context.</item>
<item>Use clear formatting to distinguish different messages.</item>
<item>Ensure the summary is within the length limit while retaining all essential details.</item>
<item>Do not add interpretations, assumptions, or extra context beyond what is provided.</item>
</output_requirements>
</instructions>
<example_input>
<thread>
<title>83(b) Election Mailing</title>
<participants>
<participant>Josh</participant>
<participant>Adam</participant>
<participant>Emily</participant>
</participants>
<messages>
<message>
<from>Josh</from>
<to>Adam</to>
<cc>Emily</cc>
<date>2025-03-24T14:23:00</date>
<body>Adam, nothing further needed on your end. I've asked our mail team to expedite the mailing of Adam's 83(b) election, which will go out tomorrow. I'll send the proof of mailing to YC after it is sent and will confirm separately with you.</body>
</message>
<message>
<from>Adam</from>
<to>Josh</to>
<cc>Emily</cc>
<date>2025-03-24T15:10:00</date>
<body>Thanks, Josh. Please let me know once it's sent.</body>
</message>
<message>
<from>Josh</from>
<to>Adam</to>
<cc>Emily</cc>
<date>2025-03-25T09:45:00</date>
<body>The mail team has sent out the 83(b) election. I've attached the proof of mailing. Let me know if you need anything else.</body>
</message>
</messages>
</thread>
</example_input>
<expected_output>
<summary>
Thread: 83(b) Election Mailing
Participants: Josh, Adam, Emily
- March 24, 2:23 PM Josh informs Adam (CC: Emily) that no further action is needed. The mail team will expedite the mailing of Adam's 83(b) election tomorrow. Proof of mailing will be sent to YC, and Josh will confirm separately.
- March 24, 3:10 PM Adam acknowledges Josh's message and requests confirmation once the mailing is sent.
- March 25, 9:45 AM Josh confirms that the 83(b) election has been sent and attaches proof of mailing. He asks if anything else is needed.
</summary>
</expected_output>
<strict_guidelines>Maintain absolute accuracy. No omissions. No extra assumptions. No distortions. Ensure clarity and brevity within the length limit.</strict_guidelines>
<strict_guidelines>Do not include any notes or additional context beyond the summary.</strict_guidelines>
<strict_guidelines>Never say "Here is"</strict_guidelines>
</system_prompt>
`;
export const ReSummarizeThread = `
<system_prompt>
<role>You are a high-accuracy email thread summarization agent. Your task is to process a full email thread, including new messages and an existing summary, and generate a structured, limited-length updated summary that retains all critical details.</role>
<instructions>
<input_structure>
<item>Thread title</item>
<item>List of participants (sender, recipients, CCs)</item>
<item>Existing summary (if available)</item>
<item>Ordered sequence of new messages, each containing:</item>
<subitem>Sender name</subitem>
<subitem>Timestamp (exact date and time)</subitem>
<subitem>Message content</subitem>
</input_structure>
<update_logic>
<item>If an existing summary is provided, update it by integrating new messages while preserving all prior details.</item>
<item>Maintain chronological order and ensure completeness.</item>
<item>Summarize each new message concisely while preserving its exact meaning.</item>
<item>Ensure clarity and readability by distinguishing different messages.</item>
<item>Enforce a strict length limit while retaining all essential details.</item>
</update_logic>
<strict_requirements>
<item>No omissions, distortions, or assumptions.</item>
<item>Do not modify or rewrite prior content except to append new updates.</item>
<item>Ensure final summary remains structured and factual.</item>
<item>Do not include any notes or additional context beyond the summary.</item>
</strict_requirements>
</instructions>
<example_input>
<thread>
<title>83(b) Election Mailing</title>
<participants>
<participant>Josh</participant>
<participant>Adam</participant>
<participant>Emily</participant>
</participants>
<existing_summary>
Thread: 83(b) Election Mailing
Participants: Josh, Adam, Emily
- March 24, 2:23 PM Josh informs Adam (CC: Emily) that no further action is needed. The mail team will expedite the mailing of Adam's 83(b) election tomorrow. Proof of mailing will be sent to YC, and Josh will confirm separately.
- March 24, 3:10 PM Adam acknowledges Josh's message and requests confirmation once the mailing is sent.
</existing_summary>
<new_messages>
<message>
<from>Josh</from>
<to>Adam</to>
<cc>Emily</cc>
<date>2025-03-25T09:45:00</date>
<body>The mail team has sent out the 83(b) election. I've attached the proof of mailing. Let me know if you need anything else.</body>
</message>
</new_messages>
</thread>
</example_input>
<expected_output>
<updated_summary>
Thread: 83(b) Election Mailing
Participants: Josh, Adam, Emily
- March 24, 2:23 PM Josh informs Adam (CC: Emily) that no further action is needed. The mail team will expedite the mailing of Adam's 83(b) election tomorrow. Proof of mailing will be sent to YC, and Josh will confirm separately.
- March 24, 3:10 PM Adam acknowledges Josh's message and requests confirmation once the mailing is sent.
- March 25, 9:45 AM Josh confirms that the 83(b) election has been sent and attaches proof of mailing. He asks if anything else is needed.
</updated_summary>
</expected_output>
<strict_guidelines>Maintain absolute accuracy. No missing details. No extra assumptions. No modifications to previous content beyond appending updates. Ensure clarity and brevity within the length limit. Never say "Here is"</strict_guidelines>
</system_prompt>`;

View File

@@ -1,4 +1,6 @@
import { ReSummarizeThread, SummarizeMessage, SummarizeThread } from './brain.fallback.prompts';
import { env } from 'cloudflare:workers';
import { EPrompts } from '../types';
export const enableBrainFunction = async (connection: { id: string; providerId: string }) => {
return await env.zero.subscribe({
@@ -13,3 +15,39 @@ export const disableBrainFunction = async (connection: { id: string; providerId:
providerId: connection.providerId,
});
};
const getPromptName = (connectionId: string, prompt: EPrompts) => {
return `${connectionId}-${prompt}`;
};
export const getPrompt = async (promptName: string, fallback: string) => {
const existingPrompt = await env.prompts_storage.get(promptName);
if (!existingPrompt) {
await env.prompts_storage.put(promptName, fallback);
return fallback;
}
return existingPrompt;
};
export const getPrompts = async ({ connectionId }: { connectionId: string }) => {
const prompts: Record<EPrompts, string> = {
[EPrompts.SummarizeMessage]: '',
[EPrompts.ReSummarizeThread]: '',
[EPrompts.SummarizeThread]: '',
// [EPrompts.ThreadLabels]: '',
// [EPrompts.Chat]: '',
};
const fallbackPrompts = {
[EPrompts.SummarizeMessage]: SummarizeMessage,
[EPrompts.ReSummarizeThread]: ReSummarizeThread,
[EPrompts.SummarizeThread]: SummarizeThread,
// [EPrompts.ThreadLabels]: '',
// [EPrompts.Chat]: '',
};
for (const promptType of Object.values(EPrompts)) {
const promptName = getPromptName(connectionId, promptType);
const prompt = await getPrompt(promptName, fallbackPrompts[promptType]);
prompts[promptType] = prompt;
}
return prompts;
};

View File

@@ -1,5 +1,5 @@
import { activeConnectionProcedure, brainServerAvailableMiddleware, router } from '../trpc';
import { disableBrainFunction, enableBrainFunction } from '../../lib/brain';
import { disableBrainFunction, enableBrainFunction, getPrompts } from '../../lib/brain';
import { env } from 'cloudflare:workers';
import { z } from 'zod';
@@ -105,6 +105,12 @@ export const brainRouter = router({
return [];
}
}),
getPrompts: activeConnectionProcedure
.use(brainServerAvailableMiddleware)
.query(async ({ ctx }) => {
const connection = ctx.activeConnection;
return await getPrompts({ connectionId: connection.id });
}),
updateLabels: activeConnectionProcedure
.use(brainServerAvailableMiddleware)
.input(

View File

@@ -155,3 +155,11 @@ export enum Tools {
}
export type AppContext = Context<{ Bindings: Env }>;
export enum EPrompts {
SummarizeMessage = 'SummarizeMessage',
ReSummarizeThread = 'ReSummarizeThread',
SummarizeThread = 'SummarizeThread',
// ThreadLabels = 'ThreadLabels',
// Chat = 'Chat',
}