feat: add daily brief functionality and integrate into AI chat panel
This commit is contained in:
@@ -1,10 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"shadcn": {
|
"shadcn": {
|
||||||
"command": "cmd",
|
"command": "npx",
|
||||||
"args": [
|
"args": [
|
||||||
"/c",
|
|
||||||
"npx",
|
|
||||||
"shadcn@latest",
|
"shadcn@latest",
|
||||||
"mcp"
|
"mcp"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,28 +99,21 @@ export function AIChatPanel({
|
|||||||
setDailyBrief(briefContentRef.current);
|
setDailyBrief(briefContentRef.current);
|
||||||
});
|
});
|
||||||
|
|
||||||
chatMutation.mutate(
|
briefMutation.mutate(undefined, {
|
||||||
{
|
onSuccess: (data) => {
|
||||||
message:
|
if (data.error) {
|
||||||
'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: () => {
|
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
setDailyBrief(null);
|
setDailyBrief(null);
|
||||||
setBriefLoading(false);
|
setBriefLoading(false);
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
);
|
onError: () => {
|
||||||
}, [isHomePage, hasTokenQuery.data]); // chatMutation excluded — only fire once
|
unsubscribe();
|
||||||
|
setDailyBrief(null);
|
||||||
|
setBriefLoading(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [isHomePage, hasTokenQuery.data]); // briefMutation excluded — only fire once
|
||||||
|
|
||||||
const handleSend = useCallback(() => {
|
const handleSend = useCallback(() => {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
@@ -256,53 +250,78 @@ 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 items-center justify-between gap-4 flex-wrap">
|
<div className="flex flex-col gap-1">
|
||||||
<h1 className="text-[30px] font-semibold" style={{ letterSpacing: '-1px' }}>
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||||
✦ Hello, {userName}
|
<h1 className="text-[30px] font-semibold" style={{ letterSpacing: '-1px' }}>
|
||||||
</h1>
|
✦ Hello, {userName}
|
||||||
<Badge variant="secondary">
|
</h1>
|
||||||
{dueCount} Task{dueCount !== 1 ? 's' : ''} due
|
<Badge variant="secondary">
|
||||||
</Badge>
|
{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>
|
</div>
|
||||||
|
|
||||||
{/* Daily brief card */}
|
{/* Inline input + suggestion chips */}
|
||||||
<Card>
|
<div>
|
||||||
<CardContent className="pt-6">
|
<ChatInput
|
||||||
{hasTokenQuery.data === false ? (
|
input={input}
|
||||||
<div className="flex flex-col items-center gap-3 py-2">
|
isStreaming={isStreaming || briefLoading}
|
||||||
<KeyRound size={24} className="text-muted-foreground" />
|
onInputChange={setInput}
|
||||||
<p className="text-sm text-muted-foreground text-center">
|
onKeyDown={handleKeyDown}
|
||||||
Configure your AI provider in Settings to enable the daily brief.
|
onSend={handleSend}
|
||||||
</p>
|
isHomePage={isHomePage}
|
||||||
<Button variant="outline" size="sm" onClick={onOpenSettings}>
|
/>
|
||||||
Open Settings
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mt-3">
|
||||||
</Button>
|
{SUGGESTION_CHIPS.map((chip) => (
|
||||||
</div>
|
<button
|
||||||
) : briefLoading && !dailyBrief ? (
|
key={chip.label}
|
||||||
<div className="space-y-2">
|
type="button"
|
||||||
<Skeleton className="h-4 w-3/4" />
|
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"
|
||||||
<Skeleton className="h-4 w-1/2" />
|
onClick={() => setInput(chip.label)}
|
||||||
<Skeleton className="h-4 w-2/3" />
|
>
|
||||||
</div>
|
<chip.icon size={16} className="shrink-0 mt-0.5 text-muted-foreground" />
|
||||||
) : dailyBrief ? (
|
<span>{chip.label}</span>
|
||||||
<ChatMarkdown content={dailyBrief} />
|
</button>
|
||||||
) : (
|
))}
|
||||||
<p className="text-sm text-muted-foreground">
|
</div>
|
||||||
Your daily brief will appear here.
|
</div>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</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} />
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chat messages */}
|
{/* Chat messages */}
|
||||||
@@ -440,37 +457,22 @@ export function AIChatPanel({
|
|||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Fixed input — pinned to the bottom */}
|
{/* Fixed input — pinned to the bottom (hidden on home initial state) */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-6 pb-4 pt-12 pointer-events-none">
|
{!(isHomePage && !hasMessages) && (
|
||||||
<div className="absolute inset-x-0 top-0 h-full bg-gradient-to-b from-transparent via-background/80 to-background" />
|
<div className="absolute bottom-0 left-0 right-0 z-10 px-6 pb-4 pt-12 pointer-events-none">
|
||||||
<div className={`relative pointer-events-auto mx-auto ${isHomePage ? 'max-w-3xl' : 'max-w-[1088px]'}`}>
|
<div className="absolute inset-x-0 top-0 h-full bg-gradient-to-b from-transparent via-background/80 to-background" />
|
||||||
<ChatInput
|
<div className={`relative pointer-events-auto mx-auto ${isHomePage ? 'max-w-3xl' : 'max-w-[1088px]'}`}>
|
||||||
input={input}
|
<ChatInput
|
||||||
isStreaming={isStreaming || briefLoading}
|
input={input}
|
||||||
onInputChange={setInput}
|
isStreaming={isStreaming || briefLoading}
|
||||||
onKeyDown={handleKeyDown}
|
onInputChange={setInput}
|
||||||
onSend={handleSend}
|
onKeyDown={handleKeyDown}
|
||||||
isHomePage={isHomePage}
|
onSend={handleSend}
|
||||||
/>
|
isHomePage={isHomePage}
|
||||||
|
/>
|
||||||
{/* Suggestion chips (home page only, before first message) */}
|
</div>
|
||||||
{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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user