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": {
"shadcn": {
"command": "cmd",
"command": "npx",
"args": [
"/c",
"npx",
"shadcn@latest",
"mcp"
]

View File

@@ -946,3 +946,27 @@ export async function orchestrate(input: OrchestrateInput): Promise<OrchestrateR
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 { getStore } from '../store';
import { saveTokenAndInit, hasActiveToken } from '../ai/provider';
import { orchestrate } from '../ai/orchestrator';
import { orchestrate, dailyBrief } from '../ai/orchestrator';
import { upsertNoteEmbedding } from '../db/vectordb';
import type { TRPCContext } from '../ipc';
@@ -572,6 +572,15 @@ const aiRouter = router({
await saveTokenAndInit(input.token);
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 () => {
return hasActiveToken();
}),

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,13 +99,7 @@ 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' },
},
{
briefMutation.mutate(undefined, {
onSuccess: (data) => {
if (data.error) {
unsubscribe();
@@ -117,9 +112,8 @@ export function AIChatPanel({
setDailyBrief(null);
setBriefLoading(false);
},
},
);
}, [isHomePage, hasTokenQuery.data]); // chatMutation excluded — only fire once
});
}, [isHomePage, hasTokenQuery.data]); // briefMutation excluded — only fire once
const handleSend = useCallback(() => {
const trimmed = input.trim();
@@ -256,16 +250,17 @@ 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="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}
@@ -275,9 +270,8 @@ export function AIChatPanel({
</Badge>
</div>
{/* Daily brief card */}
<Card>
<CardContent className="pt-6">
{/* Daily brief */}
<div>
{hasTokenQuery.data === false ? (
<div className="flex flex-col items-center gap-3 py-2">
<KeyRound size={24} className="text-muted-foreground" />
@@ -301,8 +295,33 @@ export function AIChatPanel({
Your daily brief will appear here.
</p>
)}
</CardContent>
</Card>
</div>
</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>
)}
@@ -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">
<div className="mb-2">
<ChatMarkdown content={dailyBrief} />
</CardContent>
</Card>
</div>
)}
{/* Chat messages */}
@@ -440,7 +457,8 @@ export function AIChatPanel({
)}
</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 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]'}`}>
@@ -452,26 +470,10 @@ export function AIChatPanel({
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>
</div>
)}
</div>
</div>
</div>
);
}