feat: add daily brief functionality and integrate into AI chat panel
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}),
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user