feat: add daily brief functionality and integrate into AI chat panel

This commit is contained in:
Roberto Musso
2026-02-26 16:18:41 +01:00
parent f4eb278692
commit f09afd2d9e
4 changed files with 130 additions and 97 deletions

View File

@@ -61,6 +61,7 @@ export function AIChatPanel({
const streamingContentRef = useRef('');
const chatMutation = trpc.ai.chat.useMutation();
const briefMutation = trpc.ai.dailyBrief.useMutation();
const scrollToBottom = useCallback(() => {
const el = messagesContainerRef.current;
@@ -98,28 +99,21 @@ export function AIChatPanel({
setDailyBrief(briefContentRef.current);
});
chatMutation.mutate(
{
message:
'Give me a concise daily brief for today. Highlight tasks due today and this week, any overdue items, and recent project activity. Use **bold** for key phrases. Keep it to 3-5 sentences.',
context: { type: 'global' },
},
{
onSuccess: (data) => {
if (data.error) {
unsubscribe();
setDailyBrief(null);
setBriefLoading(false);
}
},
onError: () => {
briefMutation.mutate(undefined, {
onSuccess: (data) => {
if (data.error) {
unsubscribe();
setDailyBrief(null);
setBriefLoading(false);
},
}
},
);
}, [isHomePage, hasTokenQuery.data]); // chatMutation excluded — only fire once
onError: () => {
unsubscribe();
setDailyBrief(null);
setBriefLoading(false);
},
});
}, [isHomePage, hasTokenQuery.data]); // briefMutation excluded — only fire once
const handleSend = useCallback(() => {
const trimmed = input.trim();
@@ -256,53 +250,78 @@ export function AIChatPanel({
viewportRef={messagesContainerRef}
viewportClassName={
isHomePage && !hasMessages
? undefined
? '[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-center'
: '[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-end'
}
onWheel={handleWheel}
>
{/* Home page initial state: greeting + brief */}
{isHomePage && !hasMessages && (
<div className="mx-auto w-full max-w-3xl px-6 pt-8 pb-44">
<div className="flex flex-col gap-6">
{/* Greeting + badge */}
<div className="flex items-center justify-between gap-4 flex-wrap">
<h1 className="text-[30px] font-semibold" style={{ letterSpacing: '-1px' }}>
Hello, {userName}
</h1>
<Badge variant="secondary">
{dueCount} Task{dueCount !== 1 ? 's' : ''} due
</Badge>
<div className="mx-auto w-full max-w-3xl px-6 pt-8 pb-8">
<div className="flex flex-col gap-8">
{/* Greeting + brief grouped closely */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between gap-4 flex-wrap">
<h1 className="text-[30px] font-semibold" style={{ letterSpacing: '-1px' }}>
Hello, {userName}
</h1>
<Badge variant="secondary">
{dueCount} Task{dueCount !== 1 ? 's' : ''} due
</Badge>
</div>
{/* Daily brief */}
<div>
{hasTokenQuery.data === false ? (
<div className="flex flex-col items-center gap-3 py-2">
<KeyRound size={24} className="text-muted-foreground" />
<p className="text-sm text-muted-foreground text-center">
Configure your AI provider in Settings to enable the daily brief.
</p>
<Button variant="outline" size="sm" onClick={onOpenSettings}>
Open Settings
</Button>
</div>
) : briefLoading && !dailyBrief ? (
<div className="space-y-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-2/3" />
</div>
) : dailyBrief ? (
<ChatMarkdown content={dailyBrief} />
) : (
<p className="text-sm text-muted-foreground">
Your daily brief will appear here.
</p>
)}
</div>
</div>
{/* Daily brief card */}
<Card>
<CardContent className="pt-6">
{hasTokenQuery.data === false ? (
<div className="flex flex-col items-center gap-3 py-2">
<KeyRound size={24} className="text-muted-foreground" />
<p className="text-sm text-muted-foreground text-center">
Configure your AI provider in Settings to enable the daily brief.
</p>
<Button variant="outline" size="sm" onClick={onOpenSettings}>
Open Settings
</Button>
</div>
) : briefLoading && !dailyBrief ? (
<div className="space-y-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-2/3" />
</div>
) : dailyBrief ? (
<ChatMarkdown content={dailyBrief} />
) : (
<p className="text-sm text-muted-foreground">
Your daily brief will appear here.
</p>
)}
</CardContent>
</Card>
{/* Inline input + suggestion chips */}
<div>
<ChatInput
input={input}
isStreaming={isStreaming || briefLoading}
onInputChange={setInput}
onKeyDown={handleKeyDown}
onSend={handleSend}
isHomePage={isHomePage}
/>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mt-3">
{SUGGESTION_CHIPS.map((chip) => (
<button
key={chip.label}
type="button"
className="flex items-start gap-2 rounded-xl border border-border bg-background px-3 py-2.5 text-left text-sm text-foreground shadow-sm transition-colors hover:bg-muted cursor-pointer"
onClick={() => setInput(chip.label)}
>
<chip.icon size={16} className="shrink-0 mt-0.5 text-muted-foreground" />
<span>{chip.label}</span>
</button>
))}
</div>
</div>
</div>
</div>
)}
@@ -311,13 +330,11 @@ export function AIChatPanel({
{isHomePage && hasMessages && (
<div className="mx-auto w-full max-w-3xl px-6 pt-8 pb-44">
<div className="flex flex-col gap-4">
{/* Brief card persists */}
{/* Brief persists */}
{dailyBrief && (
<Card className="mb-2">
<CardContent className="pt-6">
<ChatMarkdown content={dailyBrief} />
</CardContent>
</Card>
<div className="mb-2">
<ChatMarkdown content={dailyBrief} />
</div>
)}
{/* Chat messages */}
@@ -440,37 +457,22 @@ export function AIChatPanel({
)}
</ScrollArea>
{/* Fixed input — pinned to the bottom */}
<div className="absolute bottom-0 left-0 right-0 z-10 px-6 pb-4 pt-12 pointer-events-none">
<div className="absolute inset-x-0 top-0 h-full bg-gradient-to-b from-transparent via-background/80 to-background" />
<div className={`relative pointer-events-auto mx-auto ${isHomePage ? 'max-w-3xl' : 'max-w-[1088px]'}`}>
<ChatInput
input={input}
isStreaming={isStreaming || briefLoading}
onInputChange={setInput}
onKeyDown={handleKeyDown}
onSend={handleSend}
isHomePage={isHomePage}
/>
{/* Suggestion chips (home page only, before first message) */}
{isHomePage && !hasMessages && (
<div className="flex flex-wrap gap-2 mt-3">
{SUGGESTION_CHIPS.map((chip) => (
<Button
key={chip.label}
variant="outline"
className="gap-2 h-auto py-2.5 px-4 text-left"
onClick={() => setInput(chip.label)}
>
<chip.icon size={16} className="shrink-0" />
<span className="text-sm">{chip.label}</span>
</Button>
))}
</div>
)}
{/* Fixed input — pinned to the bottom (hidden on home initial state) */}
{!(isHomePage && !hasMessages) && (
<div className="absolute bottom-0 left-0 right-0 z-10 px-6 pb-4 pt-12 pointer-events-none">
<div className="absolute inset-x-0 top-0 h-full bg-gradient-to-b from-transparent via-background/80 to-background" />
<div className={`relative pointer-events-auto mx-auto ${isHomePage ? 'max-w-3xl' : 'max-w-[1088px]'}`}>
<ChatInput
input={input}
isStreaming={isStreaming || briefLoading}
onInputChange={setInput}
onKeyDown={handleKeyDown}
onSend={handleSend}
isHomePage={isHomePage}
/>
</div>
</div>
</div>
)}
</div>
);
}