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

@@ -1,10 +1,8 @@
{ {
"mcpServers": { "mcpServers": {
"shadcn": { "shadcn": {
"command": "cmd", "command": "npx",
"args": [ "args": [
"/c",
"npx",
"shadcn@latest", "shadcn@latest",
"mcp" "mcp"
] ]

View File

@@ -946,3 +946,27 @@ export async function orchestrate(input: OrchestrateInput): Promise<OrchestrateR
return { response: '', error: errMsg }; return { response: '', error: errMsg };
} }
} }
// ---------------------------------------------------------------------------
// Daily Brief (dedicated entry point)
// ---------------------------------------------------------------------------
const DAILY_BRIEF_PROMPT =
`Act as a professional and efficient executive assistant. Give me a concise daily brief for today.
Strict Rules:
- Adopt a polite, formal, and helpful tone. Do not use emojis, slang, or overly casual encouragement.
- Focus strictly on actionable or critical items: tasks due today, upcoming deadlines this week, overdue items, and significant project activity.
- Do NOT mention zero-counts (e.g., "no overdue items") or general statistics (e.g., "2 active projects", "2 completed tasks"). Only report what needs my attention.
- Do NOT include any headers, titles, dates, or greetings.
- Do NOT use labels like "Due today:" or "Overdue:". Integrate the information naturally into sentences.
- Use **bold** for key phrases, task names, or project names.
- Keep the entire response to 3-5 sentences.`;
export async function dailyBrief(sender?: Electron.WebContents): Promise<OrchestrateResult> {
return orchestrate({
message: DAILY_BRIEF_PROMPT,
context: { type: 'global' },
sender,
});
}

View File

@@ -6,7 +6,7 @@ import { getDb } from '../db';
import { clients, projects, tasks, checkpoints, notes, taskComments } from '../db/schema'; import { clients, projects, tasks, checkpoints, notes, taskComments } from '../db/schema';
import { getStore } from '../store'; import { getStore } from '../store';
import { saveTokenAndInit, hasActiveToken } from '../ai/provider'; import { saveTokenAndInit, hasActiveToken } from '../ai/provider';
import { orchestrate } from '../ai/orchestrator'; import { orchestrate, dailyBrief } from '../ai/orchestrator';
import { upsertNoteEmbedding } from '../db/vectordb'; import { upsertNoteEmbedding } from '../db/vectordb';
import type { TRPCContext } from '../ipc'; import type { TRPCContext } from '../ipc';
@@ -572,6 +572,15 @@ const aiRouter = router({
await saveTokenAndInit(input.token); await saveTokenAndInit(input.token);
return { success: true }; return { success: true };
}), }),
dailyBrief: publicProcedure
.mutation(async ({ ctx }) => {
try {
return await dailyBrief(ctx.sender);
} catch (err) {
const msg = err instanceof Error ? err.message : 'Unknown error';
return { response: '', error: msg };
}
}),
hasToken: publicProcedure.query(async () => { hasToken: publicProcedure.query(async () => {
return hasActiveToken(); return hasActiveToken();
}), }),

View File

@@ -61,6 +61,7 @@ export function AIChatPanel({
const streamingContentRef = useRef(''); const streamingContentRef = useRef('');
const chatMutation = trpc.ai.chat.useMutation(); const chatMutation = trpc.ai.chat.useMutation();
const briefMutation = trpc.ai.dailyBrief.useMutation();
const scrollToBottom = useCallback(() => { const scrollToBottom = useCallback(() => {
const el = messagesContainerRef.current; const el = messagesContainerRef.current;
@@ -98,13 +99,7 @@ export function AIChatPanel({
setDailyBrief(briefContentRef.current); setDailyBrief(briefContentRef.current);
}); });
chatMutation.mutate( briefMutation.mutate(undefined, {
{
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) => { onSuccess: (data) => {
if (data.error) { if (data.error) {
unsubscribe(); unsubscribe();
@@ -117,9 +112,8 @@ export function AIChatPanel({
setDailyBrief(null); setDailyBrief(null);
setBriefLoading(false); setBriefLoading(false);
}, },
}, });
); }, [isHomePage, hasTokenQuery.data]); // briefMutation excluded — only fire once
}, [isHomePage, hasTokenQuery.data]); // chatMutation excluded — only fire once
const handleSend = useCallback(() => { const handleSend = useCallback(() => {
const trimmed = input.trim(); const trimmed = input.trim();
@@ -256,16 +250,17 @@ export function AIChatPanel({
viewportRef={messagesContainerRef} viewportRef={messagesContainerRef}
viewportClassName={ viewportClassName={
isHomePage && !hasMessages 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' : '[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-end'
} }
onWheel={handleWheel} onWheel={handleWheel}
> >
{/* Home page initial state: greeting + brief */} {/* Home page initial state: greeting + brief */}
{isHomePage && !hasMessages && ( {isHomePage && !hasMessages && (
<div className="mx-auto w-full max-w-3xl px-6 pt-8 pb-44"> <div className="mx-auto w-full max-w-3xl px-6 pt-8 pb-8">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-8">
{/* Greeting + badge */} {/* Greeting + brief grouped closely */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between gap-4 flex-wrap"> <div className="flex items-center justify-between gap-4 flex-wrap">
<h1 className="text-[30px] font-semibold" style={{ letterSpacing: '-1px' }}> <h1 className="text-[30px] font-semibold" style={{ letterSpacing: '-1px' }}>
Hello, {userName} Hello, {userName}
@@ -275,9 +270,8 @@ export function AIChatPanel({
</Badge> </Badge>
</div> </div>
{/* Daily brief card */} {/* Daily brief */}
<Card> <div>
<CardContent className="pt-6">
{hasTokenQuery.data === false ? ( {hasTokenQuery.data === false ? (
<div className="flex flex-col items-center gap-3 py-2"> <div className="flex flex-col items-center gap-3 py-2">
<KeyRound size={24} className="text-muted-foreground" /> <KeyRound size={24} className="text-muted-foreground" />
@@ -301,8 +295,33 @@ export function AIChatPanel({
Your daily brief will appear here. Your daily brief will appear here.
</p> </p>
)} )}
</CardContent> </div>
</Card> </div>
{/* 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>
</div> </div>
)} )}
@@ -311,13 +330,11 @@ export function AIChatPanel({
{isHomePage && hasMessages && ( {isHomePage && hasMessages && (
<div className="mx-auto w-full max-w-3xl px-6 pt-8 pb-44"> <div className="mx-auto w-full max-w-3xl px-6 pt-8 pb-44">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Brief card persists */} {/* Brief persists */}
{dailyBrief && ( {dailyBrief && (
<Card className="mb-2"> <div className="mb-2">
<CardContent className="pt-6">
<ChatMarkdown content={dailyBrief} /> <ChatMarkdown content={dailyBrief} />
</CardContent> </div>
</Card>
)} )}
{/* Chat messages */} {/* Chat messages */}
@@ -440,7 +457,8 @@ export function AIChatPanel({
)} )}
</ScrollArea> </ScrollArea>
{/* Fixed input — pinned to the bottom */} {/* 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 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="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]'}`}> <div className={`relative pointer-events-auto mx-auto ${isHomePage ? 'max-w-3xl' : 'max-w-[1088px]'}`}>
@@ -452,26 +470,10 @@ export function AIChatPanel({
onSend={handleSend} onSend={handleSend}
isHomePage={isHomePage} isHomePage={isHomePage}
/> />
</div>
{/* 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> </div>
)} )}
</div> </div>
</div>
</div>
); );
} }