Skip to content

Commit f20a455

Browse files
authored
Perfect Meetings: full-width prep dashboard and independent research (#124)
Make meeting prep understandable from the first visit: explain what the app does, put **Begin Sync** before the checklist, and show whether calendar sync and research are actually progressing. All custom-block states fill their container, with responsive profile cards and quieter status reporting. - Redesign onboarding, empty states, Today/Next meeting navigation, and people/company cards with shadcn components styled by Notion tokens. Move low-confidence matching into a **Possible match** badge and remove duplicate profile-source links. - Track calendar sync separately from people/company research. Distinguish active, queued, completed, failed, stalled, loading, unavailable, and partial results; retain the last good data after refresh errors. - Research People and Companies through native agent property triggers. Keep each meeting brief queued until its participant and company research finishes, then load the completed profiles and hand off the brief. Bound durable waits, report failed research, and discard stale requests without overwriting newer work. Preserve notes throughout. - Temporarily remove Gmail from preparation workflows. Keep calendar history and share the automatic-refresh schedule between workflows and UI copy. - Restructure `APP.md` around Today, supporting library links, and collapsed usage instructions. ### Verification - `npm run check`, `npm test` (**168 tests**), and `npm run build` passed. - The custom-block Vite production build passed. - Browser checks cover the local host, sync-request feedback, full-width layouts, and light/dark and narrow layouts. Verified that queued briefs show “waiting for research” without an active writing spinner. - Repository verification passed catalog validation, agent checks/tests, project typechecks/tests, and Markdown lint. Its final formatting check only flagged ignored local `app.json` and dev-shell cache metadata. - Live calendar access, deployed agent execution, and deployment were not exercised by these local checks. ### Screenshots Five representative states, captured locally with synthetic data. <details open> <summary>Ready — meeting prep and researched profiles</summary> ![Ready meeting with company and participant profiles](https://github.com/user-attachments/assets/7a1cfafe-cdd2-4216-86df-4a45f34426ac) </details> <details> <summary>First run — explanation and Begin Sync</summary> ![First-run onboarding with Begin Sync before the checklist](https://github.com/user-attachments/assets/941dc4b6-6356-4743-a224-34253450abeb) </details> <details> <summary>In progress — calendar, people, companies, and briefs</summary> ![Profile research in progress with meeting briefs waiting for research](https://github.com/user-attachments/assets/8a532486-8ca4-471c-bcca-a4544992a9c8) </details> <details> <summary>Empty — calendar checked, no upcoming outside meetings</summary> ![Empty calendar with Sync now and automatic update status](https://github.com/user-attachments/assets/91c2c117-60ea-476a-8244-a5287b0668b4) </details> <details> <summary>Error — actionable calendar sync recovery</summary> ![Calendar sync failed with an explanation and Retry sync](https://github.com/user-attachments/assets/f2883053-c89e-44ab-9fd0-6d04da565b86) </details>
1 parent c0c147c commit f20a455

48 files changed

Lines changed: 4001 additions & 2022 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/templates/perfect-meetings/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ app.json
33
workers.json
44
dist/
55
node_modules/
6+
.dev-shell/
7+
data/worker_*.json
68
package-lock.json
79
.env
810
.env.*
Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
Prepare for Google Calendar meetings with people outside your organization, with a brief on the people and companies you'll meet.
2+
3+
## Today
4+
15
<database inline="true" data-source-url="{{meetings-next-view}}">Today</database>
26

7+
## Your library
8+
9+
- <mention-page url="{{meetings-db}}">Meetings</mention-page> — Meeting briefs and your notes.
10+
- <mention-page url="{{people-db}}">People</mention-page> — Get to know the people you're meeting.
11+
- <mention-page url="{{companies-db}}">Companies</mention-page> — Background on their companies.
12+
313
<details>
4-
<summary>Debug Data</summary>
5-
<mention-page url="{{sync-runs-db}}">Workflow runs</mention-page> — Run history of background tasks
6-
<mention-page url="{{meetings-db}}">Meetings</mention-page> — Calendar events synced with Notion
7-
<mention-page url="{{companies-db}}">Companies</mention-page> — Companies you meet with
8-
<mention-page url="{{people-db}}">People</mention-page> — People you meet with
14+
<summary>How it works</summary>
15+
**Before a meeting.** Choose a meeting in Today, then select **Open prep**. Add your thoughts under **Notes**; refreshes preserve them.
16+
**Updates happen automatically.** Your calendar syncs every hour, and today's briefs refresh each morning after participant and company research finishes. Progress appears above as it happens.
17+
**Need a fresh brief?** Open the meeting page and check **Regenerate prep** to refresh or retry it.
18+
<mention-page url="{{sync-runs-db}}">Workflow runs</mention-page> — Calendar sync history, if you need to check what happened.
919
</details>

‎apps/templates/perfect-meetings/README.md‎

Lines changed: 152 additions & 74 deletions
Large diffs are not rendered by default.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { cn } from "../../lib/utils"
5+
import { Avatar as AvatarPrimitive } from "radix-ui"
6+
7+
function Avatar({
8+
className,
9+
size = "default",
10+
...props
11+
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
12+
size?: "default" | "sm" | "lg"
13+
}) {
14+
return (
15+
<AvatarPrimitive.Root
16+
data-slot="avatar"
17+
data-size={size}
18+
className={cn(
19+
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
20+
className
21+
)}
22+
{...props}
23+
/>
24+
)
25+
}
26+
27+
function AvatarImage({
28+
className,
29+
...props
30+
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
31+
return (
32+
<AvatarPrimitive.Image
33+
data-slot="avatar-image"
34+
className={cn("aspect-square size-full", className)}
35+
{...props}
36+
/>
37+
)
38+
}
39+
40+
function AvatarFallback({
41+
className,
42+
...props
43+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
44+
return (
45+
<AvatarPrimitive.Fallback
46+
data-slot="avatar-fallback"
47+
className={cn(
48+
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
49+
className
50+
)}
51+
{...props}
52+
/>
53+
)
54+
}
55+
56+
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
57+
return (
58+
<span
59+
data-slot="avatar-badge"
60+
className={cn(
61+
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background select-none",
62+
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
63+
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
64+
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
65+
className
66+
)}
67+
{...props}
68+
/>
69+
)
70+
}
71+
72+
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
73+
return (
74+
<div
75+
data-slot="avatar-group"
76+
className={cn(
77+
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
78+
className
79+
)}
80+
{...props}
81+
/>
82+
)
83+
}
84+
85+
function AvatarGroupCount({
86+
className,
87+
...props
88+
}: React.ComponentProps<"div">) {
89+
return (
90+
<div
91+
data-slot="avatar-group-count"
92+
className={cn(
93+
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
94+
className
95+
)}
96+
{...props}
97+
/>
98+
)
99+
}
100+
101+
export {
102+
Avatar,
103+
AvatarImage,
104+
AvatarFallback,
105+
AvatarBadge,
106+
AvatarGroup,
107+
AvatarGroupCount,
108+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
import { cn } from "../../lib/utils"
4+
import { Slot } from "radix-ui"
5+
6+
const badgeVariants = cva(
7+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
8+
{
9+
variants: {
10+
variant: {
11+
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
12+
secondary:
13+
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
14+
destructive:
15+
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
16+
outline:
17+
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
18+
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19+
link: "text-primary underline-offset-4 [a&]:hover:underline",
20+
},
21+
},
22+
defaultVariants: {
23+
variant: "default",
24+
},
25+
}
26+
)
27+
28+
function Badge({
29+
className,
30+
variant = "default",
31+
asChild = false,
32+
...props
33+
}: React.ComponentProps<"span"> &
34+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35+
const Comp = asChild ? Slot.Root : "span"
36+
37+
return (
38+
<Comp
39+
data-slot="badge"
40+
data-variant={variant}
41+
className={cn(badgeVariants({ variant }), className)}
42+
{...props}
43+
/>
44+
)
45+
}
46+
47+
export { Badge, badgeVariants }
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
import { cn } from "../../lib/utils"
4+
import { Slot } from "radix-ui"
5+
6+
const buttonVariants = cva(
7+
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8+
{
9+
variants: {
10+
variant: {
11+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
12+
destructive:
13+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
14+
outline:
15+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
16+
secondary:
17+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
18+
ghost:
19+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
20+
link: "text-primary underline-offset-4 hover:underline",
21+
},
22+
size: {
23+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
24+
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
25+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
26+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27+
icon: "size-9",
28+
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
29+
"icon-sm": "size-8",
30+
"icon-lg": "size-10",
31+
},
32+
},
33+
defaultVariants: {
34+
variant: "default",
35+
size: "default",
36+
},
37+
}
38+
)
39+
40+
function Button({
41+
className,
42+
variant = "default",
43+
size = "default",
44+
asChild = false,
45+
...props
46+
}: React.ComponentProps<"button"> &
47+
VariantProps<typeof buttonVariants> & {
48+
asChild?: boolean
49+
}) {
50+
const Comp = asChild ? Slot.Root : "button"
51+
52+
return (
53+
<Comp
54+
data-slot="button"
55+
data-variant={variant}
56+
data-size={size}
57+
className={cn(buttonVariants({ variant, size, className }))}
58+
{...props}
59+
/>
60+
)
61+
}
62+
63+
export { Button, buttonVariants }
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as React from "react"
2+
import { cn } from "../../lib/utils"
3+
4+
function Card({ className, ...props }: React.ComponentProps<"div">) {
5+
return (
6+
<div
7+
data-slot="card"
8+
className={cn(
9+
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
10+
className
11+
)}
12+
{...props}
13+
/>
14+
)
15+
}
16+
17+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
18+
return (
19+
<div
20+
data-slot="card-header"
21+
className={cn(
22+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
23+
className
24+
)}
25+
{...props}
26+
/>
27+
)
28+
}
29+
30+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
31+
return (
32+
<div
33+
data-slot="card-title"
34+
className={cn("leading-none font-semibold", className)}
35+
{...props}
36+
/>
37+
)
38+
}
39+
40+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
41+
return (
42+
<div
43+
data-slot="card-description"
44+
className={cn("text-sm text-muted-foreground", className)}
45+
{...props}
46+
/>
47+
)
48+
}
49+
50+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
51+
return (
52+
<div
53+
data-slot="card-action"
54+
className={cn(
55+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
56+
className
57+
)}
58+
{...props}
59+
/>
60+
)
61+
}
62+
63+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
64+
return (
65+
<div
66+
data-slot="card-content"
67+
className={cn("px-6", className)}
68+
{...props}
69+
/>
70+
)
71+
}
72+
73+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
74+
return (
75+
<div
76+
data-slot="card-footer"
77+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
78+
{...props}
79+
/>
80+
)
81+
}
82+
83+
export {
84+
Card,
85+
CardHeader,
86+
CardFooter,
87+
CardTitle,
88+
CardAction,
89+
CardDescription,
90+
CardContent,
91+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from "react"
2+
import { cn } from "../../lib/utils"
3+
import { Progress as ProgressPrimitive } from "radix-ui"
4+
5+
function Progress({
6+
className,
7+
value,
8+
...props
9+
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
10+
return (
11+
<ProgressPrimitive.Root
12+
data-slot="progress"
13+
className={cn(
14+
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
15+
className
16+
)}
17+
value={value}
18+
{...props}
19+
>
20+
<ProgressPrimitive.Indicator
21+
data-slot="progress-indicator"
22+
className="h-full w-full flex-1 bg-primary transition-all"
23+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
24+
/>
25+
</ProgressPrimitive.Root>
26+
)
27+
}
28+
29+
export { Progress }

0 commit comments

Comments
 (0)